release-skill 0.2.5 → 0.2.6
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 +32 -8
- package/INSTALL.zh-CN.md +28 -6
- package/README.md +26 -14
- package/README.zh-CN.md +24 -14
- 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 +1340 -614
- package/adapters/claude/schemas/.render-manifest.json +6 -6
- package/adapters/claude/schemas/release-plan.schema.json +67 -0
- package/adapters/claude/schemas/release-project.schema.json +6 -0
- package/adapters/claude/schemas/release-run.schema.json +65 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1340 -614
- package/adapters/codex/schemas/.render-manifest.json +6 -6
- package/adapters/codex/schemas/release-plan.schema.json +67 -0
- package/adapters/codex/schemas/release-project.schema.json +6 -0
- package/adapters/codex/schemas/release-run.schema.json +65 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1340 -614
- package/adapters/kimi/schemas/.render-manifest.json +6 -6
- package/adapters/kimi/schemas/release-plan.schema.json +67 -0
- package/adapters/kimi/schemas/release-project.schema.json +6 -0
- package/adapters/kimi/schemas/release-run.schema.json +65 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1340 -614
- package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
- package/adapters/workbuddy/schemas/release-plan.schema.json +67 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +6 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +65 -0
- package/bin/release-skill.bundle.mjs +1340 -614
- package/package.json +1 -1
- package/references/.render-manifest.json +4 -4
- package/references/02-project-config.md +28 -2
- package/references/05-evidence-and-errors.md +6 -0
- package/schemas/.render-manifest.json +6 -6
- package/schemas/release-plan.schema.json +67 -0
- package/schemas/release-project.schema.json +6 -0
- package/schemas/release-run.schema.json +65 -0
- package/src/commands/prepare.mjs +158 -1
- package/src/commands/publish.mjs +72 -0
- package/src/commands/reconcile.mjs +19 -0
- package/src/commands/setup.mjs +26 -0
- package/src/commands/verify.mjs +32 -0
- package/src/core/errors.mjs +12 -0
- package/src/core/source-authority.mjs +547 -0
package/package.json
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"bytes": 8840
|
|
11
11
|
},
|
|
12
12
|
"02-project-config.md": {
|
|
13
|
-
"digest": "
|
|
14
|
-
"bytes":
|
|
13
|
+
"digest": "0cff13ebb270f3515290e0ce60b91303143777d1a4948db28cf2c69612ceff8e",
|
|
14
|
+
"bytes": 19029
|
|
15
15
|
},
|
|
16
16
|
"03-readme-quality.md": {
|
|
17
17
|
"digest": "d74e41052592443ebbd450d0ace5f9b9b19c7a94db50f952b17c1c5a4c5dc230",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"bytes": 4682
|
|
23
23
|
},
|
|
24
24
|
"05-evidence-and-errors.md": {
|
|
25
|
-
"digest": "
|
|
26
|
-
"bytes":
|
|
25
|
+
"digest": "ebcb4940ff2a25ce6945c3b11cfcafe751f4a1f515276b7e021717ea5844eebf",
|
|
26
|
+
"bytes": 9688
|
|
27
27
|
},
|
|
28
28
|
"06-adapter-contract.md": {
|
|
29
29
|
"digest": "39f0271bc06eae38dade518ece8f7e16a18f53099a433e3ae07e9d20ff7a142e",
|
|
@@ -23,8 +23,9 @@ apiVersion: release-skill/v1
|
|
|
23
23
|
kind: ReleaseProject
|
|
24
24
|
|
|
25
25
|
project:
|
|
26
|
-
name: <string>
|
|
27
|
-
defaultBranch: <string>
|
|
26
|
+
name: <string> # 项目标识,必填
|
|
27
|
+
defaultBranch: <string> # workspace 远端默认分支,必填;不限定为 main
|
|
28
|
+
sourceRepository: <owner/repo> # workspace 源仓库;生产发布必填
|
|
28
29
|
|
|
29
30
|
releaseUnits: # 发布单元数组,至少 1 个
|
|
30
31
|
- id: <string> # 唯一标识
|
|
@@ -125,6 +126,31 @@ human-owned 权威源,然后重新 prepare、审阅并 approve;后续环节
|
|
|
125
126
|
|
|
126
127
|
旧的 `policy.requiredPublicFiles` 已不支持。加载器必须返回 `CONFIG_INVALID`,并引导迁移到各自的 `releaseUnits[].requiredPublicFiles`。
|
|
127
128
|
|
|
129
|
+
### 3.1.1 Workspace 源码权威内容闭包
|
|
130
|
+
|
|
131
|
+
`project.sourceRepository` 标识承载当前 workspace 人工源文件的 GitHub
|
|
132
|
+
`owner/repo`,它与各发布单元的 `publicRepo` 不是同一概念;多个发布单元可以投影到不同
|
|
133
|
+
公开仓库,但它们仍可共享一个 workspace 源仓库。`project.defaultBranch` 表示该源仓库的
|
|
134
|
+
真实远端默认分支,可以是 `main`、`master`、`develop`、`trunk` 或其他明确名称。
|
|
135
|
+
setup 只在 workspace Git remotes 唯一一致时提出候选;缺失或冲突时要求人工裁决,不保存
|
|
136
|
+
`auto`。
|
|
137
|
+
|
|
138
|
+
生产 prepare 在 hooks 完成后,从所有 `publicFiles.from`(目录递归展开)和每个 unit 的
|
|
139
|
+
`version.source` 计算源码输入闭包,冻结 workspace 相对路径、内容 SHA-256 和 Git mode。
|
|
140
|
+
只有闭包内 staged、unstaged 或 untracked 的文件会阻断 prepare;无关 dirty 文件不阻断。
|
|
141
|
+
离线 prepare 明确记录 `unobserved-offline`,远端内容的强制比较留在 publish。
|
|
142
|
+
|
|
143
|
+
production publish 在任何 adapter `execute` 前,从
|
|
144
|
+
`sourceRepository/defaultBranch` 读取冻结路径集合,先确认配置分支仍是远端实际默认分支,
|
|
145
|
+
再比较内容和 mode。判定只依赖内容闭包,不依赖 commit ancestry,因此普通 merge、
|
|
146
|
+
squash、rebase 和默认分支上的无关后续提交可以通过;冲突解决或 revert 丢失 README、
|
|
147
|
+
版本源或其他公开输入会按具体路径失败关闭。系统不会自动 merge、rebase、切分支、push
|
|
148
|
+
或创建 PR。
|
|
149
|
+
|
|
150
|
+
通过后,publish 写入与 plan digest 绑定的源码权威收据;PARTIAL 经 reconcile 恢复时继承
|
|
151
|
+
并重新校验该收据。verify 只消费这份冻结收据,不在发布完成后用可变默认分支重新判定历史
|
|
152
|
+
发布。
|
|
153
|
+
|
|
128
154
|
### 3.2 公开分支策略
|
|
129
155
|
|
|
130
156
|
- `create-release-branch`:目标分支必须不存在;创建独立 release 分支,冻结提交没有父提交。
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
| `RELEASE_DOCS_CONFLICT` | 目标文档存在人工冲突 | 非受管同版本 CHANGELOG 条目、受管标记缺失/重复/损坏、版本标记非唯一 | 人工修复目标并保留人工修改后重新演练 |
|
|
27
27
|
| `RELEASE_DOCS_REFRESH_STALE` | 写入确认绑定的候选已变化 | 演练后说明源或目标发生变化,仍用旧 `refreshDigest` 写入 | 重新演练取得新的 `refreshDigest` 并重新确认 |
|
|
28
28
|
| `RELEASE_DOCS_STALE` | prepare 检测到发布文档未刷新 | 说明源已更新但 README/CHANGELOG 受管内容未同步 | `docs refresh` 演练 → 确认写入 → 审阅提交 → 重新 prepare |
|
|
29
|
+
| `CONFIG_MISSING` | 生产源码权威配置缺失 | 缺少明确的 `project.sourceRepository` 或 `project.defaultBranch` | 补充经人工确认的 workspace 源仓库与默认分支后重新 prepare |
|
|
30
|
+
| `REMOTE_UNAVAILABLE` | 无法可靠读取 workspace 源仓库 | 网络、认证或远端协议错误导致默认分支内容不可观察 | 修复网络或认证后基于同一冻结计划重试;不得猜测成功 |
|
|
31
|
+
| `REF_MISSING` | 配置的远端默认分支不存在 | `refs/heads/<defaultBranch>` 不存在 | 核对真实分支名并重新 prepare |
|
|
32
|
+
| `NOT_DEFAULT` | 配置分支不是远端实际默认分支 | 默认分支在 prepare 后改变,或配置错误 | 人工确认远端默认分支,更新配置并重新 prepare |
|
|
33
|
+
| `CONTENT_MISMATCH` | 远端默认分支缺少冻结源码内容 | README、版本源、公开映射输入的内容或 mode 不一致 | 人工 merge/adopt/reject;接受的内容进入默认分支后重试 publish |
|
|
34
|
+
| `DIRTY_SOURCE_INPUT` | 源码输入闭包存在未提交变化 | `publicFiles.from` 或 `version.source` 有 staged、unstaged、untracked 变化 | 提交或撤销这些具体输入的变化后重新 prepare;无关 dirty 不受影响 |
|
|
29
35
|
|
|
30
36
|
---
|
|
31
37
|
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"bytes": 2390
|
|
23
23
|
},
|
|
24
24
|
"release-plan.schema.json": {
|
|
25
|
-
"digest": "
|
|
26
|
-
"bytes":
|
|
25
|
+
"digest": "01c96659223918b5ce0e2d0d01d1ce5095baeb056de401babb8928add4d2a221",
|
|
26
|
+
"bytes": 35256
|
|
27
27
|
},
|
|
28
28
|
"release-project.schema.json": {
|
|
29
|
-
"digest": "
|
|
30
|
-
"bytes":
|
|
29
|
+
"digest": "a3d46be1e1e8105103a505c6d58d14f3733c8da91ab8289e097ffd259bf1cd93",
|
|
30
|
+
"bytes": 30982
|
|
31
31
|
},
|
|
32
32
|
"release-run.schema.json": {
|
|
33
|
-
"digest": "
|
|
34
|
-
"bytes":
|
|
33
|
+
"digest": "bf3c8fb4ca2fc078272a45a6ee6bf663fd2420a722b2f995dbaabf2635e2179e",
|
|
34
|
+
"bytes": 14824
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -992,6 +992,73 @@
|
|
|
992
992
|
"type": "string",
|
|
993
993
|
"minLength": 1
|
|
994
994
|
},
|
|
995
|
+
"sourceAuthority": {
|
|
996
|
+
"type": "object",
|
|
997
|
+
"description": "Source authority content closure gate binding: proves frozen source inputs exist in the workspace remote default branch",
|
|
998
|
+
"required": [
|
|
999
|
+
"sourceRepository",
|
|
1000
|
+
"defaultBranch",
|
|
1001
|
+
"entries",
|
|
1002
|
+
"inputDigest",
|
|
1003
|
+
"algorithmVersion"
|
|
1004
|
+
],
|
|
1005
|
+
"additionalProperties": false,
|
|
1006
|
+
"properties": {
|
|
1007
|
+
"sourceRepository": {
|
|
1008
|
+
"type": "string",
|
|
1009
|
+
"minLength": 1,
|
|
1010
|
+
"pattern": "^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$",
|
|
1011
|
+
"description": "GitHub owner/repo of the workspace source repository"
|
|
1012
|
+
},
|
|
1013
|
+
"defaultBranch": {
|
|
1014
|
+
"type": "string",
|
|
1015
|
+
"minLength": 1,
|
|
1016
|
+
"description": "Workspace remote default branch name"
|
|
1017
|
+
},
|
|
1018
|
+
"entries": {
|
|
1019
|
+
"type": "array",
|
|
1020
|
+
"minItems": 1,
|
|
1021
|
+
"description": "Frozen workspace-relative source inputs compared with the remote default branch",
|
|
1022
|
+
"items": {
|
|
1023
|
+
"type": "object",
|
|
1024
|
+
"required": [
|
|
1025
|
+
"path",
|
|
1026
|
+
"digest",
|
|
1027
|
+
"mode"
|
|
1028
|
+
],
|
|
1029
|
+
"additionalProperties": false,
|
|
1030
|
+
"properties": {
|
|
1031
|
+
"path": {
|
|
1032
|
+
"type": "string",
|
|
1033
|
+
"minLength": 1,
|
|
1034
|
+
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
|
|
1035
|
+
},
|
|
1036
|
+
"digest": {
|
|
1037
|
+
"type": "string",
|
|
1038
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
1039
|
+
},
|
|
1040
|
+
"mode": {
|
|
1041
|
+
"type": "string",
|
|
1042
|
+
"enum": [
|
|
1043
|
+
"100644",
|
|
1044
|
+
"100755"
|
|
1045
|
+
]
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
"inputDigest": {
|
|
1051
|
+
"type": "string",
|
|
1052
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
1053
|
+
"description": "SHA-256 digest of the deterministic source-input closure"
|
|
1054
|
+
},
|
|
1055
|
+
"algorithmVersion": {
|
|
1056
|
+
"type": "integer",
|
|
1057
|
+
"minimum": 1,
|
|
1058
|
+
"description": "Source-input closure algorithm version"
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
995
1062
|
"waivers": {
|
|
996
1063
|
"type": "array",
|
|
997
1064
|
"items": {
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
"defaultBranch": {
|
|
36
36
|
"type": "string",
|
|
37
37
|
"minLength": 1
|
|
38
|
+
},
|
|
39
|
+
"sourceRepository": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"minLength": 1,
|
|
42
|
+
"pattern": "^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$",
|
|
43
|
+
"description": "GitHub owner/repo of the workspace source repository for source authority content gate"
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
},
|
|
@@ -227,6 +227,71 @@
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
},
|
|
230
|
+
"sourceAuthorityReceipts": {
|
|
231
|
+
"type": "array",
|
|
232
|
+
"description": "Source authority content closure receipts: one per publish run proving remote content matches frozen inputs",
|
|
233
|
+
"items": {
|
|
234
|
+
"type": "object",
|
|
235
|
+
"required": [
|
|
236
|
+
"sourceRepository",
|
|
237
|
+
"defaultBranch",
|
|
238
|
+
"inputDigest",
|
|
239
|
+
"algorithmVersion",
|
|
240
|
+
"entryCount",
|
|
241
|
+
"planDigest",
|
|
242
|
+
"verifiedAt",
|
|
243
|
+
"result"
|
|
244
|
+
],
|
|
245
|
+
"additionalProperties": false,
|
|
246
|
+
"properties": {
|
|
247
|
+
"sourceRepository": {
|
|
248
|
+
"type": "string",
|
|
249
|
+
"minLength": 1
|
|
250
|
+
},
|
|
251
|
+
"defaultBranch": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"minLength": 1
|
|
254
|
+
},
|
|
255
|
+
"inputDigest": {
|
|
256
|
+
"type": "string",
|
|
257
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
258
|
+
},
|
|
259
|
+
"algorithmVersion": {
|
|
260
|
+
"type": "integer",
|
|
261
|
+
"minimum": 1
|
|
262
|
+
},
|
|
263
|
+
"entryCount": {
|
|
264
|
+
"type": "integer",
|
|
265
|
+
"minimum": 1
|
|
266
|
+
},
|
|
267
|
+
"planDigest": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"minLength": 1
|
|
270
|
+
},
|
|
271
|
+
"verifiedAt": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"format": "date-time"
|
|
274
|
+
},
|
|
275
|
+
"result": {
|
|
276
|
+
"type": "string",
|
|
277
|
+
"enum": [
|
|
278
|
+
"CONSISTENT",
|
|
279
|
+
"MISMATCH"
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
"observedCommit": {
|
|
283
|
+
"type": "string",
|
|
284
|
+
"pattern": "^[a-f0-9]{40,64}$"
|
|
285
|
+
},
|
|
286
|
+
"mismatchedPaths": {
|
|
287
|
+
"type": "array",
|
|
288
|
+
"items": {
|
|
289
|
+
"type": "string"
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
230
295
|
"consumerVerificationReceipts": {
|
|
231
296
|
"type": "array",
|
|
232
297
|
"description": "消费端验证收据,每个 marketplace action 一条记录",
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -49,7 +49,13 @@ import {
|
|
|
49
49
|
normalizeGitTimestamp,
|
|
50
50
|
sealFrozenSnapshot,
|
|
51
51
|
} from '../snapshot/frozen.mjs';
|
|
52
|
-
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE } from '../core/errors.mjs';
|
|
52
|
+
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, CONFIG_MISSING, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE, DIRTY_SOURCE_INPUT } from '../core/errors.mjs';
|
|
53
|
+
import {
|
|
54
|
+
SOURCE_INPUT_ALGORITHM_VERSION,
|
|
55
|
+
computeSourceInputClosure,
|
|
56
|
+
checkSourceInputDirty,
|
|
57
|
+
verifySnapshotSourcesMatchClosure,
|
|
58
|
+
} from '../core/source-authority.mjs';
|
|
53
59
|
import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
54
60
|
import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
|
|
55
61
|
import { verifyFrozenNpmTarballIdentity } from '../adapters/npm.mjs';
|
|
@@ -1919,6 +1925,87 @@ export async function prepareRelease(options) {
|
|
|
1919
1925
|
});
|
|
1920
1926
|
}
|
|
1921
1927
|
|
|
1928
|
+
// --- Step 3c: Source authority content closure gate ---
|
|
1929
|
+
// After hooks complete, compute the deterministic source-input closure
|
|
1930
|
+
// and verify that closure inputs are clean (no staged/unstaged/untracked
|
|
1931
|
+
// changes). Production configs must declare sourceRepository.
|
|
1932
|
+
const sourceRepository = config.project?.sourceRepository ?? null;
|
|
1933
|
+
const configDefaultBranch = config.project?.defaultBranch ?? null;
|
|
1934
|
+
|
|
1935
|
+
let sourceAuthority = null;
|
|
1936
|
+
let sourceInputClosure = null;
|
|
1937
|
+
if (production) {
|
|
1938
|
+
if (!sourceRepository || typeof sourceRepository !== 'string') {
|
|
1939
|
+
throw new ReleaseError(
|
|
1940
|
+
CONFIG_MISSING,
|
|
1941
|
+
'production prepare requires project.sourceRepository in configuration; source authority content gate needs a workspace source repository',
|
|
1942
|
+
{ configPath },
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
await evidence.append({ phase: 'source-authority', status: 'started' });
|
|
1947
|
+
|
|
1948
|
+
// Compute source-input closure from resolved unit configs
|
|
1949
|
+
const unitConfigsForClosure = configUnits.map((unit, idx) => ({
|
|
1950
|
+
...unit,
|
|
1951
|
+
version: { ...unit.version },
|
|
1952
|
+
}));
|
|
1953
|
+
sourceInputClosure = await computeSourceInputClosure({
|
|
1954
|
+
units: unitConfigsForClosure,
|
|
1955
|
+
root: realRoot,
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
await evidence.append({
|
|
1959
|
+
phase: 'source-authority',
|
|
1960
|
+
step: 'closure-computed',
|
|
1961
|
+
entryCount: sourceInputClosure.entries.length,
|
|
1962
|
+
inputDigest: sourceInputClosure.digest,
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// Check only closure inputs for dirty (not whole workspace)
|
|
1966
|
+
const dirtyResult = await checkSourceInputDirty({
|
|
1967
|
+
closure: sourceInputClosure,
|
|
1968
|
+
root: realRoot,
|
|
1969
|
+
});
|
|
1970
|
+
if (dirtyResult.dirty) {
|
|
1971
|
+
await evidence.append({
|
|
1972
|
+
phase: 'source-authority',
|
|
1973
|
+
status: 'blocking',
|
|
1974
|
+
reason: 'DIRTY_SOURCE_INPUT',
|
|
1975
|
+
dirtyPaths: dirtyResult.dirtyPaths,
|
|
1976
|
+
});
|
|
1977
|
+
throw new ReleaseError(
|
|
1978
|
+
DIRTY_SOURCE_INPUT,
|
|
1979
|
+
`source-input closure files have uncommitted changes: ${dirtyResult.dirtyPaths.join(', ')}`,
|
|
1980
|
+
{ dirtyPaths: dirtyResult.dirtyPaths },
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
await evidence.append({
|
|
1985
|
+
phase: 'source-authority',
|
|
1986
|
+
step: 'dirty-check',
|
|
1987
|
+
status: 'clean',
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Build sourceAuthority binding for plan digest
|
|
1991
|
+
sourceAuthority = {
|
|
1992
|
+
sourceRepository,
|
|
1993
|
+
defaultBranch: configDefaultBranch,
|
|
1994
|
+
entries: sourceInputClosure.entries,
|
|
1995
|
+
inputDigest: sourceInputClosure.digest,
|
|
1996
|
+
algorithmVersion: SOURCE_INPUT_ALGORITHM_VERSION,
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
await evidence.append({
|
|
2000
|
+
phase: 'source-authority',
|
|
2001
|
+
status: 'completed',
|
|
2002
|
+
sourceRepository,
|
|
2003
|
+
defaultBranch: configDefaultBranch,
|
|
2004
|
+
inputDigest: sourceInputClosure.digest,
|
|
2005
|
+
remoteObservation: offline ? 'unobserved-offline' : 'deferred-to-publish',
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1922
2009
|
// --- Step 4: Capture Git baseline (AFTER hooks, so workspaceDigest
|
|
1923
2010
|
// reflects any file changes introduced by hooks) ---
|
|
1924
2011
|
await evidence.append({ phase: 'baseline', status: 'started' });
|
|
@@ -2183,6 +2270,75 @@ export async function prepareRelease(options) {
|
|
|
2183
2270
|
gateCount: snapshotGateResults.length,
|
|
2184
2271
|
});
|
|
2185
2272
|
|
|
2273
|
+
// Bind the remote source-authority proof to the exact bytes that entered
|
|
2274
|
+
// the frozen snapshots, not merely to an earlier read of the workspace.
|
|
2275
|
+
// Then re-read the complete closure and dirty state once more so version
|
|
2276
|
+
// sources and non-snapshot closure entries cannot drift during prepare.
|
|
2277
|
+
if (production) {
|
|
2278
|
+
const snapshotSourceResult = verifySnapshotSourcesMatchClosure({
|
|
2279
|
+
closure: sourceInputClosure,
|
|
2280
|
+
unitResults,
|
|
2281
|
+
});
|
|
2282
|
+
if (!snapshotSourceResult.passed) {
|
|
2283
|
+
throw new ReleaseError(
|
|
2284
|
+
DIRTY_SOURCE_INPUT,
|
|
2285
|
+
'source inputs changed between closure calculation and frozen snapshot construction',
|
|
2286
|
+
{
|
|
2287
|
+
reason: 'SNAPSHOT_SOURCE_DRIFT',
|
|
2288
|
+
dirtyPaths: snapshotSourceResult.error.paths,
|
|
2289
|
+
},
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
const finalClosure = await computeSourceInputClosure({
|
|
2294
|
+
units: configUnits,
|
|
2295
|
+
root: realRoot,
|
|
2296
|
+
});
|
|
2297
|
+
if (finalClosure.digest !== sourceInputClosure.digest) {
|
|
2298
|
+
const initialByPath = new Map(
|
|
2299
|
+
sourceInputClosure.entries.map((entry) => [entry.path, entry]),
|
|
2300
|
+
);
|
|
2301
|
+
const finalByPath = new Map(
|
|
2302
|
+
finalClosure.entries.map((entry) => [entry.path, entry]),
|
|
2303
|
+
);
|
|
2304
|
+
const changedPaths = [...new Set([
|
|
2305
|
+
...sourceInputClosure.entries.map((entry) => entry.path),
|
|
2306
|
+
...finalClosure.entries.map((entry) => entry.path),
|
|
2307
|
+
])].filter((path) => (
|
|
2308
|
+
JSON.stringify(initialByPath.get(path) ?? null)
|
|
2309
|
+
!== JSON.stringify(finalByPath.get(path) ?? null)
|
|
2310
|
+
)).sort();
|
|
2311
|
+
throw new ReleaseError(
|
|
2312
|
+
DIRTY_SOURCE_INPUT,
|
|
2313
|
+
'source-input closure changed while preparing frozen snapshots',
|
|
2314
|
+
{ reason: 'SOURCE_CLOSURE_DRIFT', dirtyPaths: changedPaths },
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
const finalDirtyResult = await checkSourceInputDirty({
|
|
2319
|
+
closure: finalClosure,
|
|
2320
|
+
root: realRoot,
|
|
2321
|
+
});
|
|
2322
|
+
if (finalDirtyResult.dirty) {
|
|
2323
|
+
throw new ReleaseError(
|
|
2324
|
+
DIRTY_SOURCE_INPUT,
|
|
2325
|
+
`source-input closure files have uncommitted changes after snapshot construction: ${finalDirtyResult.dirtyPaths.join(', ')}`,
|
|
2326
|
+
{
|
|
2327
|
+
reason: 'DIRTY_AFTER_SNAPSHOT',
|
|
2328
|
+
dirtyPaths: finalDirtyResult.dirtyPaths,
|
|
2329
|
+
},
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
await evidence.append({
|
|
2334
|
+
phase: 'source-authority',
|
|
2335
|
+
step: 'snapshot-binding',
|
|
2336
|
+
status: 'completed',
|
|
2337
|
+
inputDigest: sourceInputClosure.digest,
|
|
2338
|
+
snapshotSourceCount: snapshotSourceResult.observation.snapshotSourceCount,
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2186
2342
|
// --- Step 6: Remote uniqueness (deferred to publish preflight) ---
|
|
2187
2343
|
// Prepare only observes the previous public baseline (already done above).
|
|
2188
2344
|
// Remote uniqueness checks (tag, GitHub Release, npm version) are deferred
|
|
@@ -2665,6 +2821,7 @@ export async function prepareRelease(options) {
|
|
|
2665
2821
|
} : {}),
|
|
2666
2822
|
units,
|
|
2667
2823
|
externalActions,
|
|
2824
|
+
...(sourceAuthority ? { sourceAuthority } : {}),
|
|
2668
2825
|
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
|
2669
2826
|
};
|
|
2670
2827
|
|
package/src/commands/publish.mjs
CHANGED
|
@@ -59,10 +59,15 @@ import { appendRunState, createProductionRunDir, writeRunAtomic, resolveDefaultR
|
|
|
59
59
|
import {
|
|
60
60
|
ReleaseError,
|
|
61
61
|
GATE_FAILED,
|
|
62
|
+
CONFIG_MISSING,
|
|
62
63
|
BASELINE_CHANGED,
|
|
63
64
|
PARTIAL_RELEASE,
|
|
64
65
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
65
66
|
} from '../core/errors.mjs';
|
|
67
|
+
import {
|
|
68
|
+
verifyRemoteSourceContent,
|
|
69
|
+
createSourceAuthorityReceipt,
|
|
70
|
+
} from '../core/source-authority.mjs';
|
|
66
71
|
import { assertTransition, PUBLISHING, PUBLISHED, PARTIAL } from '../core/state-machine.mjs';
|
|
67
72
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
68
73
|
import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY, isPropagatingMissing } from '../core/observe-retry.mjs';
|
|
@@ -527,6 +532,13 @@ export async function publishRelease(options) {
|
|
|
527
532
|
if (productionMode && !isProductionPlan) {
|
|
528
533
|
throw new ReleaseError(GATE_FAILED, 'production publish requires a github-npm-v1 frozen plan');
|
|
529
534
|
}
|
|
535
|
+
if (isProductionPlan && !plan.sourceAuthority) {
|
|
536
|
+
throw new ReleaseError(
|
|
537
|
+
CONFIG_MISSING,
|
|
538
|
+
'production publish requires a frozen sourceAuthority binding; re-run prepare with project.sourceRepository configured',
|
|
539
|
+
{ gate: 'source-authority' },
|
|
540
|
+
);
|
|
541
|
+
}
|
|
530
542
|
const verifiedFrozenSnapshots = new Map();
|
|
531
543
|
if (isProductionPlan) {
|
|
532
544
|
if (!plan.production.assetRoot || plan.production.assetRoot === '.') {
|
|
@@ -932,6 +944,65 @@ export async function publishRelease(options) {
|
|
|
932
944
|
}
|
|
933
945
|
}
|
|
934
946
|
|
|
947
|
+
// =======================================================================
|
|
948
|
+
// Safety Gate 11: Source authority content closure verification
|
|
949
|
+
// Verifies that the frozen source-input closure content exists in the
|
|
950
|
+
// workspace's remote default branch. Must pass before ANY adapter execute.
|
|
951
|
+
// Failure => zero adapter execute calls.
|
|
952
|
+
// =======================================================================
|
|
953
|
+
let sourceAuthorityReceipt = null;
|
|
954
|
+
if (plan.sourceAuthority) {
|
|
955
|
+
await evidence.append({ phase: 'safety-gate', gate: 'source-authority', status: 'started' });
|
|
956
|
+
|
|
957
|
+
const sa = plan.sourceAuthority;
|
|
958
|
+
|
|
959
|
+
// Verify the remote source tree against the entries frozen into the
|
|
960
|
+
// digest-bound plan. Publish must never rebuild authority from the
|
|
961
|
+
// mutable workspace or current config.
|
|
962
|
+
const frozenClosure = {
|
|
963
|
+
algorithmVersion: sa.algorithmVersion,
|
|
964
|
+
digest: sa.inputDigest,
|
|
965
|
+
entries: sa.entries,
|
|
966
|
+
};
|
|
967
|
+
const remoteResult = await verifyRemoteSourceContent({
|
|
968
|
+
sourceRepository: sa.sourceRepository,
|
|
969
|
+
defaultBranch: sa.defaultBranch,
|
|
970
|
+
closure: frozenClosure,
|
|
971
|
+
readRemoteFn: options.readRemoteSourceFn,
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
if (!remoteResult.passed) {
|
|
975
|
+
const errorCode = remoteResult.error?.code ?? GATE_FAILED;
|
|
976
|
+
await evidence.append({
|
|
977
|
+
phase: 'safety-gate',
|
|
978
|
+
gate: 'source-authority',
|
|
979
|
+
status: 'failed',
|
|
980
|
+
error: remoteResult.error,
|
|
981
|
+
});
|
|
982
|
+
throw new ReleaseError(
|
|
983
|
+
errorCode,
|
|
984
|
+
`source authority content gate failed: ${remoteResult.error?.message}`,
|
|
985
|
+
remoteResult.error,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Create receipt for successful verification
|
|
990
|
+
sourceAuthorityReceipt = createSourceAuthorityReceipt({
|
|
991
|
+
plan,
|
|
992
|
+
result: 'CONSISTENT',
|
|
993
|
+
observation: remoteResult.observation,
|
|
994
|
+
clock: clockFn,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
await evidence.append({
|
|
998
|
+
phase: 'safety-gate',
|
|
999
|
+
gate: 'source-authority',
|
|
1000
|
+
status: 'passed',
|
|
1001
|
+
sourceRepository: sa.sourceRepository,
|
|
1002
|
+
defaultBranch: sa.defaultBranch,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
935
1006
|
// =======================================================================
|
|
936
1007
|
// All safety gates passed -- prepare for execution
|
|
937
1008
|
// =======================================================================
|
|
@@ -1069,6 +1140,7 @@ export async function publishRelease(options) {
|
|
|
1069
1140
|
: {}),
|
|
1070
1141
|
})),
|
|
1071
1142
|
startedAt,
|
|
1143
|
+
...(sourceAuthorityReceipt ? { sourceAuthorityReceipts: [sourceAuthorityReceipt] } : {}),
|
|
1072
1144
|
...(finishedAt ? { finishedAt } : {}),
|
|
1073
1145
|
});
|
|
1074
1146
|
let stateSequence = 0;
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
60
60
|
} from '../core/errors.mjs';
|
|
61
61
|
import { assertTransition, PARTIAL, PUBLISHED, BLOCKED } from '../core/state-machine.mjs';
|
|
62
|
+
import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
|
|
62
63
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
63
64
|
import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY } from '../core/observe-retry.mjs';
|
|
64
65
|
|
|
@@ -233,6 +234,18 @@ export async function reconcileRelease(options) {
|
|
|
233
234
|
`reconcile source command must be publish or reconcile, got "${sourceRun.command}"`,
|
|
234
235
|
);
|
|
235
236
|
}
|
|
237
|
+
let sourceAuthorityReceipt = null;
|
|
238
|
+
if (plan.sourceAuthority) {
|
|
239
|
+
const receiptResult = verifySourceAuthorityReceipt({ plan, run: sourceRun });
|
|
240
|
+
if (!receiptResult.passed) {
|
|
241
|
+
throw new ReleaseError(
|
|
242
|
+
GATE_FAILED,
|
|
243
|
+
`reconcile source run has no valid source-authority receipt: ${receiptResult.reason}`,
|
|
244
|
+
{ gate: 'source-authority', sourceRunId: sourceRun.runId },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
sourceAuthorityReceipt = receiptResult.receipt;
|
|
248
|
+
}
|
|
236
249
|
|
|
237
250
|
let consumedApprovalPath = sourceRun.approvalPath;
|
|
238
251
|
let consumedApprovalDigest = sourceRun.approvalDigest;
|
|
@@ -956,6 +969,9 @@ export async function reconcileRelease(options) {
|
|
|
956
969
|
sourceRunDigest: sourceAuthorityDigest,
|
|
957
970
|
sourceRunPath,
|
|
958
971
|
status,
|
|
972
|
+
...(sourceAuthorityReceipt
|
|
973
|
+
? { sourceAuthorityReceipts: [sourceAuthorityReceipt] }
|
|
974
|
+
: {}),
|
|
959
975
|
checkpoints: planActions.map((action) => {
|
|
960
976
|
const value = actionResults.get(action.id);
|
|
961
977
|
const normalized = value === 'deferred' ? 'deferred'
|
|
@@ -1303,6 +1319,9 @@ export async function reconcileRelease(options) {
|
|
|
1303
1319
|
sourceRunDigest,
|
|
1304
1320
|
sourceRunPath,
|
|
1305
1321
|
status: overallStatus,
|
|
1322
|
+
...(sourceAuthorityReceipt
|
|
1323
|
+
? { sourceAuthorityReceipts: [sourceAuthorityReceipt] }
|
|
1324
|
+
: {}),
|
|
1306
1325
|
checkpoints: planActions.map((a) => {
|
|
1307
1326
|
const status = actionResults.get(a.id) ?? 'pending';
|
|
1308
1327
|
const normalized = status === 'deferred' ? 'deferred'
|
package/src/commands/setup.mjs
CHANGED
|
@@ -976,6 +976,21 @@ function buildRecommendedProposal(facts, candidates) {
|
|
|
976
976
|
const legacyOwner = facts.legacyReleaseConfigs
|
|
977
977
|
.map((c) => c.owner)
|
|
978
978
|
.find(Boolean);
|
|
979
|
+
const sourceRepositoryCandidates = [...new Set(
|
|
980
|
+
facts.git.remotes.map((remote) => remote.repo).filter(Boolean),
|
|
981
|
+
)].sort();
|
|
982
|
+
if (sourceRepositoryCandidates.length !== 1) {
|
|
983
|
+
return {
|
|
984
|
+
answers: null,
|
|
985
|
+
conflicts: [{
|
|
986
|
+
code: sourceRepositoryCandidates.length === 0
|
|
987
|
+
? 'SOURCE_REPOSITORY_MISSING'
|
|
988
|
+
: 'SOURCE_REPOSITORY_AMBIGUOUS',
|
|
989
|
+
candidates: sourceRepositoryCandidates,
|
|
990
|
+
}],
|
|
991
|
+
assumptions,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
979
994
|
|
|
980
995
|
const units = [];
|
|
981
996
|
for (const unit of candidates.units) {
|
|
@@ -1156,6 +1171,7 @@ function buildRecommendedProposal(facts, candidates) {
|
|
|
1156
1171
|
project: {
|
|
1157
1172
|
name: facts.packages[0]?.name ?? 'project',
|
|
1158
1173
|
defaultBranch: facts.legacyReleaseConfigs[0]?.defaultBranch ?? facts.git.branch ?? 'main',
|
|
1174
|
+
sourceRepository: sourceRepositoryCandidates[0],
|
|
1159
1175
|
},
|
|
1160
1176
|
releaseUnits: units,
|
|
1161
1177
|
...(selectedGateIds.length > 0 ? {
|
|
@@ -1517,6 +1533,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1517
1533
|
|
|
1518
1534
|
const facts = await discoverFacts(rootReal);
|
|
1519
1535
|
const candidates = buildCandidates(facts);
|
|
1536
|
+
const sourceRepositoryCandidates = [...new Set(
|
|
1537
|
+
facts.git.remotes.map((remote) => remote.repo).filter(Boolean),
|
|
1538
|
+
)].sort();
|
|
1520
1539
|
let answers = null;
|
|
1521
1540
|
if (answersPath) {
|
|
1522
1541
|
const resolvedAnswers = isAbsolute(answersPath) ? answersPath : resolve(rootReal, answersPath);
|
|
@@ -1527,6 +1546,7 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1527
1546
|
const digestAuthority = {
|
|
1528
1547
|
setupVersion: 1,
|
|
1529
1548
|
facts,
|
|
1549
|
+
sourceRepositoryCandidates,
|
|
1530
1550
|
releaseUnitCandidates: candidates.units,
|
|
1531
1551
|
gateCandidates: candidates.gates,
|
|
1532
1552
|
selectedGateIds,
|
|
@@ -1599,6 +1619,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1599
1619
|
const lockedAuthority = {
|
|
1600
1620
|
setupVersion: 1,
|
|
1601
1621
|
facts: lockedFacts,
|
|
1622
|
+
sourceRepositoryCandidates: [...new Set(
|
|
1623
|
+
lockedFacts.git.remotes.map((remote) => remote.repo).filter(Boolean),
|
|
1624
|
+
)].sort(),
|
|
1602
1625
|
releaseUnitCandidates: lockedCandidates.units,
|
|
1603
1626
|
gateCandidates: lockedCandidates.gates,
|
|
1604
1627
|
selectedGateIds: lockedAnswers.selectedGateIds,
|
|
@@ -1622,6 +1645,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1622
1645
|
const finalAuthority = {
|
|
1623
1646
|
setupVersion: 1,
|
|
1624
1647
|
facts: finalFacts,
|
|
1648
|
+
sourceRepositoryCandidates: [...new Set(
|
|
1649
|
+
finalFacts.git.remotes.map((remote) => remote.repo).filter(Boolean),
|
|
1650
|
+
)].sort(),
|
|
1625
1651
|
releaseUnitCandidates: finalCandidates.units,
|
|
1626
1652
|
gateCandidates: finalCandidates.gates,
|
|
1627
1653
|
selectedGateIds: finalAnswers.selectedGateIds,
|