release-skill 0.2.3 → 0.2.5

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.
Files changed (55) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +46 -0
  7. package/CONTRIBUTING.md +27 -0
  8. package/INSTALL.md +95 -139
  9. package/INSTALL.zh-CN.md +70 -121
  10. package/README.md +266 -913
  11. package/README.zh-CN.md +224 -535
  12. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  13. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  14. package/adapters/claude/bin/release-skill.bundle.mjs +19805 -17556
  15. package/adapters/claude/schemas/release-plan.schema.json +228 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +155 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +19805 -17556
  20. package/adapters/codex/schemas/release-plan.schema.json +228 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +155 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +19805 -17556
  25. package/adapters/kimi/schemas/release-plan.schema.json +228 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +155 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +19805 -17556
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +228 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +155 -2
  33. package/bin/release-skill.bundle.mjs +19805 -17556
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +228 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +155 -2
  38. package/scripts/sync-public-files.mjs +20 -9
  39. package/src/adapters/plugin-marketplace.mjs +1237 -403
  40. package/src/commands/prepare.mjs +454 -40
  41. package/src/commands/publish.mjs +169 -75
  42. package/src/commands/reconcile.mjs +92 -327
  43. package/src/commands/setup.mjs +148 -20
  44. package/src/commands/verify.mjs +454 -20
  45. package/src/core/baseline.mjs +8 -1
  46. package/src/core/checkpoints.mjs +50 -7
  47. package/src/core/config.mjs +15 -0
  48. package/src/core/errors.mjs +2 -0
  49. package/src/core/installation-contract.mjs +341 -0
  50. package/src/core/plan.mjs +158 -6
  51. package/src/core/skill-resource-closure.mjs +425 -0
  52. package/src/platforms/codebuddy.mjs +206 -280
  53. package/src/platforms/codex.mjs +369 -0
  54. package/src/platforms/kimi.mjs +191 -119
  55. package/src/platforms/registry.mjs +24 -6
@@ -33,6 +33,11 @@ import { runSnapshotVerificationGates } from '../core/verification-gates.mjs';
33
33
  import { createEvidenceWriter } from '../core/evidence.mjs';
34
34
  import { computePlanDigest, writePlanAtomic, writePlanImmutable } from '../core/plan.mjs';
35
35
  import { sha256Hex } from '../core/digest.mjs';
36
+ import {
37
+ CHECKER_VERSION as SKILL_RESOURCE_CHECKER_VERSION,
38
+ checkSkillResourceClosure,
39
+ createSkillResourceClosureReceipt,
40
+ } from '../core/skill-resource-closure.mjs';
36
41
  import { buildPublicStaging } from '../snapshot/public-map.mjs';
37
42
  import { resolveUnitScopedPath } from '../snapshot/public-path.mjs';
38
43
  import { scanSnapshot } from '../snapshot/scan.mjs';
@@ -50,6 +55,15 @@ import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } fro
50
55
  import { verifyFrozenNpmTarballIdentity } from '../adapters/npm.mjs';
51
56
  import { createProductionPrepareRunDir } from '../core/run.mjs';
52
57
  import { PLATFORMS } from '../platforms/registry.mjs';
58
+ import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
59
+ import { buildInstallationContract, computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from '../core/installation-contract.mjs';
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // 安装契约常量
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /** 消费端安装验证配方版本。算法变更时递增。 */
66
+ const CONSUMER_INSTALL_RECIPE_VERSION = 'consumer-install-v1';
53
67
 
54
68
  // ---------------------------------------------------------------------------
55
69
  // Version resolution
@@ -1085,12 +1099,15 @@ export async function resolveExternalMarketplaceFreezes({
1085
1099
  const version = resolvedVersions[index];
1086
1100
  const githubHost = unit.production?.githubHost ?? 'github.com';
1087
1101
  for (const dist of unit.distributions ?? []) {
1088
- if (dist.marketplaceRepo === undefined || dist.marketplaceRepo === null) continue;
1102
+ // 只处理 standalone-index 来源;bundled-family 不需要外部冻结。
1103
+ if (dist.marketplaceSourceType !== 'standalone-index') continue;
1104
+ // standalone-index 必须有 marketplaceRepo;没有则跳过(兼容旧配置)。
1105
+ if (!dist.marketplaceRepo) continue;
1089
1106
  const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
1090
- if (!platform || platform.marketplaceRefForm === null) {
1107
+ if (!platform) {
1091
1108
  throw new ReleaseError(
1092
1109
  GATE_FAILED,
1093
- `unit "${unit.id}" ${dist.type} distribution declares marketplaceRepo but the platform has no marketplace add capability`,
1110
+ `unit "${unit.id}" ${dist.type} distribution declares standalone-index but the platform is unknown`,
1094
1111
  { unitId: unit.id, distributionType: dist.type },
1095
1112
  );
1096
1113
  }
@@ -1098,7 +1115,7 @@ export async function resolveExternalMarketplaceFreezes({
1098
1115
  throw new ReleaseError(
1099
1116
  GATE_FAILED,
1100
1117
  `unit "${unit.id}" ${dist.type} external marketplace form requires online production prepare to freeze the marketplace commit sha`,
1101
- { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1118
+ { unitId: unit.id, marketplaceSourceType: dist.marketplaceSourceType },
1102
1119
  );
1103
1120
  }
1104
1121
  const observed = await observeHeadFn(dist.marketplaceRepo, { githubHost });
@@ -1110,7 +1127,16 @@ export async function resolveExternalMarketplaceFreezes({
1110
1127
  );
1111
1128
  }
1112
1129
  const sha = observed.sha;
1113
- const manifestPath = platform.manifestPaths.marketplace;
1130
+ // 索引路径:distribution 显式声明优先,否则使用平台默认路径。
1131
+ // 平台注册表没有默认路径时(kimi、codebuddy),必须显式提供。
1132
+ const manifestPath = dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
1133
+ if (!manifestPath) {
1134
+ throw new ReleaseError(
1135
+ GATE_FAILED,
1136
+ `unit "${unit.id}" ${dist.type} cannot determine marketplace index path: neither marketplaceIndexPath nor platform.manifestPaths.marketplace is set`,
1137
+ { unitId: unit.id, distributionType: dist.type },
1138
+ );
1139
+ }
1114
1140
  const fetched = await fetchIndexFn(dist.marketplaceRepo, manifestPath, sha, { githubHost });
1115
1141
  if (fetched.status !== 'fetched' || !fetched.index || typeof fetched.index !== 'object') {
1116
1142
  throw new ReleaseError(
@@ -1120,7 +1146,8 @@ export async function resolveExternalMarketplaceFreezes({
1120
1146
  );
1121
1147
  }
1122
1148
  const marketplaceIndex = fetched.index;
1123
- if (marketplaceIndex.name !== dist.marketplace) {
1149
+ // 校验市场名称(仅当 distribution 显式声明 marketplace 时)
1150
+ if (dist.marketplace && marketplaceIndex.name !== dist.marketplace) {
1124
1151
  throw new ReleaseError(
1125
1152
  GATE_FAILED,
1126
1153
  `unit "${unit.id}" external marketplace index name "${marketplaceIndex.name}" does not match distribution marketplace "${dist.marketplace}"`,
@@ -1144,12 +1171,21 @@ export async function resolveExternalMarketplaceFreezes({
1144
1171
  { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1145
1172
  );
1146
1173
  }
1147
- const ref = platform.marketplaceRefForm === 'sha' ? sha : observed.defaultBranch;
1174
+ // marketplaceRef:Claude 使用默认分支名(name-ref),其余平台使用提交 SHA。
1175
+ // 无 CLI 的平台(kimi、codebuddy)也必须能冻结,ref 用 SHA。
1176
+ const marketplaceRef = platform.marketplaceRefForm === 'name' ? observed.defaultBranch : sha;
1148
1177
  freezes.set(`${unit.id} ${dist.type}`, {
1178
+ // 向后兼容字段(buildExternalActions 使用 repo / ref / marketplace)
1149
1179
  repo: dist.marketplaceRepo,
1150
- ref,
1180
+ ref: marketplaceRef,
1151
1181
  marketplaceCommitSha: sha,
1152
1182
  marketplace: dist.marketplace,
1183
+ // B3B 完整冻结字段
1184
+ marketplaceRepo: dist.marketplaceRepo,
1185
+ marketplaceRef,
1186
+ marketplaceIndexPath: manifestPath,
1187
+ marketplaceName: marketplaceIndex.name,
1188
+ selectedEntry: pluginEntries[0],
1153
1189
  });
1154
1190
  await evidence.append({
1155
1191
  phase: 'external-marketplace-freeze',
@@ -1158,8 +1194,11 @@ export async function resolveExternalMarketplaceFreezes({
1158
1194
  status: 'completed',
1159
1195
  marketplaceRepo: dist.marketplaceRepo,
1160
1196
  marketplaceCommitSha: sha,
1197
+ marketplaceRef,
1198
+ marketplaceIndexPath: manifestPath,
1199
+ marketplaceName: marketplaceIndex.name,
1200
+ selectedEntry: pluginEntries[0],
1161
1201
  defaultBranch: observed.defaultBranch,
1162
- addRef: ref,
1163
1202
  });
1164
1203
  }
1165
1204
  }
@@ -1181,7 +1220,7 @@ export async function resolveExternalMarketplaceFreezes({
1181
1220
  * @param {string} realRoot - The project root for relative path calculation.
1182
1221
  * @returns {object[]} Array of external action descriptors.
1183
1222
  */
1184
- export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map()) {
1223
+ export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map(), frozenDistributions = null) {
1185
1224
  const actions = [];
1186
1225
 
1187
1226
  if (!productionAssets) {
@@ -1246,8 +1285,11 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1246
1285
  // carry: Kimi Code has no non-interactive install/marketplace API, so
1247
1286
  // the kimi action carries no marketplace identity (MINOR-1); plugin +
1248
1287
  // entrySkill are the meaningful identity fields there).
1288
+ const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
1249
1289
  for (const platform of PLATFORMS) {
1250
- const dist = (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1290
+ const dist = frozenUnitDists
1291
+ ? frozenUnitDists.find((d) => d.type === platform.distributionType)
1292
+ : (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1251
1293
  if (!dist) continue;
1252
1294
  const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1253
1295
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
@@ -1261,11 +1303,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1261
1303
  // Normalized marketplace form: explicit mutually exclusive declaration.
1262
1304
  // bundled-family: marketplace and plugin live in the same repo.
1263
1305
  // standalone-index: external marketplace repo indexes a separate plugin repo.
1264
- // Non-automatable platforms (kimi, codebuddy) use human-attestation and
1265
- // carry no marketplace form.
1266
- const marketplaceForm = requiresMarketplace
1267
- ? (externalMarketplace ? 'standalone-index' : 'bundled-family')
1268
- : null;
1306
+ // All four platforms carry marketplaceForm and sourceDescriptor when a
1307
+ // marketplace source type is declared (bundled-family or standalone-index).
1308
+ const marketplaceSourceType = dist.marketplaceSourceType ?? (externalMarketplace ? 'standalone-index' : 'bundled-family');
1309
+ const marketplaceForm = marketplaceSourceType;
1269
1310
  const sourceDescriptor = marketplaceForm === 'standalone-index'
1270
1311
  ? Object.freeze({
1271
1312
  form: 'standalone-index',
@@ -1314,6 +1355,14 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1314
1355
  ...(externalMarketplace ? { marketplaceLocation: 'external' } : {}),
1315
1356
  ...(marketplaceForm ? { marketplaceForm } : {}),
1316
1357
  ...(sourceDescriptor ? { sourceDescriptor } : {}),
1358
+ // 安装契约摘要、算法版本和来源类型,用于完整性交叉校验
1359
+ ...(dist.installationContractDigest ? {
1360
+ installationContractDigest: dist.installationContractDigest,
1361
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1362
+ marketplaceSourceType: dist.marketplaceSourceType,
1363
+ } : {}),
1364
+ // standalone-index 审计字段仅在生产在线冻结成功后出现;
1365
+ // 非生产 action 不携带这三个字段(未冻结时无真实值可用)。
1317
1366
  },
1318
1367
  expected: {
1319
1368
  installed: true,
@@ -1336,6 +1385,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1336
1385
  const asset = productionAssets[index];
1337
1386
  const tagTemplate = unit.version?.tagTemplate ?? `${unit.id}-v{version}`;
1338
1387
  const resolvedTag = asset.tag;
1388
+ const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
1339
1389
 
1340
1390
  // Push snapshot
1341
1391
  actions.push({
@@ -1474,7 +1524,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1474
1524
  // entrySkillFound/manifestDigest expected). Marketplace identity follows
1475
1525
  // the registry's schema required fields — kimi carries none (MINOR-1).
1476
1526
  for (const platform of PLATFORMS) {
1477
- const dist = (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1527
+ const dist = frozenUnitDists
1528
+ ? frozenUnitDists.find((d) => d.type === platform.distributionType)
1529
+ : (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1478
1530
  if (!dist) continue;
1479
1531
  const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1480
1532
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
@@ -1486,9 +1538,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1486
1538
  const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1487
1539
  const freeze = externalMarketplace ? externalFreezes.get(`${unit.id} ${dist.type}`) : null;
1488
1540
  // Normalized marketplace form: explicit mutually exclusive declaration.
1489
- const marketplaceForm = requiresMarketplace
1490
- ? (externalMarketplace ? 'standalone-index' : 'bundled-family')
1491
- : null;
1541
+ // All four platforms carry marketplaceForm and sourceDescriptor.
1542
+ const marketplaceSourceType = dist.marketplaceSourceType ?? (externalMarketplace ? 'standalone-index' : 'bundled-family');
1543
+ const marketplaceForm = marketplaceSourceType;
1492
1544
  const sourceDescriptor = marketplaceForm === 'standalone-index'
1493
1545
  ? Object.freeze({
1494
1546
  form: 'standalone-index',
@@ -1543,6 +1595,18 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1543
1595
  // bundled-family: sourceDescriptor.commit 绑定到此值。
1544
1596
  // standalone-index: 通过此值绑定插件载荷来源。
1545
1597
  sourceCommit: asset.commit,
1598
+ // 安装契约摘要、算法版本和来源类型,用于完整性交叉校验
1599
+ ...(dist.installationContractDigest ? {
1600
+ installationContractDigest: dist.installationContractDigest,
1601
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1602
+ marketplaceSourceType: dist.marketplaceSourceType,
1603
+ } : {}),
1604
+ // standalone-index 审计字段:供后续静态预检使用
1605
+ ...(externalMarketplace && freeze ? {
1606
+ marketplaceIndexPath: freeze.marketplaceIndexPath,
1607
+ marketplaceName: freeze.marketplaceName,
1608
+ selectedEntry: freeze.selectedEntry,
1609
+ } : {}),
1546
1610
  },
1547
1611
  expected: {
1548
1612
  installed: true,
@@ -2175,9 +2239,365 @@ export async function prepareRelease(options) {
2175
2239
  )
2176
2240
  : null;
2177
2241
 
2178
- const units = unitResults.map(({ unit, manifest }, idx) => {
2242
+ // --- Step 7b: Skill resource closure gate ---
2243
+ // Production snapshots are sealed inside buildProductionAssets. Scan only
2244
+ // after that transition so the receipt binds the exact byte/mode identity
2245
+ // publish will re-verify, rather than the writable pre-freeze staging tree.
2246
+ // Non-production plans scan the final staging tree at the same point.
2247
+ // This gate is built in, read-only, and cannot be disabled by overlays.
2248
+ const skillResourceClosureResults = [];
2249
+ for (const { unit, manifest } of unitResults) {
2250
+ await evidence.append({
2251
+ phase: 'skill-resource-closure',
2252
+ status: 'started',
2253
+ unitId: unit.id,
2254
+ });
2255
+
2256
+ const closureResult = await checkSkillResourceClosure({
2257
+ snapshotDir: manifest.outputDir,
2258
+ host: 'root',
2259
+ });
2260
+
2261
+ const receipt = createSkillResourceClosureReceipt(closureResult, { unitId: unit.id });
2262
+ skillResourceClosureResults.push(receipt);
2263
+
2264
+ if (closureResult.findings.length > 0) {
2265
+ await evidence.append({
2266
+ phase: 'skill-resource-closure',
2267
+ status: 'blocking',
2268
+ unitId: unit.id,
2269
+ findingCount: closureResult.findings.length,
2270
+ findings: closureResult.findings.map((f) => ({
2271
+ skill: f.skill,
2272
+ line: f.line,
2273
+ reference: f.reference,
2274
+ classification: f.classification,
2275
+ code: f.code,
2276
+ })),
2277
+ });
2278
+ throw new ReleaseError(
2279
+ GATE_FAILED,
2280
+ `skill resource closure gate failed for unit "${unit.id}": ${closureResult.findings.length} finding(s)`,
2281
+ {
2282
+ unitId: unit.id,
2283
+ findingCount: closureResult.findings.length,
2284
+ findings: closureResult.findings,
2285
+ },
2286
+ );
2287
+ }
2288
+
2289
+ await evidence.append({
2290
+ phase: 'skill-resource-closure',
2291
+ status: 'completed',
2292
+ unitId: unit.id,
2293
+ checkerVersion: closureResult.checkerVersion,
2294
+ surfaceCount: receipt.surfaceCount,
2295
+ skillCount: receipt.skillCount,
2296
+ referenceCount: closureResult.referenceCount,
2297
+ sourceOnlyCount: closureResult.sourceOnlyCount,
2298
+ findingCount: 0,
2299
+ receiptDigest: closureResult.receiptDigest,
2300
+ });
2301
+ }
2302
+
2303
+ // Freeze external independent marketplace HEADs (production + online only):
2304
+ // for each claude/codex/codebuddy distribution declaring marketplaceRepo,
2305
+ // resolve the external repo's HEAD sha + default branch and validate the
2306
+ // marketplace index entry at that sha before freezing the add-ref. This
2307
+ // MUST happen before installation contract construction so that standalone-index
2308
+ // contracts can use the frozen external entries. Offline production with a
2309
+ // declared marketplaceRepo fails closed inside the resolver. The remote is
2310
+ // only ever read (git ls-remote / gh api), never written.
2311
+ const externalMarketplaceFreezes = production
2312
+ ? await resolveExternalMarketplaceFreezes({
2313
+ unitResults,
2314
+ resolvedVersions,
2315
+ offline,
2316
+ evidence,
2317
+ observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
2318
+ fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
2319
+ })
2320
+ : new Map();
2321
+
2322
+ // 为每个分发渠道校验 marketplaceSourceType 并冻结安装契约。
2323
+ // 对每个插件 distribution:
2324
+ // 1. 从 unitResults[].manifest.outputDir 的真实快照读取 manifest
2325
+ // 2. manifest 读取:若平台 strategy.readManifest 存在则调用它,否则读 platform.manifestPaths.plugin
2326
+ // 3. 确定 includeMarketplaceEntry:
2327
+ // - bundled-family: Kimi=false, Claude/Codex/CodeBuddy=true(从快照读取)
2328
+ // - standalone-index: Kimi=false, Claude/Codex/CodeBuddy=true(使用外部冻结条目)
2329
+ // 4. 需要条目时:
2330
+ // - bundled-family: 从 dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace 读取 bundled 索引
2331
+ // - standalone-index: 从 externalMarketplaceFreezes 获取冻结的 selectedEntry 和 marketplaceIndexPath
2332
+ // 5. 使用 buildInstallationContract 构建完整可审计契约对象
2333
+ // 6. 在 distribution 中冻结 marketplaceSourceType / installationContract / installationContractDigest
2334
+ // 以及独立市场审计字段
2335
+ const units = await Promise.all(unitResults.map(async ({ unit, manifest }, idx) => {
2179
2336
  const unitVersion = resolvedVersions[idx];
2180
2337
  const unitBaseline = unitBaselineResults.get(unit.id);
2338
+ const snapshotDir = manifest.outputDir;
2339
+
2340
+ const distributionsWithSource = await Promise.all((unit.distributions ?? []).map(async (dist) => {
2341
+ const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
2342
+ if (!platform) return dist;
2343
+
2344
+ // 校验 marketplaceSourceType:从配置读取,不允许硬编码默认值
2345
+ const sourceTypeResult = validateMarketplaceSourceSelection(
2346
+ platform.id,
2347
+ dist, // config
2348
+ dist, // plan (same source at prepare time)
2349
+ );
2350
+ if (!sourceTypeResult.valid) {
2351
+ throw new ReleaseError(
2352
+ CONFIG_INVALID,
2353
+ `unit "${unit.id}" ${dist.type} marketplace source validation failed: ${sourceTypeResult.error}`,
2354
+ { unitId: unit.id, distributionType: dist.type },
2355
+ );
2356
+ }
2357
+
2358
+ // 仅插件 distribution 需要安装契约
2359
+ if (dist.type === 'npm') {
2360
+ return dist;
2361
+ }
2362
+
2363
+ // 1. 确定 marketplaceSourceType(防御性归一化:旧配置可能仍缺少字段)
2364
+ let marketplaceSourceType = sourceTypeResult.selectedSource;
2365
+ if (!marketplaceSourceType) {
2366
+ marketplaceSourceType = dist.marketplaceRepo ? 'standalone-index' : 'bundled-family';
2367
+ }
2368
+
2369
+ // 2. 确定 includeMarketplaceEntry 和 selectedMarketplaceEntry
2370
+ // bundled-family: 从快照中的 bundled 索引读取唯一选中条目。
2371
+ // standalone-index: 从 externalMarketplaceFreezes 获取冻结的 selectedEntry 和 marketplaceIndexPath。
2372
+ // Kimi 不纳入市场条目(platform.manifestPaths.marketplace === null 且无显式路径);
2373
+ // Claude/Codex/CodeBuddy 使用默认或显式路径。
2374
+ const hasDefaultMarketplace = platform.manifestPaths.marketplace !== null;
2375
+ const hasExplicitMarketplacePath = dist.marketplaceIndexPath != null;
2376
+ const isBundledFamily = marketplaceSourceType === 'bundled-family';
2377
+ const isStandaloneIndex = marketplaceSourceType === 'standalone-index';
2378
+
2379
+ // includeMarketplaceEntry 代表"契约实际包含一条市场条目",
2380
+ // 不能只代表平台理论上支持市场条目。
2381
+ // bundled-family: 平台支持市场时即包含(从快照读取)
2382
+ // standalone-index: 只有拿到冻结条目且非 Kimi 时才包含
2383
+ // Kimi 不纳入市场条目:Kimi 无市场 CLI,selectedEntry 仅供静态校验。
2384
+ let includeMarketplaceEntry;
2385
+ if (isBundledFamily) {
2386
+ includeMarketplaceEntry = hasDefaultMarketplace || hasExplicitMarketplacePath;
2387
+ } else if (isStandaloneIndex) {
2388
+ if (platform.id === 'kimi') {
2389
+ // Kimi standalone: 安装契约不纳入市场条目
2390
+ includeMarketplaceEntry = false;
2391
+ } else {
2392
+ const freezeKey = `${unit.id} ${dist.type}`;
2393
+ const freeze = externalMarketplaceFreezes.get(freezeKey);
2394
+ if (freeze) {
2395
+ includeMarketplaceEntry = true;
2396
+ } else {
2397
+ // 生产在线的独立市场缺冻结结果必须失败关闭
2398
+ if (production && !offline) {
2399
+ throw new ReleaseError(
2400
+ GATE_FAILED,
2401
+ `unit "${unit.id}" ${dist.type} standalone-index requires external marketplace freeze but no freeze result found`,
2402
+ { unitId: unit.id, distributionType: dist.type },
2403
+ );
2404
+ }
2405
+ // 非生产/离线没有冻结结果时,契约仍记录来源形态,但不包含空条目
2406
+ includeMarketplaceEntry = false;
2407
+ }
2408
+ }
2409
+ } else {
2410
+ includeMarketplaceEntry = false;
2411
+ }
2412
+
2413
+ // 3. 需要条目时,根据来源类型获取
2414
+ let selectedMarketplaceEntry = null;
2415
+ let marketplaceIndexRelative = null;
2416
+ let bundledMarketIndex = null;
2417
+ if (includeMarketplaceEntry) {
2418
+ if (isStandaloneIndex) {
2419
+ // standalone-index: 从外部冻结结果获取条目和路径(freeze 已确认存在)
2420
+ const freezeKey = `${unit.id} ${dist.type}`;
2421
+ const freeze = externalMarketplaceFreezes.get(freezeKey);
2422
+ selectedMarketplaceEntry = freeze.selectedEntry;
2423
+ marketplaceIndexRelative = freeze.marketplaceIndexPath;
2424
+ } else {
2425
+ // bundled-family: 从快照中的 bundled 索引读取
2426
+ marketplaceIndexRelative = dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
2427
+ if (!marketplaceIndexRelative) {
2428
+ throw new ReleaseError(
2429
+ GATE_FAILED,
2430
+ `unit "${unit.id}" ${dist.type} cannot determine marketplace index path: neither marketplaceIndexPath nor platform.manifestPaths.marketplace is set`,
2431
+ { unitId: unit.id, distributionType: dist.type },
2432
+ );
2433
+ }
2434
+ const marketplaceIndexPath = resolve(snapshotDir, marketplaceIndexRelative);
2435
+ let marketplaceIndexRaw;
2436
+ try {
2437
+ marketplaceIndexRaw = await readFile(marketplaceIndexPath, 'utf8');
2438
+ } catch (err) {
2439
+ throw new ReleaseError(
2440
+ GATE_FAILED,
2441
+ `unit "${unit.id}" ${dist.type} cannot read bundled marketplace index "${marketplaceIndexRelative}": ${err.message}`,
2442
+ { unitId: unit.id, distributionType: dist.type, cause: err.code },
2443
+ );
2444
+ }
2445
+ let marketplaceIndex;
2446
+ try {
2447
+ marketplaceIndex = JSON.parse(marketplaceIndexRaw);
2448
+ } catch (err) {
2449
+ throw new ReleaseError(
2450
+ GATE_FAILED,
2451
+ `unit "${unit.id}" ${dist.type} invalid JSON in bundled marketplace index "${marketplaceIndexRelative}": ${err.message}`,
2452
+ { unitId: unit.id, distributionType: dist.type },
2453
+ );
2454
+ }
2455
+ if (!marketplaceIndex || typeof marketplaceIndex !== 'object' || !Array.isArray(marketplaceIndex.plugins)) {
2456
+ throw new ReleaseError(
2457
+ GATE_FAILED,
2458
+ `unit "${unit.id}" ${dist.type} bundled marketplace index "${marketplaceIndexRelative}" must be an object with a plugins array`,
2459
+ { unitId: unit.id, distributionType: dist.type },
2460
+ );
2461
+ }
2462
+ const matchingEntries = marketplaceIndex.plugins.filter(
2463
+ (entry) => entry && entry.name === dist.plugin,
2464
+ );
2465
+ if (matchingEntries.length !== 1) {
2466
+ throw new ReleaseError(
2467
+ GATE_FAILED,
2468
+ `unit "${unit.id}" ${dist.type} bundled marketplace index must contain exactly one plugin entry named "${dist.plugin}", found ${matchingEntries.length}`,
2469
+ { unitId: unit.id, distributionType: dist.type },
2470
+ );
2471
+ }
2472
+ // 传完整解析条目给 buildInstallationContract,不得只取名字
2473
+ selectedMarketplaceEntry = matchingEntries[0];
2474
+ bundledMarketIndex = marketplaceIndex;
2475
+ }
2476
+ }
2477
+
2478
+ // 4. 从真实快照读取插件 manifest
2479
+ // bundled-family + 有市场索引:使用 resolvePluginManifestFromMarketplaceEntrySource
2480
+ // 从条目 source 安全解析插件根并读取 manifest(支持子目录布局)。
2481
+ // 其他路径:保留原有策略。
2482
+ let pluginManifestRelative;
2483
+ let pluginManifestParsed;
2484
+ if (isBundledFamily && bundledMarketIndex && platform.marketplaceSourceForm !== null) {
2485
+ // bundled-family 有市场索引且平台支持市场来源解析(Claude/Codex):
2486
+ // 通过条目 source 路径解析 manifest。
2487
+ // 计算市场根:从 marketplaceIndexRelative 推断(精确后缀匹配)。
2488
+ const mktRoot = resolveMarketplaceRoot(platform, marketplaceIndexRelative);
2489
+ try {
2490
+ const resolved = await resolvePluginManifestFromMarketplaceEntrySource(
2491
+ bundledMarketIndex, dist.plugin, platform, snapshotDir, mktRoot,
2492
+ );
2493
+ pluginManifestParsed = resolved.manifest;
2494
+ pluginManifestRelative = resolved.manifestRelativePath;
2495
+ } catch (err) {
2496
+ throw new ReleaseError(
2497
+ GATE_FAILED,
2498
+ `unit "${unit.id}" ${dist.type} cannot resolve plugin manifest from marketplace entry source: ${err.message}`,
2499
+ { unitId: unit.id, distributionType: dist.type },
2500
+ );
2501
+ }
2502
+ } else if (platform.strategy.readManifest) {
2503
+ // Kimi/Codex/CodeBuddy 有自定义 manifest 读取策略
2504
+ const readResult = await platform.strategy.readManifest(snapshotDir);
2505
+ pluginManifestParsed = readResult.manifest;
2506
+ pluginManifestRelative = readResult.manifestRelative ?? platform.manifestPaths.plugin;
2507
+ } else {
2508
+ // Claude fallback(standalone-index 或无市场索引时)
2509
+ pluginManifestRelative = platform.manifestPaths.plugin;
2510
+ const pluginManifestPath = resolve(snapshotDir, pluginManifestRelative);
2511
+ let raw;
2512
+ try {
2513
+ raw = await readFile(pluginManifestPath, 'utf8');
2514
+ } catch (err) {
2515
+ throw new ReleaseError(
2516
+ GATE_FAILED,
2517
+ `unit "${unit.id}" ${dist.type} cannot read plugin manifest "${pluginManifestRelative}": ${err.message}`,
2518
+ { unitId: unit.id, distributionType: dist.type, cause: err.code },
2519
+ );
2520
+ }
2521
+ try {
2522
+ pluginManifestParsed = JSON.parse(raw);
2523
+ } catch (err) {
2524
+ throw new ReleaseError(
2525
+ GATE_FAILED,
2526
+ `unit "${unit.id}" ${dist.type} invalid JSON in plugin manifest "${pluginManifestRelative}": ${err.message}`,
2527
+ { unitId: unit.id, distributionType: dist.type },
2528
+ );
2529
+ }
2530
+ }
2531
+
2532
+ // 静态校验 manifest 名称和版本(被摘要剔除不等于不校验)
2533
+ if (pluginManifestParsed.name !== dist.plugin) {
2534
+ throw new ReleaseError(
2535
+ GATE_FAILED,
2536
+ `unit "${unit.id}" ${dist.type} plugin manifest name "${pluginManifestParsed.name}" does not match distribution plugin "${dist.plugin}"`,
2537
+ { unitId: unit.id, distributionType: dist.type },
2538
+ );
2539
+ }
2540
+ if (typeof pluginManifestParsed.version === 'string' && pluginManifestParsed.version !== unitVersion) {
2541
+ throw new ReleaseError(
2542
+ GATE_FAILED,
2543
+ `unit "${unit.id}" ${dist.type} plugin manifest version "${pluginManifestParsed.version}" does not match target version "${unitVersion}"`,
2544
+ { unitId: unit.id, distributionType: dist.type },
2545
+ );
2546
+ }
2547
+
2548
+ // 5. 使用 buildInstallationContract 构建完整可审计契约对象
2549
+ const installationContract = buildInstallationContract({
2550
+ distributionType: dist.type,
2551
+ manifestRelativePath: pluginManifestRelative,
2552
+ manifest: pluginManifestParsed,
2553
+ marketplaceSourceType,
2554
+ includeMarketplaceEntry,
2555
+ ...(includeMarketplaceEntry ? {
2556
+ marketplaceIndexRelativePath: marketplaceIndexRelative,
2557
+ selectedMarketplaceEntry,
2558
+ } : {}),
2559
+ verificationRecipeVersion: CONSUMER_INSTALL_RECIPE_VERSION,
2560
+ });
2561
+
2562
+ // 6. 计算摘要(使用权威算法入口,保证契约对象与摘要一致)
2563
+ const installationContractDigest = computeInstallationContractDigest({
2564
+ distributionType: dist.type,
2565
+ manifestRelativePath: pluginManifestRelative,
2566
+ manifest: pluginManifestParsed,
2567
+ marketplaceSourceType,
2568
+ includeMarketplaceEntry,
2569
+ ...(includeMarketplaceEntry ? {
2570
+ marketplaceIndexRelativePath: marketplaceIndexRelative,
2571
+ selectedMarketplaceEntry,
2572
+ } : {}),
2573
+ verificationRecipeVersion: CONSUMER_INSTALL_RECIPE_VERSION,
2574
+ });
2575
+
2576
+ // 7. 构建返回对象,包含冻结的审计字段
2577
+ const frozenDist = {
2578
+ ...dist,
2579
+ marketplaceSourceType,
2580
+ installationContract,
2581
+ installationContractDigest,
2582
+ };
2583
+
2584
+ // standalone-index 审计字段:来自外部冻结结果
2585
+ if (isStandaloneIndex) {
2586
+ const freezeKey = `${unit.id} ${dist.type}`;
2587
+ const freeze = externalMarketplaceFreezes.get(freezeKey);
2588
+ if (freeze) {
2589
+ frozenDist.marketplaceRepo = freeze.marketplaceRepo;
2590
+ frozenDist.marketplaceCommitSha = freeze.marketplaceCommitSha;
2591
+ frozenDist.marketplaceRef = freeze.marketplaceRef;
2592
+ frozenDist.marketplaceIndexPath = freeze.marketplaceIndexPath;
2593
+ frozenDist.marketplaceName = freeze.marketplaceName;
2594
+ frozenDist.selectedEntry = freeze.selectedEntry;
2595
+ }
2596
+ }
2597
+
2598
+ return frozenDist;
2599
+ }));
2600
+
2181
2601
  return {
2182
2602
  id: unit.id,
2183
2603
  targetVersion: unitVersion,
@@ -2202,29 +2622,15 @@ export async function prepareRelease(options) {
2202
2622
  npm: productionAssets[idx].npm,
2203
2623
  },
2204
2624
  } : {}),
2205
- distributions: unit.distributions,
2625
+ distributions: distributionsWithSource,
2206
2626
  ...(unitBaseline ? { previousPublicBaseline: unitBaseline } : {}),
2207
2627
  };
2208
- });
2628
+ }));
2209
2629
 
2210
- // Freeze external independent marketplace HEADs (production + online only):
2211
- // for each claude/codex distribution declaring marketplaceRepo, resolve the
2212
- // external repo's HEAD sha + default branch and validate the marketplace
2213
- // index entry at that sha before freezing the add-ref. Offline production
2214
- // with a declared marketplaceRepo fails closed inside the resolver. The
2215
- // remote is only ever read (git ls-remote / gh api), never written.
2216
- const externalMarketplaceFreezes = production
2217
- ? await resolveExternalMarketplaceFreezes({
2218
- unitResults,
2219
- resolvedVersions,
2220
- offline,
2221
- evidence,
2222
- observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
2223
- fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
2224
- })
2225
- : new Map();
2630
+ // 构建冻结分发映射:unitId -> frozen distributions
2631
+ const frozenDistributionsMap = new Map(units.map((u) => [u.id, u.distributions]));
2226
2632
 
2227
- const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes);
2633
+ const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes, frozenDistributionsMap);
2228
2634
 
2229
2635
  // Compute overall snapshot digest
2230
2636
  const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
@@ -2241,6 +2647,14 @@ export async function prepareRelease(options) {
2241
2647
  capturedAt: baseline.capturedAt,
2242
2648
  },
2243
2649
  configDigest,
2650
+ skillResourceClosure: {
2651
+ checkerVersion: SKILL_RESOURCE_CHECKER_VERSION,
2652
+ unitReceipts: skillResourceClosureResults,
2653
+ totalSkillCount: skillResourceClosureResults.reduce((sum, item) => sum + item.skillCount, 0),
2654
+ totalReferenceCount: skillResourceClosureResults.reduce((sum, item) => sum + item.referenceCount, 0),
2655
+ totalSourceOnlyCount: skillResourceClosureResults.reduce((sum, item) => sum + item.sourceOnlyCount, 0),
2656
+ totalFindingCount: 0,
2657
+ },
2244
2658
  verificationGates: config.verificationGates ?? [],
2245
2659
  snapshotDigest: overallSnapshotDigest,
2246
2660
  ...(production ? {