release-skill 0.2.6 → 0.2.7
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +21 -0
- package/INSTALL.md +12 -12
- package/INSTALL.zh-CN.md +10 -10
- package/README.md +26 -11
- package/README.zh-CN.md +22 -11
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +1579 -1186
- package/adapters/claude/skills/release-prepare/SKILL.md +5 -0
- package/adapters/claude/skills/release-setup/SKILL.md +10 -1
- package/adapters/claude/skills/release-verify/SKILL.md +4 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1579 -1186
- package/adapters/codex/skills/release-prepare/SKILL.md +5 -0
- package/adapters/codex/skills/release-setup/SKILL.md +10 -1
- package/adapters/codex/skills/release-verify/SKILL.md +4 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1579 -1186
- package/adapters/kimi/skills/release-prepare/SKILL.md +5 -0
- package/adapters/kimi/skills/release-setup/SKILL.md +10 -1
- package/adapters/kimi/skills/release-verify/SKILL.md +4 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1579 -1186
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +10 -1
- package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -2
- package/bin/release-skill.bundle.mjs +1579 -1186
- package/package.json +1 -1
- package/skills/release-prepare/SKILL.md +5 -0
- package/skills/release-setup/SKILL.md +10 -1
- package/skills/release-verify/SKILL.md +4 -2
- package/skills-src/release-prepare/SKILL.md +5 -0
- package/skills-src/release-setup/SKILL.md +10 -1
- package/skills-src/release-verify/SKILL.md +4 -2
- package/src/adapters/npm.mjs +54 -2
- package/src/adapters/plugin-marketplace.mjs +4 -29
- package/src/commands/prepare.mjs +3 -3
- package/src/commands/publish.mjs +2 -2
- package/src/commands/reconcile.mjs +39 -0
- package/src/commands/setup.mjs +168 -0
- package/src/commands/verify.mjs +28 -1
- package/src/npm/npm-entry-closure.mjs +195 -0
- package/src/platforms/codebuddy.mjs +19 -11
- package/src/platforms/codex.mjs +18 -10
- package/src/platforms/kimi.mjs +26 -39
- package/src/snapshot/frozen.mjs +51 -19
package/package.json
CHANGED
|
@@ -36,6 +36,10 @@ description: Freeze an immutable release plan with local configuration, document
|
|
|
36
36
|
|
|
37
37
|
若用户明确要求 GitHub+npm 生产发布,加入 `--production`。该模式还会封存独立
|
|
38
38
|
Git commit/tree 和 npm tarball,并把路径、SHA/integrity、branch/tag 写入计划。
|
|
39
|
+
每个 npm tarball 在计划落盘前必须静态验证 `package.json` 的具体
|
|
40
|
+
`bin`/`main`/`module`/`types`/`typings`/`exports` 入口均为 tarball 内普通文件;
|
|
41
|
+
该门禁不依赖项目是否配置 `requiredPublicFiles` 或 `smokeBin`。通配符 exports 不做
|
|
42
|
+
猜测展开;它与 fallback array 都属于首版最小边界外的阻断形态。
|
|
39
43
|
每个 release unit 必须显式配置 `previousPublicBaseline`。只有确认不存在前序公开
|
|
40
44
|
版本时用 `mode: none`;已有版本必须用 `mode: bound` + 精确 repo/ref/commit,并以
|
|
41
45
|
`--online --production` 逐 unit 观察 ref→commit mapping。默认 observer 不下载远端
|
|
@@ -79,6 +83,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offli
|
|
|
79
83
|
| GATE_FAILED (gate 授权) | 向用户展示 snapshot gate 命令和风险,获得授权后加 `--acknowledge-gate-side-effects` 重试 |
|
|
80
84
|
| GATE_FAILED (bound + offline) | 改用 `--online --production`,不得把 unobserved-offline plan 交给 publish |
|
|
81
85
|
| GATE_FAILED (前序基线漂移) | 先取得并比较实际远端内容;人工选择 merge/adopt/reject。merge/adopt 都必须把接受内容落回 human-owned 权威源,并把 `previousPublicBaseline` 更新为接受状态的精确 repo/ref/commit 后重新 online production prepare;reject 停止调查,禁止改 `mode: none` 绕过 |
|
|
86
|
+
| GATE_FAILED (`npm-entry-closure`) | 修复打包内容或入口声明后重新 prepare;不得用 `requiredPublicFiles`/`smokeBin` 缺省绕过 |
|
|
82
87
|
| GATE_FAILED (其他) | 修复门失败原因后重试;以 CLI exit code 为准 |
|
|
83
88
|
| RELEASE_DOCS_STALE | 文档相对说明源已陈旧;按详情运行只读演练,展示文件/语种/版本/摘要,经用户授权“本地发布文档写入”后执行写入,审阅提交再重新 prepare |
|
|
84
89
|
| RELEASE_DOCS_INVALID / TRANSLATION_MISSING / CONFLICT / REFRESH_STALE | 修复配置/说明源/目标或重新演练取得新 `refreshDigest`;不得扩大写入范围绕过 |
|
|
@@ -14,6 +14,9 @@ description: "首次接入 release-skill:只读发现发布单元与个性化
|
|
|
14
14
|
- 默认只读。README、slogan、CHANGELOG、业务脚本和已有配置均为人工权威内容,不得重写或覆盖。
|
|
15
15
|
- 发现脚本不等于选择或授权。间接脚本以 `SIDE_EFFECTS_UNPROVEN` 排除;项目特有 hook/gate 只能人工增量注册。
|
|
16
16
|
- 仓库、公开文件等证据冲突时停止自动提案,交给人工修正权威事实后重新发现。
|
|
17
|
+
- npm 单元只读提取 `package.json` 的具体入口和旧 `npmRequiredPackagePaths`,报告
|
|
18
|
+
tracked/untracked/ignored/missing/non-regular 状态与覆盖漂移;这些是人工复核候选,
|
|
19
|
+
不得自动写入 `publicFiles` 或 `requiredPublicFiles`。
|
|
17
20
|
- 写入只允许 create-once(仅创建一次),必须同时提供 answers 与精确 `setupDigest`;无安全原生写入能力时失败关闭。
|
|
18
21
|
- Agent 的多次 shell 调用彼此独立;只能用首轮打印的会话目录绝对路径续接,不得假设变量仍存在。
|
|
19
22
|
|
|
@@ -43,7 +46,13 @@ node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.
|
|
|
43
46
|
node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if((r.proposalConflicts??[]).length){console.error("proposal conflicts require human resolution");process.exit(2)}if(!r.recommendedAnswers){console.error("recommendedAnswers missing");process.exit(2)}fs.writeFileSync(process.argv[2],JSON.stringify(r.recommendedAnswers,null,2)+"\n",{flag:"wx",mode:0o600})' "$REPORT" "$ANSWERS"
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
若 `proposalConflicts`
|
|
49
|
+
若 `proposalConflicts` 非空,暂停,让用户修正仓库/映射权威事实或 npm 入口候选,
|
|
50
|
+
删除本会话目录后从第 2 步重跑,不得猜测选边。构建后才产生且被忽略的入口应先由
|
|
51
|
+
人工确认构建流程与最终 tarball 边界,不能因为 setup 发现了路径就自动信任或纳入公开
|
|
52
|
+
配置。无冲突时可人工增量编辑 `answers.json`:hook 写入
|
|
53
|
+
`recommendedAnswers.projectConfig.hooks` 对应的 `projectConfig.hooks`;gate 写入
|
|
54
|
+
`verificationGates`,并把同一 id 加入 `selectedGateIds`。人工文件保持
|
|
55
|
+
`mode: preserve`,跨单元共享源使用 `sourceScope: workspace`。
|
|
47
56
|
|
|
48
57
|
3. 使用上一步打印的两个绝对字面量重新赋值,运行绑定 dry-run;任何人工编辑后都必须重跑本步:
|
|
49
58
|
|
|
@@ -17,7 +17,7 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
|
|
|
17
17
|
|
|
18
18
|
## 职责与边界
|
|
19
19
|
|
|
20
|
-
验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm
|
|
20
|
+
验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、静态入口闭包,并在配置时验证 bin 路径安全和 CLI 烟雾输出。对每个声明的 marketplace distribution 执行全新隔离消费者安装验证。
|
|
21
21
|
|
|
22
22
|
**阶段通过规则**: 只有 CLI exit code 0 和结构化状态码 `VERIFIED` 才是完整终态。
|
|
23
23
|
|
|
@@ -55,8 +55,10 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
|
|
|
55
55
|
- 在 `os.tmpdir()` 创建隔离目录
|
|
56
56
|
- 执行 `npm install <package>@<version>` 带安全标志
|
|
57
57
|
- 验证安装的 `package.json` name 和 version 精确匹配
|
|
58
|
+
- 无条件静态验证已安装包中具体 `bin`/`main`/`module`/`types`/`typings`/`exports`
|
|
59
|
+
目标是普通文件;失败时不得写入 `VERIFIED`
|
|
58
60
|
- 若配置了 `smokeBin`:验证 bin 路径安全(无逃逸、无 symlink),从精确安装根、隔离 HOME 和最小环境执行并验证输出;这会运行已安装代码,必须显式授权
|
|
59
|
-
- 若未配置 `smokeBin
|
|
61
|
+
- 若未配置 `smokeBin`:跳过运行代码,但静态入口闭包检查仍然强制执行
|
|
60
62
|
|
|
61
63
|
## 常见错误
|
|
62
64
|
|
|
@@ -36,6 +36,10 @@ description: Freeze an immutable release plan with local configuration, document
|
|
|
36
36
|
|
|
37
37
|
若用户明确要求 GitHub+npm 生产发布,加入 `--production`。该模式还会封存独立
|
|
38
38
|
Git commit/tree 和 npm tarball,并把路径、SHA/integrity、branch/tag 写入计划。
|
|
39
|
+
每个 npm tarball 在计划落盘前必须静态验证 `package.json` 的具体
|
|
40
|
+
`bin`/`main`/`module`/`types`/`typings`/`exports` 入口均为 tarball 内普通文件;
|
|
41
|
+
该门禁不依赖项目是否配置 `requiredPublicFiles` 或 `smokeBin`。通配符 exports 不做
|
|
42
|
+
猜测展开;它与 fallback array 都属于首版最小边界外的阻断形态。
|
|
39
43
|
每个 release unit 必须显式配置 `previousPublicBaseline`。只有确认不存在前序公开
|
|
40
44
|
版本时用 `mode: none`;已有版本必须用 `mode: bound` + 精确 repo/ref/commit,并以
|
|
41
45
|
`--online --production` 逐 unit 观察 ref→commit mapping。默认 observer 不下载远端
|
|
@@ -79,6 +83,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offli
|
|
|
79
83
|
| GATE_FAILED (gate 授权) | 向用户展示 snapshot gate 命令和风险,获得授权后加 `--acknowledge-gate-side-effects` 重试 |
|
|
80
84
|
| GATE_FAILED (bound + offline) | 改用 `--online --production`,不得把 unobserved-offline plan 交给 publish |
|
|
81
85
|
| GATE_FAILED (前序基线漂移) | 先取得并比较实际远端内容;人工选择 merge/adopt/reject。merge/adopt 都必须把接受内容落回 human-owned 权威源,并把 `previousPublicBaseline` 更新为接受状态的精确 repo/ref/commit 后重新 online production prepare;reject 停止调查,禁止改 `mode: none` 绕过 |
|
|
86
|
+
| GATE_FAILED (`npm-entry-closure`) | 修复打包内容或入口声明后重新 prepare;不得用 `requiredPublicFiles`/`smokeBin` 缺省绕过 |
|
|
82
87
|
| GATE_FAILED (其他) | 修复门失败原因后重试;以 CLI exit code 为准 |
|
|
83
88
|
| RELEASE_DOCS_STALE | 文档相对说明源已陈旧;按详情运行只读演练,展示文件/语种/版本/摘要,经用户授权“本地发布文档写入”后执行写入,审阅提交再重新 prepare |
|
|
84
89
|
| RELEASE_DOCS_INVALID / TRANSLATION_MISSING / CONFLICT / REFRESH_STALE | 修复配置/说明源/目标或重新演练取得新 `refreshDigest`;不得扩大写入范围绕过 |
|
|
@@ -14,6 +14,9 @@ description: "首次接入 release-skill:只读发现发布单元与个性化
|
|
|
14
14
|
- 默认只读。README、slogan、CHANGELOG、业务脚本和已有配置均为人工权威内容,不得重写或覆盖。
|
|
15
15
|
- 发现脚本不等于选择或授权。间接脚本以 `SIDE_EFFECTS_UNPROVEN` 排除;项目特有 hook/gate 只能人工增量注册。
|
|
16
16
|
- 仓库、公开文件等证据冲突时停止自动提案,交给人工修正权威事实后重新发现。
|
|
17
|
+
- npm 单元只读提取 `package.json` 的具体入口和旧 `npmRequiredPackagePaths`,报告
|
|
18
|
+
tracked/untracked/ignored/missing/non-regular 状态与覆盖漂移;这些是人工复核候选,
|
|
19
|
+
不得自动写入 `publicFiles` 或 `requiredPublicFiles`。
|
|
17
20
|
- 写入只允许 create-once(仅创建一次),必须同时提供 answers 与精确 `setupDigest`;无安全原生写入能力时失败关闭。
|
|
18
21
|
- Agent 的多次 shell 调用彼此独立;只能用首轮打印的会话目录绝对路径续接,不得假设变量仍存在。
|
|
19
22
|
|
|
@@ -43,7 +46,13 @@ node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.
|
|
|
43
46
|
node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if((r.proposalConflicts??[]).length){console.error("proposal conflicts require human resolution");process.exit(2)}if(!r.recommendedAnswers){console.error("recommendedAnswers missing");process.exit(2)}fs.writeFileSync(process.argv[2],JSON.stringify(r.recommendedAnswers,null,2)+"\n",{flag:"wx",mode:0o600})' "$REPORT" "$ANSWERS"
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
若 `proposalConflicts`
|
|
49
|
+
若 `proposalConflicts` 非空,暂停,让用户修正仓库/映射权威事实或 npm 入口候选,
|
|
50
|
+
删除本会话目录后从第 2 步重跑,不得猜测选边。构建后才产生且被忽略的入口应先由
|
|
51
|
+
人工确认构建流程与最终 tarball 边界,不能因为 setup 发现了路径就自动信任或纳入公开
|
|
52
|
+
配置。无冲突时可人工增量编辑 `answers.json`:hook 写入
|
|
53
|
+
`recommendedAnswers.projectConfig.hooks` 对应的 `projectConfig.hooks`;gate 写入
|
|
54
|
+
`verificationGates`,并把同一 id 加入 `selectedGateIds`。人工文件保持
|
|
55
|
+
`mode: preserve`,跨单元共享源使用 `sourceScope: workspace`。
|
|
47
56
|
|
|
48
57
|
3. 使用上一步打印的两个绝对字面量重新赋值,运行绑定 dry-run;任何人工编辑后都必须重跑本步:
|
|
49
58
|
|
|
@@ -17,7 +17,7 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
|
|
|
17
17
|
|
|
18
18
|
## 职责与边界
|
|
19
19
|
|
|
20
|
-
验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm
|
|
20
|
+
验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、静态入口闭包,并在配置时验证 bin 路径安全和 CLI 烟雾输出。对每个声明的 marketplace distribution 执行全新隔离消费者安装验证。
|
|
21
21
|
|
|
22
22
|
**阶段通过规则**: 只有 CLI exit code 0 和结构化状态码 `VERIFIED` 才是完整终态。
|
|
23
23
|
|
|
@@ -55,8 +55,10 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
|
|
|
55
55
|
- 在 `os.tmpdir()` 创建隔离目录
|
|
56
56
|
- 执行 `npm install <package>@<version>` 带安全标志
|
|
57
57
|
- 验证安装的 `package.json` name 和 version 精确匹配
|
|
58
|
+
- 无条件静态验证已安装包中具体 `bin`/`main`/`module`/`types`/`typings`/`exports`
|
|
59
|
+
目标是普通文件;失败时不得写入 `VERIFIED`
|
|
58
60
|
- 若配置了 `smokeBin`:验证 bin 路径安全(无逃逸、无 symlink),从精确安装根、隔离 HOME 和最小环境执行并验证输出;这会运行已安装代码,必须显式授权
|
|
59
|
-
- 若未配置 `smokeBin
|
|
61
|
+
- 若未配置 `smokeBin`:跳过运行代码,但静态入口闭包检查仍然强制执行
|
|
60
62
|
|
|
61
63
|
## 常见错误
|
|
62
64
|
|
package/src/adapters/npm.mjs
CHANGED
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
createResult,
|
|
17
17
|
matchObservation,
|
|
18
18
|
} from './contract.mjs';
|
|
19
|
+
import {
|
|
20
|
+
checkNpmEntryClosure,
|
|
21
|
+
buildTarballFileIndex,
|
|
22
|
+
} from '../npm/npm-entry-closure.mjs';
|
|
23
|
+
import { ReleaseError, GATE_FAILED } from '../core/errors.mjs';
|
|
19
24
|
|
|
20
25
|
const execFile = promisify(execFileCb);
|
|
21
26
|
const NAME = 'npm';
|
|
@@ -349,6 +354,53 @@ export async function verifyFrozenNpmTarballIdentity(action, root) {
|
|
|
349
354
|
});
|
|
350
355
|
}
|
|
351
356
|
|
|
357
|
+
async function verifyNpmTarballBufferContract(buffer, action, tarballDir) {
|
|
358
|
+
const manifest = extractManifestFromTarball(buffer, {
|
|
359
|
+
name: action.package,
|
|
360
|
+
version: action.version,
|
|
361
|
+
});
|
|
362
|
+
const fileIndex = await buildTarballFileIndex(buffer, tarballDir);
|
|
363
|
+
const closureResult = checkNpmEntryClosure(manifest, fileIndex);
|
|
364
|
+
if (closureResult.errors.length > 0) {
|
|
365
|
+
throw new ReleaseError(
|
|
366
|
+
GATE_FAILED,
|
|
367
|
+
`npm entry closure check failed for ${action.package}@${action.version}: ${closureResult.errors.length} error(s)`,
|
|
368
|
+
{
|
|
369
|
+
gate: 'npm-entry-closure',
|
|
370
|
+
package: action.package,
|
|
371
|
+
version: action.version,
|
|
372
|
+
entries: closureResult.entries,
|
|
373
|
+
errors: closureResult.errors,
|
|
374
|
+
diagnostics: closureResult.diagnostics,
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return { manifest, closureResult };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Shared frozen tarball contract helper: reads the verified tarball bytes ONCE
|
|
383
|
+
* and performs both identity verification (name/version from manifest) and
|
|
384
|
+
* entry closure validation (bin/main/module/types/typings/exports targets
|
|
385
|
+
* exist as regular files inside the tarball).
|
|
386
|
+
*
|
|
387
|
+
* The same Buffer that passes `readVerifiedTarballBytes` is reused for both
|
|
388
|
+
* the existing identity reader and the controlled extraction/file-index
|
|
389
|
+
* boundary. This adds no new tar parser and performs no second source read.
|
|
390
|
+
*
|
|
391
|
+
* @param {object} action - The frozen action with tarballPath, tarballSha256,
|
|
392
|
+
* integrity, package, and version.
|
|
393
|
+
* @param {string} root - Absolute project root.
|
|
394
|
+
* @param {string} [tarballDir] - Temp directory for tar extraction (defaults to os.tmpdir()).
|
|
395
|
+
* @returns {Promise<{ manifest: object, closureResult: object }>}
|
|
396
|
+
* @throws {ReleaseError} GATE_FAILED with details.gate='npm-entry-closure'
|
|
397
|
+
* when any declared entry is missing or invalid.
|
|
398
|
+
*/
|
|
399
|
+
export async function verifyFrozenNpmTarballContract(action, root, tarballDir) {
|
|
400
|
+
const buffer = await readVerifiedTarballBytes(action, root);
|
|
401
|
+
return verifyNpmTarballBufferContract(buffer, action, tarballDir);
|
|
402
|
+
}
|
|
403
|
+
|
|
352
404
|
/**
|
|
353
405
|
* Read and verify the frozen tarball, then write it to a controlled named
|
|
354
406
|
* temp file under a temp directory adjacent to the source. Returns the temp
|
|
@@ -700,7 +752,7 @@ export function createNpmAdapter(deps = {}) {
|
|
|
700
752
|
}
|
|
701
753
|
const cwd = resolvePackageCwd(action.cwd, context.root);
|
|
702
754
|
if (action.tarballPath) {
|
|
703
|
-
await
|
|
755
|
+
await verifyFrozenNpmTarballContract(action, context.root);
|
|
704
756
|
}
|
|
705
757
|
|
|
706
758
|
let whoamiUser;
|
|
@@ -775,7 +827,7 @@ export function createNpmAdapter(deps = {}) {
|
|
|
775
827
|
throw new Error('frozen npm tarball requires a stable Buffer publish capability');
|
|
776
828
|
}
|
|
777
829
|
const buffer = await readVerifiedTarballBytes(action, context.root);
|
|
778
|
-
const manifest =
|
|
830
|
+
const { manifest } = await verifyNpmTarballBufferContract(buffer, action);
|
|
779
831
|
let token;
|
|
780
832
|
try {
|
|
781
833
|
token = await resolveAuthToken({ registry: normalizedRegistry, cwd, exec, env: authEnv });
|
|
@@ -2393,7 +2393,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2393
2393
|
|
|
2394
2394
|
// A non-automatable platform (kimi) has NO scriptable install CLI.
|
|
2395
2395
|
// It is handled entirely by its manual-requirement strategy, which
|
|
2396
|
-
// uses a stable
|
|
2396
|
+
// uses a stable plugin-level authority directory and deliberately SKIPS the
|
|
2397
2397
|
// per-run isolated consumer dir and its runDir containment check
|
|
2398
2398
|
// (that model only fits structured-cli platforms, which exec a real
|
|
2399
2399
|
// CLI into a per-run HOME).
|
|
@@ -2913,25 +2913,12 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2913
2913
|
});
|
|
2914
2914
|
}
|
|
2915
2915
|
|
|
2916
|
-
// installPath
|
|
2916
|
+
// installPath 验证:必须是真实目录,不得是符号链接。
|
|
2917
|
+
// 安装路径不要求位于 attestationDir 内(用户使用实际全局/用户安装目录)。
|
|
2917
2918
|
let verifiedInstallPath = null;
|
|
2918
2919
|
if (normalizedAttestation.installPath) {
|
|
2919
|
-
const managedRoot = resolve(attestationDir, 'kimi-home', 'plugins', 'managed');
|
|
2920
2920
|
const installPathAbs = resolve(normalizedAttestation.installPath);
|
|
2921
2921
|
|
|
2922
|
-
// 先验证 managed root 存在(KIMI_CODE_HOME 检查)
|
|
2923
|
-
const managedRootReal = await realpath(managedRoot).catch(() => null);
|
|
2924
|
-
if (!managedRootReal) {
|
|
2925
|
-
return createResult({
|
|
2926
|
-
actionType,
|
|
2927
|
-
status: ActionStatus.OBSERVED,
|
|
2928
|
-
observation: {
|
|
2929
|
-
installed: false,
|
|
2930
|
-
error: 'KIMI_CODE_HOME does not exist',
|
|
2931
|
-
},
|
|
2932
|
-
});
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
2922
|
// 检查 installPath 词法路径是否存在(lstat 在 realpath 之前)
|
|
2936
2923
|
let lexicalStat;
|
|
2937
2924
|
try {
|
|
@@ -2968,18 +2955,6 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2968
2955
|
},
|
|
2969
2956
|
});
|
|
2970
2957
|
}
|
|
2971
|
-
const rel = relative(managedRootReal, installPathReal);
|
|
2972
|
-
const sep = process.platform === 'win32' ? '\\' : '/';
|
|
2973
|
-
if (rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)) {
|
|
2974
|
-
return createResult({
|
|
2975
|
-
actionType,
|
|
2976
|
-
status: ActionStatus.OBSERVED,
|
|
2977
|
-
observation: {
|
|
2978
|
-
installed: false,
|
|
2979
|
-
error: `kimi attestation installPath escapes the managed root: ${normalizedAttestation.installPath}`,
|
|
2980
|
-
},
|
|
2981
|
-
});
|
|
2982
|
-
}
|
|
2983
2958
|
verifiedInstallPath = installPathReal;
|
|
2984
2959
|
}
|
|
2985
2960
|
|
|
@@ -3283,7 +3258,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3283
3258
|
try {
|
|
3284
3259
|
codexRequirement = JSON.parse(await readFile(resolve(codexAttestationDir, CODEX_REQUIREMENT_FILE), 'utf8'));
|
|
3285
3260
|
} catch {
|
|
3286
|
-
// No requirement file — must not read
|
|
3261
|
+
// No requirement file — must not read the stable attestation
|
|
3287
3262
|
}
|
|
3288
3263
|
|
|
3289
3264
|
// Validate requirement binds to this action and plan (完整绑定验证)
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
} from '../core/source-authority.mjs';
|
|
59
59
|
import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
60
60
|
import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
|
|
61
|
-
import {
|
|
61
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
62
62
|
import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
63
63
|
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
64
64
|
import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
|
|
@@ -930,13 +930,13 @@ async function buildProductionAssets(
|
|
|
930
930
|
tarballDir: resolveUnitScopedPath(resolve(runDir, 'tarballs'), unit.id),
|
|
931
931
|
expectedSnapshotDigest: sealed.digest,
|
|
932
932
|
});
|
|
933
|
-
await
|
|
933
|
+
await verifyFrozenNpmTarballContract({
|
|
934
934
|
package: npmDistribution.package,
|
|
935
935
|
version,
|
|
936
936
|
tarballPath: relative(root, npm.tarballPath),
|
|
937
937
|
tarballSha256: npm.sha256,
|
|
938
938
|
integrity: npm.integrity,
|
|
939
|
-
}, root);
|
|
939
|
+
}, root, resolveUnitScopedPath(resolve(runDir, 'tarballs'), unit.id));
|
|
940
940
|
}
|
|
941
941
|
|
|
942
942
|
assets.push({
|
package/src/commands/publish.mjs
CHANGED
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
verifyFrozenGitRepository,
|
|
78
78
|
verifyFrozenSnapshot,
|
|
79
79
|
} from '../snapshot/frozen.mjs';
|
|
80
|
-
import {
|
|
80
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
81
81
|
|
|
82
82
|
function assertInsideAssetRoot(assetRoot, candidate, label) {
|
|
83
83
|
const rel = relative(assetRoot, candidate);
|
|
@@ -607,7 +607,7 @@ export async function publishRelease(options) {
|
|
|
607
607
|
if (!npmDistribution) {
|
|
608
608
|
throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" has a frozen npm tarball but no npm distribution`);
|
|
609
609
|
}
|
|
610
|
-
await
|
|
610
|
+
await verifyFrozenNpmTarballContract({
|
|
611
611
|
package: npmDistribution.package,
|
|
612
612
|
version: unit.targetVersion,
|
|
613
613
|
tarballPath: frozen.npm.tarballPath,
|
|
@@ -62,6 +62,7 @@ import { assertTransition, PARTIAL, PUBLISHED, BLOCKED } from '../core/state-mac
|
|
|
62
62
|
import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
|
|
63
63
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
64
64
|
import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY } from '../core/observe-retry.mjs';
|
|
65
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
65
66
|
|
|
66
67
|
// ---------------------------------------------------------------------------
|
|
67
68
|
// Constants
|
|
@@ -210,6 +211,44 @@ export async function reconcileRelease(options) {
|
|
|
210
211
|
|
|
211
212
|
await evidence.append({ phase: 'safety-gate', gate: 'action-completeness', status: 'passed' });
|
|
212
213
|
|
|
214
|
+
// =======================================================================
|
|
215
|
+
// Safety Gate 2c: Unconditional frozen npm entry-closure recheck
|
|
216
|
+
// This is plan-global and intentionally precedes source-run observation:
|
|
217
|
+
// a previously consistent npm checkpoint must not exempt a bad frozen
|
|
218
|
+
// tarball while another action is being reconciled.
|
|
219
|
+
// =======================================================================
|
|
220
|
+
for (const unit of plan.units ?? []) {
|
|
221
|
+
const frozen = unit.frozenSnapshot;
|
|
222
|
+
if (!frozen?.npm) continue;
|
|
223
|
+
const npmDistribution = (unit.distributions ?? []).find((distribution) => distribution.type === 'npm');
|
|
224
|
+
if (!npmDistribution) {
|
|
225
|
+
throw new ReleaseError(
|
|
226
|
+
GATE_FAILED,
|
|
227
|
+
`unit "${unit.id}" has a frozen npm tarball but no npm distribution`,
|
|
228
|
+
{ gate: 'npm-entry-closure', unitId: unit.id },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
await evidence.append({
|
|
232
|
+
phase: 'safety-gate',
|
|
233
|
+
gate: 'npm-entry-closure',
|
|
234
|
+
unitId: unit.id,
|
|
235
|
+
status: 'started',
|
|
236
|
+
});
|
|
237
|
+
await verifyFrozenNpmTarballContract({
|
|
238
|
+
package: npmDistribution.package,
|
|
239
|
+
version: unit.targetVersion,
|
|
240
|
+
tarballPath: frozen.npm.tarballPath,
|
|
241
|
+
tarballSha256: frozen.npm.tarballSha256,
|
|
242
|
+
integrity: frozen.npm.integrity,
|
|
243
|
+
}, root);
|
|
244
|
+
await evidence.append({
|
|
245
|
+
phase: 'safety-gate',
|
|
246
|
+
gate: 'npm-entry-closure',
|
|
247
|
+
unitId: unit.id,
|
|
248
|
+
status: 'passed',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
213
252
|
// =======================================================================
|
|
214
253
|
// Safety Gate 3: Load and validate source run
|
|
215
254
|
// =======================================================================
|
package/src/commands/setup.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import addFormats from 'ajv-formats';
|
|
|
24
24
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
25
25
|
import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
26
26
|
import { getPlatform, PLATFORMS } from '../platforms/registry.mjs';
|
|
27
|
+
import { checkNpmEntryClosure } from '../npm/npm-entry-closure.mjs';
|
|
27
28
|
import {
|
|
28
29
|
CONFIG_EXISTS,
|
|
29
30
|
CONFIG_INVALID,
|
|
@@ -201,6 +202,8 @@ function summarizeLegacyReleaseConfig(value, path) {
|
|
|
201
202
|
tagPrefix: optionalString(repo.tagPrefix),
|
|
202
203
|
npmPackage: optionalString(repo.npmPackage),
|
|
203
204
|
npmPackageDeclared: Object.hasOwn(repo, 'npmPackage'),
|
|
205
|
+
npmRequiredPathCandidates: stringList(repo.npmRequiredPackagePaths),
|
|
206
|
+
npmRequiredPathsDeclared: Object.hasOwn(repo, 'npmRequiredPackagePaths'),
|
|
204
207
|
docsSource: optionalString(repo.docsSource),
|
|
205
208
|
requiredPathCandidates: stringList(repo.requiredPackagePaths),
|
|
206
209
|
snapshotCommands: Array.isArray(repo.snapshotCommands) ? repo.snapshotCommands : [],
|
|
@@ -223,6 +226,8 @@ function summarizeLegacyReleaseConfig(value, path) {
|
|
|
223
226
|
npmPackageDeclared: plugins.some((plugin) => (
|
|
224
227
|
plugin && typeof plugin === 'object' && !Array.isArray(plugin) && Object.hasOwn(plugin, 'npmPackage')
|
|
225
228
|
)),
|
|
229
|
+
npmRequiredPathCandidates: stringList(value.npmRequiredPackagePaths),
|
|
230
|
+
npmRequiredPathsDeclared: Object.hasOwn(value, 'npmRequiredPackagePaths'),
|
|
226
231
|
docsSource: null,
|
|
227
232
|
requiredPathCandidates: stringList(value.requiredPaths),
|
|
228
233
|
snapshotCommands: Array.isArray(value.snapshotCommands) ? value.snapshotCommands : [],
|
|
@@ -467,6 +472,111 @@ async function discoverUnitGit(unitAbsDir, parentRoot) {
|
|
|
467
472
|
};
|
|
468
473
|
}
|
|
469
474
|
|
|
475
|
+
async function pathIsGitIgnored(unitAbsDir, target) {
|
|
476
|
+
// --no-index also classifies generated paths that do not exist yet.
|
|
477
|
+
try {
|
|
478
|
+
await execFile('git', ['check-ignore', '--no-index', '--quiet', '--', target], {
|
|
479
|
+
cwd: unitAbsDir,
|
|
480
|
+
shell: false,
|
|
481
|
+
timeout: 5000,
|
|
482
|
+
});
|
|
483
|
+
return true;
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function classifyNpmEntryCandidate(unitAbsDir, target, trackedFiles) {
|
|
490
|
+
const tracked = trackedFiles.includes(target);
|
|
491
|
+
const ignored = await pathIsGitIgnored(unitAbsDir, target);
|
|
492
|
+
let stat = null;
|
|
493
|
+
try {
|
|
494
|
+
stat = await lstat(join(unitAbsDir, target));
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (error.code !== 'ENOENT') throw error;
|
|
497
|
+
}
|
|
498
|
+
const exists = stat !== null;
|
|
499
|
+
const regular = stat?.isFile() === true && stat?.isSymbolicLink() !== true;
|
|
500
|
+
const state = exists && !regular
|
|
501
|
+
? 'NON_REGULAR'
|
|
502
|
+
: exists && tracked
|
|
503
|
+
? 'TRACKED_PRESENT'
|
|
504
|
+
: exists && ignored
|
|
505
|
+
? 'IGNORED_PRESENT'
|
|
506
|
+
: exists
|
|
507
|
+
? 'UNTRACKED_PRESENT'
|
|
508
|
+
: ignored
|
|
509
|
+
? 'IGNORED_MISSING'
|
|
510
|
+
: 'MISSING';
|
|
511
|
+
return { path: target, state, exists, tracked, ignored };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function discoverNpmEntryCandidates(root, pkg, matchingLegacyUnits, unitGitEntry) {
|
|
515
|
+
const emptyIndex = new Map();
|
|
516
|
+
const semantic = checkNpmEntryClosure(pkg.entryManifest, emptyIndex);
|
|
517
|
+
const byPath = new Map();
|
|
518
|
+
const add = (target, source) => {
|
|
519
|
+
const current = byPath.get(target) ?? { path: target, sources: new Set() };
|
|
520
|
+
current.sources.add(source);
|
|
521
|
+
byPath.set(target, current);
|
|
522
|
+
};
|
|
523
|
+
for (const entry of semantic.entries) add(entry.target, `package.json:${entry.field}`);
|
|
524
|
+
|
|
525
|
+
const diagnostics = [
|
|
526
|
+
...semantic.errors
|
|
527
|
+
.filter((error) => error.reason !== 'entry_missing')
|
|
528
|
+
.map((error) => ({ code: 'NPM_ENTRY_DECLARATION_INVALID', ...error })),
|
|
529
|
+
...semantic.diagnostics.map((diagnostic) => ({
|
|
530
|
+
code: 'NPM_ENTRY_WILDCARD_REQUIRES_REVIEW',
|
|
531
|
+
...diagnostic,
|
|
532
|
+
})),
|
|
533
|
+
];
|
|
534
|
+
const declaredLegacyPaths = new Set();
|
|
535
|
+
const legacyCoverageDeclared = matchingLegacyUnits.some((unit) => unit.npmRequiredPathsDeclared);
|
|
536
|
+
for (const legacy of matchingLegacyUnits) {
|
|
537
|
+
for (const target of legacy.npmRequiredPathCandidates) {
|
|
538
|
+
const checked = checkNpmEntryClosure({ main: target }, emptyIndex);
|
|
539
|
+
const normalized = checked.entries[0]?.target;
|
|
540
|
+
if (!normalized) {
|
|
541
|
+
diagnostics.push({
|
|
542
|
+
code: 'LEGACY_NPM_REQUIRED_PATH_INVALID',
|
|
543
|
+
path: target,
|
|
544
|
+
reason: checked.errors[0]?.reason ?? 'invalid_path',
|
|
545
|
+
});
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
declaredLegacyPaths.add(normalized);
|
|
549
|
+
add(normalized, 'public-release.json:npmRequiredPackagePaths');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (legacyCoverageDeclared) {
|
|
553
|
+
for (const entry of semantic.entries) {
|
|
554
|
+
if (!declaredLegacyPaths.has(entry.target)) {
|
|
555
|
+
diagnostics.push({
|
|
556
|
+
code: 'LEGACY_NPM_ENTRY_COVERAGE_MISSING',
|
|
557
|
+
path: entry.target,
|
|
558
|
+
field: entry.field,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const unitAbsDir = resolve(root, pkg.directory);
|
|
565
|
+
const candidates = [];
|
|
566
|
+
for (const item of [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
567
|
+
candidates.push({
|
|
568
|
+
...await classifyNpmEntryCandidate(
|
|
569
|
+
unitAbsDir,
|
|
570
|
+
item.path,
|
|
571
|
+
unitGitEntry?.trackedFiles ?? [],
|
|
572
|
+
),
|
|
573
|
+
sources: [...item.sources].sort(),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
diagnostics.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
|
|
577
|
+
return { candidates, diagnostics };
|
|
578
|
+
}
|
|
579
|
+
|
|
470
580
|
async function discoverFacts(root) {
|
|
471
581
|
const files = await walkDiscoveryFiles(root);
|
|
472
582
|
const packageFiles = files.filter((path) => (
|
|
@@ -494,6 +604,11 @@ async function discoverFacts(root) {
|
|
|
494
604
|
private: pkg.private === true,
|
|
495
605
|
repository: parseGithubRepo(pkg.repository),
|
|
496
606
|
publishRegistry: typeof pkg.publishConfig?.registry === 'string' ? pkg.publishConfig.registry : null,
|
|
607
|
+
entryManifest: Object.fromEntries(
|
|
608
|
+
['bin', 'main', 'module', 'types', 'typings', 'exports']
|
|
609
|
+
.filter((field) => Object.hasOwn(pkg, field))
|
|
610
|
+
.map((field) => [field, pkg[field]]),
|
|
611
|
+
),
|
|
497
612
|
files: Array.isArray(pkg.files) ? pkg.files.filter((item) => typeof item === 'string').sort() : [],
|
|
498
613
|
scripts: Object.fromEntries(Object.entries(pkg.scripts ?? {})
|
|
499
614
|
.filter(([, value]) => typeof value === 'string')
|
|
@@ -557,6 +672,22 @@ async function discoverFacts(root) {
|
|
|
557
672
|
for (const pkg of packages) {
|
|
558
673
|
const unitAbsDir = resolve(root, pkg.directory);
|
|
559
674
|
unitGit[pkg.directory] = await discoverUnitGit(unitAbsDir, root);
|
|
675
|
+
const inferredId = safeUnitId(pkg, pkg.directory);
|
|
676
|
+
const matchingLegacyUnits = legacyReleaseConfigs
|
|
677
|
+
.flatMap((config) => config.releaseUnits)
|
|
678
|
+
.filter((unit) => (
|
|
679
|
+
unit.id === inferredId ||
|
|
680
|
+
unit.source === pkg.directory ||
|
|
681
|
+
unit.source === dirname(pkg.path)
|
|
682
|
+
));
|
|
683
|
+
const npmDiscovery = await discoverNpmEntryCandidates(
|
|
684
|
+
root,
|
|
685
|
+
pkg,
|
|
686
|
+
matchingLegacyUnits,
|
|
687
|
+
unitGit[pkg.directory],
|
|
688
|
+
);
|
|
689
|
+
pkg.npmEntryCandidates = npmDiscovery.candidates;
|
|
690
|
+
pkg.npmEntryDiagnostics = npmDiscovery.diagnostics;
|
|
560
691
|
}
|
|
561
692
|
|
|
562
693
|
return { git, packages, manifests, skills, legacyReleaseConfigs, fileDigests, unitGit };
|
|
@@ -871,6 +1002,8 @@ function buildCandidates(facts) {
|
|
|
871
1002
|
publicFileMappingCandidates: mappingResult.mappings,
|
|
872
1003
|
publicFileMappingConflicts: mappingResult.conflicts,
|
|
873
1004
|
requiredPublicFileCandidates,
|
|
1005
|
+
npmEntryCandidates: pkg.npmEntryCandidates ?? [],
|
|
1006
|
+
npmEntryDiagnostics: pkg.npmEntryDiagnostics ?? [],
|
|
874
1007
|
entrySkillCandidates: entrySkillCandidatesForUnit(facts, pkg, id),
|
|
875
1008
|
...(hasAuthorityConflict ? {
|
|
876
1009
|
authorityConflict: {
|
|
@@ -1027,6 +1160,24 @@ function buildRecommendedProposal(facts, candidates) {
|
|
|
1027
1160
|
assumptions,
|
|
1028
1161
|
};
|
|
1029
1162
|
}
|
|
1163
|
+
if (
|
|
1164
|
+
unit.distributionCandidates.includes('npm') &&
|
|
1165
|
+
(
|
|
1166
|
+
(unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== 'TRACKED_PRESENT') ||
|
|
1167
|
+
(unit.npmEntryDiagnostics ?? []).length > 0
|
|
1168
|
+
)
|
|
1169
|
+
) {
|
|
1170
|
+
return {
|
|
1171
|
+
answers: null,
|
|
1172
|
+
conflicts: [{
|
|
1173
|
+
code: 'NPM_ENTRY_REVIEW_REQUIRED',
|
|
1174
|
+
unit: unit.id,
|
|
1175
|
+
candidates: unit.npmEntryCandidates,
|
|
1176
|
+
diagnostics: unit.npmEntryDiagnostics,
|
|
1177
|
+
}],
|
|
1178
|
+
assumptions,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1030
1181
|
|
|
1031
1182
|
// Infer npm publisher: prefer legacy owner, then parse from repo slug
|
|
1032
1183
|
const repoOwner = publicRepo.includes('/') ? publicRepo.split('/')[0] : null;
|
|
@@ -1245,6 +1396,15 @@ function buildDecisionsRequired(candidates, localOnly) {
|
|
|
1245
1396
|
id: `unit:${unit.id}:distributions-and-files`,
|
|
1246
1397
|
description: `逐项确认渠道 ${JSON.stringify(unit.distributionCandidates)}、公开文件边界和 requiredPublicFiles;候选不是授权。`,
|
|
1247
1398
|
});
|
|
1399
|
+
if (
|
|
1400
|
+
(unit.npmEntryCandidates ?? []).length > 0 ||
|
|
1401
|
+
(unit.npmEntryDiagnostics ?? []).length > 0
|
|
1402
|
+
) {
|
|
1403
|
+
decisions.push({
|
|
1404
|
+
id: `unit:${unit.id}:npm-entry-closure`,
|
|
1405
|
+
description: '审阅 package.json 入口及旧 npmRequiredPackagePaths 的 tracked/untracked/ignored/missing 状态;setup 只报告候选,不自动写入 publicFiles 或 requiredPublicFiles。',
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1248
1408
|
}
|
|
1249
1409
|
decisions.push({
|
|
1250
1410
|
id: 'verification-gates',
|
|
@@ -1271,6 +1431,8 @@ function buildCompactSummary(report) {
|
|
|
1271
1431
|
distributionCandidates: [...unit.distributionCandidates].sort(),
|
|
1272
1432
|
publicFileMappingCount: (unit.publicFileMappingCandidates ?? []).length,
|
|
1273
1433
|
requiredPublicFileCount: (unit.requiredPublicFileCandidates ?? []).length,
|
|
1434
|
+
npmEntryCandidates: unit.npmEntryCandidates ?? [],
|
|
1435
|
+
npmEntryDiagnostics: unit.npmEntryDiagnostics ?? [],
|
|
1274
1436
|
})),
|
|
1275
1437
|
gateCandidates: (report.gateCandidates ?? []).map((gate) => ({
|
|
1276
1438
|
id: gate.id,
|
|
@@ -1519,6 +1681,12 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1519
1681
|
...(unconfiguredGateCandidateIds.length > 0
|
|
1520
1682
|
? ['存在未配置的验证候选;逐项审阅副作用后决定是否人工注册。']
|
|
1521
1683
|
: []),
|
|
1684
|
+
...(candidates.units.some((unit) => (
|
|
1685
|
+
(unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== 'TRACKED_PRESENT') ||
|
|
1686
|
+
(unit.npmEntryDiagnostics ?? []).length > 0
|
|
1687
|
+
))
|
|
1688
|
+
? ['npm 入口候选存在未跟踪、被忽略、缺失、非普通文件或声明覆盖疑点;人工增量修复配置或构建流程,setup 不自动改写。']
|
|
1689
|
+
: []),
|
|
1522
1690
|
],
|
|
1523
1691
|
},
|
|
1524
1692
|
recommendedGateIds: [],
|