release-skill 0.2.8 → 0.3.0
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 +36 -0
- package/INSTALL.md +4 -4
- package/INSTALL.zh-CN.md +4 -4
- package/README.md +38 -13
- package/README.zh-CN.md +36 -13
- 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 +3961 -2882
- package/adapters/claude/skills/release-help/SKILL.md +9 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +3961 -2882
- package/adapters/codex/skills/release-help/SKILL.md +9 -1
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +3961 -2882
- package/adapters/kimi/skills/release-help/SKILL.md +9 -1
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +3961 -2882
- package/adapters/workbuddy/skills/release-help/SKILL.md +9 -1
- package/bin/release-skill-cli.mjs +180 -4
- package/bin/release-skill.bundle.mjs +3961 -2882
- package/package.json +1 -1
- package/skills/release-help/SKILL.md +9 -1
- package/skills-src/release-help/SKILL.md +9 -1
- package/src/adapters/plugin-marketplace.mjs +1 -1
- package/src/adapters/push-snapshot.mjs +3 -1
- package/src/commands/assess.mjs +18 -5
- package/src/commands/attest.mjs +195 -0
- package/src/commands/hooks.mjs +46 -0
- package/src/commands/prepare.mjs +14 -2
- package/src/commands/ship.mjs +356 -0
- package/src/commands/verify.mjs +147 -4
- package/src/core/git-transport.mjs +93 -0
- package/src/core/public-surface.mjs +26 -0
- package/src/core/release-metadata.mjs +105 -0
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ description: "Discoverable entry point for release-skill: dependency and environ
|
|
|
12
12
|
## 职责
|
|
13
13
|
|
|
14
14
|
- 依赖和环境检查:Node.js >= 22、Git 决定本地准备就绪度;npm/gh 另行决定生产依赖就绪度
|
|
15
|
-
- 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline
|
|
15
|
+
- 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`
|
|
16
16
|
- 最小示例:展示从 release-help 到 release-assess 的最短路径
|
|
17
17
|
- 只读诊断:运行 dry-run 检查,不修改任何文件
|
|
18
18
|
- 故障引导:根据错误码指向对应的修复 Skill
|
|
@@ -35,6 +35,14 @@ description: "Discoverable entry point for release-skill: dependency and environ
|
|
|
35
35
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" help --json
|
|
36
36
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" setup --root <path> --json
|
|
37
37
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offline --json
|
|
38
|
+
# 日常发布快速路径:最多两个授权摘要,状态文件可恢复
|
|
39
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-version <version> --json
|
|
40
|
+
# 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
|
|
41
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" hooks validate --root <path> \
|
|
42
|
+
--acknowledge-hook-side-effects --json
|
|
43
|
+
# Kimi/CodeBuddy 安装完成后记录人工事实,不手改 JSON
|
|
44
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" attest --root <path> \
|
|
45
|
+
--platform <kimi|codebuddy> --plugin <id> --result <passed|failed> --actor <person> --json
|
|
38
46
|
# 发布文档刷新:默认只读演练
|
|
39
47
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> --json
|
|
40
48
|
# 摘要确认后的本地写入(三项绑定缺一不可)
|
|
@@ -12,7 +12,7 @@ description: "Discoverable entry point for release-skill: dependency and environ
|
|
|
12
12
|
## 职责
|
|
13
13
|
|
|
14
14
|
- 依赖和环境检查:Node.js >= 22、Git 决定本地准备就绪度;npm/gh 另行决定生产依赖就绪度
|
|
15
|
-
- 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline
|
|
15
|
+
- 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`
|
|
16
16
|
- 最小示例:展示从 release-help 到 release-assess 的最短路径
|
|
17
17
|
- 只读诊断:运行 dry-run 检查,不修改任何文件
|
|
18
18
|
- 故障引导:根据错误码指向对应的修复 Skill
|
|
@@ -35,6 +35,14 @@ description: "Discoverable entry point for release-skill: dependency and environ
|
|
|
35
35
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" help --json
|
|
36
36
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" setup --root <path> --json
|
|
37
37
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offline --json
|
|
38
|
+
# 日常发布快速路径:最多两个授权摘要,状态文件可恢复
|
|
39
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-version <version> --json
|
|
40
|
+
# 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
|
|
41
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" hooks validate --root <path> \
|
|
42
|
+
--acknowledge-hook-side-effects --json
|
|
43
|
+
# Kimi/CodeBuddy 安装完成后记录人工事实,不手改 JSON
|
|
44
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" attest --root <path> \
|
|
45
|
+
--platform <kimi|codebuddy> --plugin <id> --result <passed|failed> --actor <person> --json
|
|
38
46
|
# 发布文档刷新:默认只读演练
|
|
39
47
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> --json
|
|
40
48
|
# 摘要确认后的本地写入(三项绑定缺一不可)
|
|
@@ -476,7 +476,7 @@ async function resolveInstalledPayloadSubpath(snapshotDir, sourceEntries, action
|
|
|
476
476
|
return extractDeclaredPluginSource(consumer, matches[0]);
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
async function verifyInstalledMarketplacePayload(action, context, installPath, consumer) {
|
|
479
|
+
export async function verifyInstalledMarketplacePayload(action, context, installPath, consumer) {
|
|
480
480
|
const sourcePath = await resolveFrozenPath(
|
|
481
481
|
context.root,
|
|
482
482
|
action.snapshotPath,
|
|
@@ -32,7 +32,9 @@ export function githubRepositoryUrl(repo, host = 'github.com') {
|
|
|
32
32
|
) {
|
|
33
33
|
throw new Error('githubHost must be a valid hostname');
|
|
34
34
|
}
|
|
35
|
-
return
|
|
35
|
+
return process.env.RELEASE_SKILL_GIT_TRANSPORT === 'ssh'
|
|
36
|
+
? `git@${host}:${repo}.git`
|
|
37
|
+
: `https://${host}/${repo}.git`;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function validateOid(value, label) {
|
package/src/commands/assess.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { promisify } from 'node:util';
|
|
|
23
23
|
|
|
24
24
|
import { loadProjectConfig } from '../core/config.mjs';
|
|
25
25
|
import { ReleaseError, CONFIG_INVALID, GATE_FAILED } from '../core/errors.mjs';
|
|
26
|
+
import { collectExpectedPublicSurfaceAdoptionWarnings } from '../core/public-surface.mjs';
|
|
26
27
|
|
|
27
28
|
const execFile = promisify(execFileCb);
|
|
28
29
|
|
|
@@ -880,23 +881,35 @@ export async function assessProject(options) {
|
|
|
880
881
|
// --- 2. Topology identification ---
|
|
881
882
|
const topology = identifyTopology(config);
|
|
882
883
|
|
|
883
|
-
// --- 3.
|
|
884
|
+
// --- 3. Expected public-surface adoption ---
|
|
885
|
+
for (const warning of collectExpectedPublicSurfaceAdoptionWarnings(config)) {
|
|
886
|
+
allGaps.push(createGap({
|
|
887
|
+
scope: GapScope.PROJECT,
|
|
888
|
+
category: GapCategory.POLICY,
|
|
889
|
+
severity: Severity.WARNING,
|
|
890
|
+
code: warning.code,
|
|
891
|
+
message: warning.message,
|
|
892
|
+
file: '.release-skill/project.yaml',
|
|
893
|
+
}));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// --- 4. Common docs check ---
|
|
884
897
|
const docGaps = await checkCommonDocs(root, config);
|
|
885
898
|
allGaps.push(...docGaps);
|
|
886
899
|
|
|
887
|
-
// ---
|
|
900
|
+
// --- 5. Plugin manifest check ---
|
|
888
901
|
const manifestGaps = await checkPluginManifests(root, config);
|
|
889
902
|
allGaps.push(...manifestGaps);
|
|
890
903
|
|
|
891
|
-
// ---
|
|
904
|
+
// --- 6. Package metadata check ---
|
|
892
905
|
const metadataGaps = await checkPackageMetadata(root, config);
|
|
893
906
|
allGaps.push(...metadataGaps);
|
|
894
907
|
|
|
895
|
-
// ---
|
|
908
|
+
// --- 7. Remote prerequisites ---
|
|
896
909
|
const remoteGaps = await checkRemotePrerequisites(root, config, offline);
|
|
897
910
|
allGaps.push(...remoteGaps);
|
|
898
911
|
|
|
899
|
-
// ---
|
|
912
|
+
// --- 8. README structure check ---
|
|
900
913
|
const readmeGaps = await checkReadmeStructure(root, config);
|
|
901
914
|
allGaps.push(...readmeGaps);
|
|
902
915
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { SAFE_ID_RE, writeEvidenceAtomic } from '../adapters/contract.mjs';
|
|
5
|
+
import { ReleaseError, GATE_FAILED, MISSING_PARAMETERS } from '../core/errors.mjs';
|
|
6
|
+
|
|
7
|
+
const PLATFORM = Object.freeze({
|
|
8
|
+
kimi: {
|
|
9
|
+
directory: 'kimi-attestations',
|
|
10
|
+
requirementFile: 'release-skill-kimi-manual-install.json',
|
|
11
|
+
},
|
|
12
|
+
codebuddy: {
|
|
13
|
+
directory: 'codebuddy-attestations',
|
|
14
|
+
requirementFile: 'release-skill-codebuddy-manual-install.json',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function validateInstalledConsumerClosure({
|
|
19
|
+
root,
|
|
20
|
+
platform,
|
|
21
|
+
requirement,
|
|
22
|
+
installPath,
|
|
23
|
+
}) {
|
|
24
|
+
const planPath = resolve(root, '.release-skill', 'plans', `${requirement.planDigest}.json`);
|
|
25
|
+
const [
|
|
26
|
+
{ validatePlan, computePlanDigest, assertImmutablePlanAuthority },
|
|
27
|
+
{ createPluginMarketplaceAdapter, verifyInstalledMarketplacePayload },
|
|
28
|
+
] = await Promise.all([
|
|
29
|
+
import('../core/plan.mjs'),
|
|
30
|
+
import('../adapters/plugin-marketplace.mjs'),
|
|
31
|
+
]);
|
|
32
|
+
let plan;
|
|
33
|
+
try {
|
|
34
|
+
plan = JSON.parse(await readFile(planPath, 'utf8'));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new ReleaseError(
|
|
37
|
+
GATE_FAILED,
|
|
38
|
+
`cannot read the frozen plan required to verify the installed ${platform} payload: ${error.message}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
validatePlan(plan);
|
|
42
|
+
assertImmutablePlanAuthority(planPath, plan);
|
|
43
|
+
if (computePlanDigest(plan) !== requirement.planDigest) {
|
|
44
|
+
throw new ReleaseError(GATE_FAILED, 'manual requirement planDigest does not match the frozen plan');
|
|
45
|
+
}
|
|
46
|
+
const actionType = `${platform}-marketplace-install`;
|
|
47
|
+
const action = (plan.externalActions ?? []).find((candidate) => (
|
|
48
|
+
candidate.type === actionType
|
|
49
|
+
&& candidate.parameters?.plugin === requirement.plugin
|
|
50
|
+
&& candidate.parameters?.version === requirement.version
|
|
51
|
+
));
|
|
52
|
+
if (!action) {
|
|
53
|
+
throw new ReleaseError(GATE_FAILED, `frozen plan has no matching ${platform} install action`);
|
|
54
|
+
}
|
|
55
|
+
const adapter = createPluginMarketplaceAdapter();
|
|
56
|
+
const context = {
|
|
57
|
+
externalWritesAuthorized: false,
|
|
58
|
+
isolatedConsumerWritesAuthorized: false,
|
|
59
|
+
plan,
|
|
60
|
+
baseline: plan.baseline,
|
|
61
|
+
root,
|
|
62
|
+
};
|
|
63
|
+
const actionInput = { actionType, ...action.parameters };
|
|
64
|
+
const preflight = await adapter.preflight(actionInput, context);
|
|
65
|
+
if (preflight.status !== 'PREFLIGHT_PASSED') {
|
|
66
|
+
throw new ReleaseError(
|
|
67
|
+
GATE_FAILED,
|
|
68
|
+
`${platform} installed-payload preflight failed: ${preflight.error}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const binding = await verifyInstalledMarketplacePayload(
|
|
72
|
+
actionInput,
|
|
73
|
+
context,
|
|
74
|
+
installPath,
|
|
75
|
+
platform,
|
|
76
|
+
);
|
|
77
|
+
return {
|
|
78
|
+
payloadDigest: action.parameters.manifestDigest,
|
|
79
|
+
manifestDigest: binding.manifestDigest,
|
|
80
|
+
extraInstalledPaths: binding.extraInstalledPaths ?? [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Record the human fact needed by an interactive-only consumer. Identity
|
|
86
|
+
* fields come exclusively from the generated requirement; the operator only
|
|
87
|
+
* supplies the result, actor and optional observed install facts.
|
|
88
|
+
*/
|
|
89
|
+
export async function recordManualAttestation(options = {}, injected = {}) {
|
|
90
|
+
const {
|
|
91
|
+
root = process.cwd(),
|
|
92
|
+
platform,
|
|
93
|
+
plugin,
|
|
94
|
+
actor,
|
|
95
|
+
result,
|
|
96
|
+
installPath,
|
|
97
|
+
installChannel,
|
|
98
|
+
note,
|
|
99
|
+
clock = () => new Date().toISOString(),
|
|
100
|
+
} = options;
|
|
101
|
+
const descriptor = PLATFORM[platform];
|
|
102
|
+
if (!descriptor || !SAFE_ID_RE.test(plugin ?? '')) {
|
|
103
|
+
throw new ReleaseError(
|
|
104
|
+
MISSING_PARAMETERS,
|
|
105
|
+
'attest requires --platform <kimi|codebuddy> and a safe --plugin <id>',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (typeof actor !== 'string' || actor.trim() === '') {
|
|
109
|
+
throw new ReleaseError(MISSING_PARAMETERS, 'attest requires --actor <person>');
|
|
110
|
+
}
|
|
111
|
+
if (result !== 'passed' && result !== 'failed') {
|
|
112
|
+
throw new ReleaseError(MISSING_PARAMETERS, 'attest requires --result <passed|failed>');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const authorityDir = resolve(root, '.release-skill', descriptor.directory, plugin);
|
|
116
|
+
const requirementPath = join(authorityDir, descriptor.requirementFile);
|
|
117
|
+
let requirement;
|
|
118
|
+
try {
|
|
119
|
+
requirement = JSON.parse(await readFile(requirementPath, 'utf8'));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new ReleaseError(
|
|
122
|
+
GATE_FAILED,
|
|
123
|
+
`cannot read generated ${platform} requirement: ${error.message}`,
|
|
124
|
+
{ platform, plugin, requirementPath },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
requirement.platform !== platform
|
|
129
|
+
|| requirement.plugin !== plugin
|
|
130
|
+
|| typeof requirement.version !== 'string'
|
|
131
|
+
|| !/^[a-f0-9]{64}$/.test(requirement.planDigest ?? '')
|
|
132
|
+
|| typeof requirement.attestationFile !== 'string'
|
|
133
|
+
) {
|
|
134
|
+
throw new ReleaseError(
|
|
135
|
+
GATE_FAILED,
|
|
136
|
+
`generated ${platform} requirement has invalid identity fields`,
|
|
137
|
+
{ platform, plugin, requirementPath },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (result === 'passed' && (typeof installPath !== 'string' || installPath === '')) {
|
|
141
|
+
throw new ReleaseError(
|
|
142
|
+
MISSING_PARAMETERS,
|
|
143
|
+
`${platform} passed attestation requires --install-path for installed payload verification`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const needsInstallChannel = Object.hasOwn(requirement.attestationTemplate ?? {}, 'installChannel');
|
|
147
|
+
if (
|
|
148
|
+
result === 'passed'
|
|
149
|
+
&& needsInstallChannel
|
|
150
|
+
&& installChannel !== 'desktop'
|
|
151
|
+
&& installChannel !== 'cli'
|
|
152
|
+
) {
|
|
153
|
+
throw new ReleaseError(
|
|
154
|
+
MISSING_PARAMETERS,
|
|
155
|
+
`${platform} passed attestation requires --install-channel <desktop|cli>`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const installBinding = result === 'passed'
|
|
160
|
+
? await (injected.validateInstalledConsumerClosure ?? validateInstalledConsumerClosure)({
|
|
161
|
+
root,
|
|
162
|
+
platform,
|
|
163
|
+
requirement,
|
|
164
|
+
installPath,
|
|
165
|
+
})
|
|
166
|
+
: null;
|
|
167
|
+
const receipt = {
|
|
168
|
+
platform,
|
|
169
|
+
version: requirement.version,
|
|
170
|
+
planDigest: requirement.planDigest,
|
|
171
|
+
result,
|
|
172
|
+
actor: actor.trim(),
|
|
173
|
+
confirmedAt: clock(),
|
|
174
|
+
...(installPath ? { installPath } : {}),
|
|
175
|
+
...(installChannel ? { installChannel } : {}),
|
|
176
|
+
...(note ? { note } : {}),
|
|
177
|
+
...(installBinding ? {
|
|
178
|
+
payloadDigest: installBinding.payloadDigest,
|
|
179
|
+
installedClosureVerified: true,
|
|
180
|
+
extraInstalledPaths: installBinding.extraInstalledPaths,
|
|
181
|
+
} : {}),
|
|
182
|
+
};
|
|
183
|
+
const attestationPath = join(authorityDir, requirement.attestationFile);
|
|
184
|
+
await writeEvidenceAtomic(attestationPath, receipt);
|
|
185
|
+
return {
|
|
186
|
+
command: 'attest',
|
|
187
|
+
status: 'RECORDED',
|
|
188
|
+
platform,
|
|
189
|
+
plugin,
|
|
190
|
+
version: requirement.version,
|
|
191
|
+
planDigest: requirement.planDigest,
|
|
192
|
+
requirementPath,
|
|
193
|
+
attestationPath,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { loadProjectConfig } from '../core/config.mjs';
|
|
5
|
+
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
6
|
+
import { runDeclaredHooks } from './prepare.mjs';
|
|
7
|
+
import { ReleaseError, GATE_FAILED } from '../core/errors.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run the declared development gates and populate the exact same content-bound
|
|
11
|
+
* hook receipts consumed by prepare. A later ship/prepare reuses only
|
|
12
|
+
* cacheable hooks whose declared input closure is byte-identical.
|
|
13
|
+
*/
|
|
14
|
+
export async function validateDeclaredHooks(options = {}) {
|
|
15
|
+
const {
|
|
16
|
+
root = process.cwd(),
|
|
17
|
+
hooksAuthorized,
|
|
18
|
+
hookCache = true,
|
|
19
|
+
runDir = resolve(root, '.release-skill', 'runs', `hooks-${Date.now()}`),
|
|
20
|
+
} = options;
|
|
21
|
+
if (hooksAuthorized !== true) {
|
|
22
|
+
throw new ReleaseError(
|
|
23
|
+
GATE_FAILED,
|
|
24
|
+
'hooks validate executes declared project commands; pass --acknowledge-hook-side-effects',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const { config, configDigest } = await loadProjectConfig({ root });
|
|
28
|
+
await mkdir(runDir, { recursive: true });
|
|
29
|
+
const evidence = createEvidenceWriter({
|
|
30
|
+
runDir,
|
|
31
|
+
command: 'hooks-validate',
|
|
32
|
+
clock: () => new Date().toISOString(),
|
|
33
|
+
});
|
|
34
|
+
const startedAt = new Date().toISOString();
|
|
35
|
+
await runDeclaredHooks(config, root, evidence, undefined, { hookCache });
|
|
36
|
+
return {
|
|
37
|
+
command: 'hooks validate',
|
|
38
|
+
status: 'PASSED',
|
|
39
|
+
configDigest,
|
|
40
|
+
hooks: Object.keys(config.hooks ?? {}).sort(),
|
|
41
|
+
cacheEnabled: hookCache,
|
|
42
|
+
evidenceDir: runDir,
|
|
43
|
+
startedAt,
|
|
44
|
+
finishedAt: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -29,7 +29,10 @@ import { loadProjectConfig } from '../core/config.mjs';
|
|
|
29
29
|
import { captureBaseline } from '../core/baseline.mjs';
|
|
30
30
|
import { runHook } from '../core/hooks.mjs';
|
|
31
31
|
import { computeHookCacheKey, readHookCache, writeHookCache } from '../core/hook-cache.mjs';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
assertExpectedPublicSurface,
|
|
34
|
+
collectExpectedPublicSurfaceAdoptionWarnings,
|
|
35
|
+
} from '../core/public-surface.mjs';
|
|
33
36
|
import { runSnapshotVerificationGates } from '../core/verification-gates.mjs';
|
|
34
37
|
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
35
38
|
import { computePlanDigest, writePlanAtomic, writePlanImmutable } from '../core/plan.mjs';
|
|
@@ -1670,7 +1673,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1670
1673
|
* every declared hook runs in full and the incremental hook cache is neither
|
|
1671
1674
|
* read nor written.
|
|
1672
1675
|
*
|
|
1673
|
-
* @returns {Promise<{ planPath: string, planDigest: string, evidenceDir: string }>}
|
|
1676
|
+
* @returns {Promise<{ planPath: string, planDigest: string, evidenceDir: string, warnings: ReadonlyArray<object> }>}
|
|
1674
1677
|
*
|
|
1675
1678
|
* @throws {ReleaseError} on any gate failure. No PREPARED plan is written.
|
|
1676
1679
|
*/
|
|
@@ -1750,6 +1753,7 @@ export async function prepareRelease(options) {
|
|
|
1750
1753
|
await evidence.append({ phase: 'config', status: 'started' });
|
|
1751
1754
|
|
|
1752
1755
|
const { config, configPath, configDigest } = await loadProjectConfig({ root: realRoot });
|
|
1756
|
+
const adoptionWarnings = collectExpectedPublicSurfaceAdoptionWarnings(config);
|
|
1753
1757
|
|
|
1754
1758
|
await evidence.append({
|
|
1755
1759
|
phase: 'config',
|
|
@@ -1757,6 +1761,13 @@ export async function prepareRelease(options) {
|
|
|
1757
1761
|
configPath: relative(realRoot, configPath),
|
|
1758
1762
|
configDigest,
|
|
1759
1763
|
});
|
|
1764
|
+
for (const warning of adoptionWarnings) {
|
|
1765
|
+
await evidence.append({
|
|
1766
|
+
phase: 'public-surface-adoption',
|
|
1767
|
+
status: 'warning',
|
|
1768
|
+
...warning,
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1760
1771
|
|
|
1761
1772
|
// --- Step 1b: Resolve authoritative versions and gate release-document
|
|
1762
1773
|
// freshness BEFORE hook authorization ---
|
|
@@ -2913,6 +2924,7 @@ export async function prepareRelease(options) {
|
|
|
2913
2924
|
planPath: writtenPath,
|
|
2914
2925
|
planDigest,
|
|
2915
2926
|
evidenceDir,
|
|
2927
|
+
warnings: adoptionWarnings,
|
|
2916
2928
|
};
|
|
2917
2929
|
} catch (err) {
|
|
2918
2930
|
// Record failure evidence
|