release-skill 0.2.2 → 0.2.4

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 (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +4 -2
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +41 -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 +265 -916
  11. package/README.zh-CN.md +222 -537
  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 +18710 -16694
  15. package/adapters/claude/schemas/release-plan.schema.json +137 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +70 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +18710 -16694
  20. package/adapters/codex/schemas/release-plan.schema.json +137 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +70 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +18710 -16694
  25. package/adapters/kimi/schemas/release-plan.schema.json +137 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +70 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +18710 -16694
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
  33. package/bin/release-skill.bundle.mjs +18710 -16694
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +137 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +70 -2
  38. package/src/adapters/plugin-marketplace.mjs +1602 -605
  39. package/src/commands/prepare.mjs +441 -32
  40. package/src/commands/publish.mjs +107 -75
  41. package/src/commands/reconcile.mjs +92 -327
  42. package/src/commands/setup.mjs +148 -20
  43. package/src/commands/verify.mjs +315 -25
  44. package/src/core/baseline.mjs +21 -1
  45. package/src/core/checkpoints.mjs +50 -7
  46. package/src/core/config.mjs +15 -0
  47. package/src/core/errors.mjs +2 -0
  48. package/src/core/installation-contract.mjs +341 -0
  49. package/src/core/plan.mjs +307 -6
  50. package/src/platforms/codebuddy.mjs +191 -238
  51. package/src/platforms/codex.mjs +369 -0
  52. package/src/platforms/kimi.mjs +164 -119
  53. package/src/platforms/registry.mjs +180 -4
  54. package/src/producers/build-adapters.mjs +9 -2
@@ -50,6 +50,15 @@ import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } fro
50
50
  import { verifyFrozenNpmTarballIdentity } from '../adapters/npm.mjs';
51
51
  import { createProductionPrepareRunDir } from '../core/run.mjs';
52
52
  import { PLATFORMS } from '../platforms/registry.mjs';
53
+ import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
54
+ import { buildInstallationContract, computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from '../core/installation-contract.mjs';
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // 安装契约常量
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /** 消费端安装验证配方版本。算法变更时递增。 */
61
+ const CONSUMER_INSTALL_RECIPE_VERSION = 'consumer-install-v1';
53
62
 
54
63
  // ---------------------------------------------------------------------------
55
64
  // Version resolution
@@ -1085,12 +1094,15 @@ export async function resolveExternalMarketplaceFreezes({
1085
1094
  const version = resolvedVersions[index];
1086
1095
  const githubHost = unit.production?.githubHost ?? 'github.com';
1087
1096
  for (const dist of unit.distributions ?? []) {
1088
- if (dist.marketplaceRepo === undefined || dist.marketplaceRepo === null) continue;
1097
+ // 只处理 standalone-index 来源;bundled-family 不需要外部冻结。
1098
+ if (dist.marketplaceSourceType !== 'standalone-index') continue;
1099
+ // standalone-index 必须有 marketplaceRepo;没有则跳过(兼容旧配置)。
1100
+ if (!dist.marketplaceRepo) continue;
1089
1101
  const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
1090
- if (!platform || platform.marketplaceRefForm === null) {
1102
+ if (!platform) {
1091
1103
  throw new ReleaseError(
1092
1104
  GATE_FAILED,
1093
- `unit "${unit.id}" ${dist.type} distribution declares marketplaceRepo but the platform has no marketplace add capability`,
1105
+ `unit "${unit.id}" ${dist.type} distribution declares standalone-index but the platform is unknown`,
1094
1106
  { unitId: unit.id, distributionType: dist.type },
1095
1107
  );
1096
1108
  }
@@ -1098,7 +1110,7 @@ export async function resolveExternalMarketplaceFreezes({
1098
1110
  throw new ReleaseError(
1099
1111
  GATE_FAILED,
1100
1112
  `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 },
1113
+ { unitId: unit.id, marketplaceSourceType: dist.marketplaceSourceType },
1102
1114
  );
1103
1115
  }
1104
1116
  const observed = await observeHeadFn(dist.marketplaceRepo, { githubHost });
@@ -1110,7 +1122,16 @@ export async function resolveExternalMarketplaceFreezes({
1110
1122
  );
1111
1123
  }
1112
1124
  const sha = observed.sha;
1113
- const manifestPath = platform.manifestPaths.marketplace;
1125
+ // 索引路径:distribution 显式声明优先,否则使用平台默认路径。
1126
+ // 平台注册表没有默认路径时(kimi、codebuddy),必须显式提供。
1127
+ const manifestPath = dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
1128
+ if (!manifestPath) {
1129
+ throw new ReleaseError(
1130
+ GATE_FAILED,
1131
+ `unit "${unit.id}" ${dist.type} cannot determine marketplace index path: neither marketplaceIndexPath nor platform.manifestPaths.marketplace is set`,
1132
+ { unitId: unit.id, distributionType: dist.type },
1133
+ );
1134
+ }
1114
1135
  const fetched = await fetchIndexFn(dist.marketplaceRepo, manifestPath, sha, { githubHost });
1115
1136
  if (fetched.status !== 'fetched' || !fetched.index || typeof fetched.index !== 'object') {
1116
1137
  throw new ReleaseError(
@@ -1120,7 +1141,8 @@ export async function resolveExternalMarketplaceFreezes({
1120
1141
  );
1121
1142
  }
1122
1143
  const marketplaceIndex = fetched.index;
1123
- if (marketplaceIndex.name !== dist.marketplace) {
1144
+ // 校验市场名称(仅当 distribution 显式声明 marketplace 时)
1145
+ if (dist.marketplace && marketplaceIndex.name !== dist.marketplace) {
1124
1146
  throw new ReleaseError(
1125
1147
  GATE_FAILED,
1126
1148
  `unit "${unit.id}" external marketplace index name "${marketplaceIndex.name}" does not match distribution marketplace "${dist.marketplace}"`,
@@ -1144,12 +1166,21 @@ export async function resolveExternalMarketplaceFreezes({
1144
1166
  { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1145
1167
  );
1146
1168
  }
1147
- const ref = platform.marketplaceRefForm === 'sha' ? sha : observed.defaultBranch;
1169
+ // marketplaceRef:Claude 使用默认分支名(name-ref),其余平台使用提交 SHA。
1170
+ // 无 CLI 的平台(kimi、codebuddy)也必须能冻结,ref 用 SHA。
1171
+ const marketplaceRef = platform.marketplaceRefForm === 'name' ? observed.defaultBranch : sha;
1148
1172
  freezes.set(`${unit.id} ${dist.type}`, {
1173
+ // 向后兼容字段(buildExternalActions 使用 repo / ref / marketplace)
1149
1174
  repo: dist.marketplaceRepo,
1150
- ref,
1175
+ ref: marketplaceRef,
1151
1176
  marketplaceCommitSha: sha,
1152
1177
  marketplace: dist.marketplace,
1178
+ // B3B 完整冻结字段
1179
+ marketplaceRepo: dist.marketplaceRepo,
1180
+ marketplaceRef,
1181
+ marketplaceIndexPath: manifestPath,
1182
+ marketplaceName: marketplaceIndex.name,
1183
+ selectedEntry: pluginEntries[0],
1153
1184
  });
1154
1185
  await evidence.append({
1155
1186
  phase: 'external-marketplace-freeze',
@@ -1158,8 +1189,11 @@ export async function resolveExternalMarketplaceFreezes({
1158
1189
  status: 'completed',
1159
1190
  marketplaceRepo: dist.marketplaceRepo,
1160
1191
  marketplaceCommitSha: sha,
1192
+ marketplaceRef,
1193
+ marketplaceIndexPath: manifestPath,
1194
+ marketplaceName: marketplaceIndex.name,
1195
+ selectedEntry: pluginEntries[0],
1161
1196
  defaultBranch: observed.defaultBranch,
1162
- addRef: ref,
1163
1197
  });
1164
1198
  }
1165
1199
  }
@@ -1181,7 +1215,7 @@ export async function resolveExternalMarketplaceFreezes({
1181
1215
  * @param {string} realRoot - The project root for relative path calculation.
1182
1216
  * @returns {object[]} Array of external action descriptors.
1183
1217
  */
1184
- export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map()) {
1218
+ export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map(), frozenDistributions = null) {
1185
1219
  const actions = [];
1186
1220
 
1187
1221
  if (!productionAssets) {
@@ -1246,8 +1280,11 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1246
1280
  // carry: Kimi Code has no non-interactive install/marketplace API, so
1247
1281
  // the kimi action carries no marketplace identity (MINOR-1); plugin +
1248
1282
  // entrySkill are the meaningful identity fields there).
1283
+ const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
1249
1284
  for (const platform of PLATFORMS) {
1250
- const dist = (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1285
+ const dist = frozenUnitDists
1286
+ ? frozenUnitDists.find((d) => d.type === platform.distributionType)
1287
+ : (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1251
1288
  if (!dist) continue;
1252
1289
  const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1253
1290
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
@@ -1258,6 +1295,38 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1258
1295
  // but no frozen ref/marketplaceCommitSha (production-only bindings),
1259
1296
  // keeping the two loops' shapes aligned for plan completeness.
1260
1297
  const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1298
+ // Normalized marketplace form: explicit mutually exclusive declaration.
1299
+ // bundled-family: marketplace and plugin live in the same repo.
1300
+ // standalone-index: external marketplace repo indexes a separate plugin repo.
1301
+ // All four platforms carry marketplaceForm and sourceDescriptor when a
1302
+ // marketplace source type is declared (bundled-family or standalone-index).
1303
+ const marketplaceSourceType = dist.marketplaceSourceType ?? (externalMarketplace ? 'standalone-index' : 'bundled-family');
1304
+ const marketplaceForm = marketplaceSourceType;
1305
+ const sourceDescriptor = marketplaceForm === 'standalone-index'
1306
+ ? Object.freeze({
1307
+ form: 'standalone-index',
1308
+ marketplaceRepo: dist.marketplaceRepo,
1309
+ marketplaceEntry: dist.plugin,
1310
+ // pluginRepo is the plugin's own public repo, NOT the external
1311
+ // marketplace repo. The marketplace repo contains the index; the
1312
+ // plugin repo contains the actual plugin code.
1313
+ pluginRepo: unit.publicRepo,
1314
+ sourceType: 'marketplace-entry',
1315
+ // Production-only fields: marketplaceCommitSha and ref are frozen
1316
+ // by resolveExternalMarketplaceFreezes in the production path.
1317
+ marketplaceCommitSha: null,
1318
+ ref: null,
1319
+ payloadDigest: null,
1320
+ })
1321
+ : marketplaceForm === 'bundled-family'
1322
+ ? Object.freeze({
1323
+ form: 'bundled-family',
1324
+ repo: unit.publicRepo,
1325
+ marketplaceEntry: dist.plugin,
1326
+ pluginSubpath: '.',
1327
+ payloadDigest: null,
1328
+ })
1329
+ : null;
1261
1330
  actions.push({
1262
1331
  id: `${platform.actionType}-${unit.id}`,
1263
1332
  type: platform.actionType,
@@ -1279,6 +1348,16 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1279
1348
  // contract (whole-tree '.' containment; see plugin-marketplace).
1280
1349
  payloadContract: externalMarketplace ? 'external-marketplace-v1' : 'declared-manifest-v1',
1281
1350
  ...(externalMarketplace ? { marketplaceLocation: 'external' } : {}),
1351
+ ...(marketplaceForm ? { marketplaceForm } : {}),
1352
+ ...(sourceDescriptor ? { sourceDescriptor } : {}),
1353
+ // 安装契约摘要、算法版本和来源类型,用于完整性交叉校验
1354
+ ...(dist.installationContractDigest ? {
1355
+ installationContractDigest: dist.installationContractDigest,
1356
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1357
+ marketplaceSourceType: dist.marketplaceSourceType,
1358
+ } : {}),
1359
+ // standalone-index 审计字段仅在生产在线冻结成功后出现;
1360
+ // 非生产 action 不携带这三个字段(未冻结时无真实值可用)。
1282
1361
  },
1283
1362
  expected: {
1284
1363
  installed: true,
@@ -1301,6 +1380,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1301
1380
  const asset = productionAssets[index];
1302
1381
  const tagTemplate = unit.version?.tagTemplate ?? `${unit.id}-v{version}`;
1303
1382
  const resolvedTag = asset.tag;
1383
+ const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
1304
1384
 
1305
1385
  // Push snapshot
1306
1386
  actions.push({
@@ -1439,7 +1519,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1439
1519
  // entrySkillFound/manifestDigest expected). Marketplace identity follows
1440
1520
  // the registry's schema required fields — kimi carries none (MINOR-1).
1441
1521
  for (const platform of PLATFORMS) {
1442
- const dist = (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1522
+ const dist = frozenUnitDists
1523
+ ? frozenUnitDists.find((d) => d.type === platform.distributionType)
1524
+ : (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1443
1525
  if (!dist) continue;
1444
1526
  const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1445
1527
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
@@ -1450,6 +1532,34 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1450
1532
  // snapshot, unchanged. Inline form (no marketplaceRepo) is byte-identical.
1451
1533
  const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1452
1534
  const freeze = externalMarketplace ? externalFreezes.get(`${unit.id} ${dist.type}`) : null;
1535
+ // Normalized marketplace form: explicit mutually exclusive declaration.
1536
+ // All four platforms carry marketplaceForm and sourceDescriptor.
1537
+ const marketplaceSourceType = dist.marketplaceSourceType ?? (externalMarketplace ? 'standalone-index' : 'bundled-family');
1538
+ const marketplaceForm = marketplaceSourceType;
1539
+ const sourceDescriptor = marketplaceForm === 'standalone-index'
1540
+ ? Object.freeze({
1541
+ form: 'standalone-index',
1542
+ marketplaceRepo: dist.marketplaceRepo,
1543
+ marketplaceCommitSha: freeze?.marketplaceCommitSha ?? null,
1544
+ marketplaceEntry: dist.plugin,
1545
+ // pluginRepo is the plugin's own public repo, NOT the external
1546
+ // marketplace repo. The marketplace repo contains the index; the
1547
+ // plugin repo contains the actual plugin code.
1548
+ pluginRepo: unit.publicRepo,
1549
+ sourceType: 'marketplace-entry',
1550
+ ref: freeze?.ref ?? null,
1551
+ payloadDigest: asset.manifestDigest,
1552
+ })
1553
+ : marketplaceForm === 'bundled-family'
1554
+ ? Object.freeze({
1555
+ form: 'bundled-family',
1556
+ repo: unit.publicRepo,
1557
+ commit: asset.commit,
1558
+ marketplaceEntry: dist.plugin,
1559
+ pluginSubpath: '.',
1560
+ payloadDigest: asset.manifestDigest,
1561
+ })
1562
+ : null;
1453
1563
  actions.push({
1454
1564
  id: `${platform.actionType}-${unit.id}`,
1455
1565
  type: platform.actionType,
@@ -1474,6 +1584,24 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1474
1584
  // contract (whole-tree '.' containment; see plugin-marketplace).
1475
1585
  payloadContract: externalMarketplace ? 'external-marketplace-v1' : 'declared-manifest-v1',
1476
1586
  ...(externalMarketplace ? { marketplaceLocation: 'external', marketplaceCommitSha: freeze.marketplaceCommitSha } : {}),
1587
+ ...(marketplaceForm ? { marketplaceForm } : {}),
1588
+ ...(sourceDescriptor ? { sourceDescriptor } : {}),
1589
+ // 冻结的插件来源提交,用于 sourceDescriptor.commit 交叉校验。
1590
+ // bundled-family: sourceDescriptor.commit 绑定到此值。
1591
+ // standalone-index: 通过此值绑定插件载荷来源。
1592
+ sourceCommit: asset.commit,
1593
+ // 安装契约摘要、算法版本和来源类型,用于完整性交叉校验
1594
+ ...(dist.installationContractDigest ? {
1595
+ installationContractDigest: dist.installationContractDigest,
1596
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1597
+ marketplaceSourceType: dist.marketplaceSourceType,
1598
+ } : {}),
1599
+ // standalone-index 审计字段:供后续静态预检使用
1600
+ ...(externalMarketplace && freeze ? {
1601
+ marketplaceIndexPath: freeze.marketplaceIndexPath,
1602
+ marketplaceName: freeze.marketplaceName,
1603
+ selectedEntry: freeze.selectedEntry,
1604
+ } : {}),
1477
1605
  },
1478
1606
  expected: {
1479
1607
  installed: true,
@@ -2106,9 +2234,304 @@ export async function prepareRelease(options) {
2106
2234
  )
2107
2235
  : null;
2108
2236
 
2109
- const units = unitResults.map(({ unit, manifest }, idx) => {
2237
+ // Freeze external independent marketplace HEADs (production + online only):
2238
+ // for each claude/codex/codebuddy distribution declaring marketplaceRepo,
2239
+ // resolve the external repo's HEAD sha + default branch and validate the
2240
+ // marketplace index entry at that sha before freezing the add-ref. This
2241
+ // MUST happen before installation contract construction so that standalone-index
2242
+ // contracts can use the frozen external entries. Offline production with a
2243
+ // declared marketplaceRepo fails closed inside the resolver. The remote is
2244
+ // only ever read (git ls-remote / gh api), never written.
2245
+ const externalMarketplaceFreezes = production
2246
+ ? await resolveExternalMarketplaceFreezes({
2247
+ unitResults,
2248
+ resolvedVersions,
2249
+ offline,
2250
+ evidence,
2251
+ observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
2252
+ fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
2253
+ })
2254
+ : new Map();
2255
+
2256
+ // 为每个分发渠道校验 marketplaceSourceType 并冻结安装契约。
2257
+ // 对每个插件 distribution:
2258
+ // 1. 从 unitResults[].manifest.outputDir 的真实快照读取 manifest
2259
+ // 2. manifest 读取:若平台 strategy.readManifest 存在则调用它,否则读 platform.manifestPaths.plugin
2260
+ // 3. 确定 includeMarketplaceEntry:
2261
+ // - bundled-family: Kimi=false, Claude/Codex/CodeBuddy=true(从快照读取)
2262
+ // - standalone-index: Kimi=false, Claude/Codex/CodeBuddy=true(使用外部冻结条目)
2263
+ // 4. 需要条目时:
2264
+ // - bundled-family: 从 dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace 读取 bundled 索引
2265
+ // - standalone-index: 从 externalMarketplaceFreezes 获取冻结的 selectedEntry 和 marketplaceIndexPath
2266
+ // 5. 使用 buildInstallationContract 构建完整可审计契约对象
2267
+ // 6. 在 distribution 中冻结 marketplaceSourceType / installationContract / installationContractDigest
2268
+ // 以及独立市场审计字段
2269
+ const units = await Promise.all(unitResults.map(async ({ unit, manifest }, idx) => {
2110
2270
  const unitVersion = resolvedVersions[idx];
2111
2271
  const unitBaseline = unitBaselineResults.get(unit.id);
2272
+ const snapshotDir = manifest.outputDir;
2273
+
2274
+ const distributionsWithSource = await Promise.all((unit.distributions ?? []).map(async (dist) => {
2275
+ const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
2276
+ if (!platform) return dist;
2277
+
2278
+ // 校验 marketplaceSourceType:从配置读取,不允许硬编码默认值
2279
+ const sourceTypeResult = validateMarketplaceSourceSelection(
2280
+ platform.id,
2281
+ dist, // config
2282
+ dist, // plan (same source at prepare time)
2283
+ );
2284
+ if (!sourceTypeResult.valid) {
2285
+ throw new ReleaseError(
2286
+ CONFIG_INVALID,
2287
+ `unit "${unit.id}" ${dist.type} marketplace source validation failed: ${sourceTypeResult.error}`,
2288
+ { unitId: unit.id, distributionType: dist.type },
2289
+ );
2290
+ }
2291
+
2292
+ // 仅插件 distribution 需要安装契约
2293
+ if (dist.type === 'npm') {
2294
+ return dist;
2295
+ }
2296
+
2297
+ // 1. 确定 marketplaceSourceType(防御性归一化:旧配置可能仍缺少字段)
2298
+ let marketplaceSourceType = sourceTypeResult.selectedSource;
2299
+ if (!marketplaceSourceType) {
2300
+ marketplaceSourceType = dist.marketplaceRepo ? 'standalone-index' : 'bundled-family';
2301
+ }
2302
+
2303
+ // 2. 确定 includeMarketplaceEntry 和 selectedMarketplaceEntry
2304
+ // bundled-family: 从快照中的 bundled 索引读取唯一选中条目。
2305
+ // standalone-index: 从 externalMarketplaceFreezes 获取冻结的 selectedEntry 和 marketplaceIndexPath。
2306
+ // Kimi 不纳入市场条目(platform.manifestPaths.marketplace === null 且无显式路径);
2307
+ // Claude/Codex/CodeBuddy 使用默认或显式路径。
2308
+ const hasDefaultMarketplace = platform.manifestPaths.marketplace !== null;
2309
+ const hasExplicitMarketplacePath = dist.marketplaceIndexPath != null;
2310
+ const isBundledFamily = marketplaceSourceType === 'bundled-family';
2311
+ const isStandaloneIndex = marketplaceSourceType === 'standalone-index';
2312
+
2313
+ // includeMarketplaceEntry 代表"契约实际包含一条市场条目",
2314
+ // 不能只代表平台理论上支持市场条目。
2315
+ // bundled-family: 平台支持市场时即包含(从快照读取)
2316
+ // standalone-index: 只有拿到冻结条目且非 Kimi 时才包含
2317
+ // Kimi 不纳入市场条目:Kimi 无市场 CLI,selectedEntry 仅供静态校验。
2318
+ let includeMarketplaceEntry;
2319
+ if (isBundledFamily) {
2320
+ includeMarketplaceEntry = hasDefaultMarketplace || hasExplicitMarketplacePath;
2321
+ } else if (isStandaloneIndex) {
2322
+ if (platform.id === 'kimi') {
2323
+ // Kimi standalone: 安装契约不纳入市场条目
2324
+ includeMarketplaceEntry = false;
2325
+ } else {
2326
+ const freezeKey = `${unit.id} ${dist.type}`;
2327
+ const freeze = externalMarketplaceFreezes.get(freezeKey);
2328
+ if (freeze) {
2329
+ includeMarketplaceEntry = true;
2330
+ } else {
2331
+ // 生产在线的独立市场缺冻结结果必须失败关闭
2332
+ if (production && !offline) {
2333
+ throw new ReleaseError(
2334
+ GATE_FAILED,
2335
+ `unit "${unit.id}" ${dist.type} standalone-index requires external marketplace freeze but no freeze result found`,
2336
+ { unitId: unit.id, distributionType: dist.type },
2337
+ );
2338
+ }
2339
+ // 非生产/离线没有冻结结果时,契约仍记录来源形态,但不包含空条目
2340
+ includeMarketplaceEntry = false;
2341
+ }
2342
+ }
2343
+ } else {
2344
+ includeMarketplaceEntry = false;
2345
+ }
2346
+
2347
+ // 3. 需要条目时,根据来源类型获取
2348
+ let selectedMarketplaceEntry = null;
2349
+ let marketplaceIndexRelative = null;
2350
+ let bundledMarketIndex = null;
2351
+ if (includeMarketplaceEntry) {
2352
+ if (isStandaloneIndex) {
2353
+ // standalone-index: 从外部冻结结果获取条目和路径(freeze 已确认存在)
2354
+ const freezeKey = `${unit.id} ${dist.type}`;
2355
+ const freeze = externalMarketplaceFreezes.get(freezeKey);
2356
+ selectedMarketplaceEntry = freeze.selectedEntry;
2357
+ marketplaceIndexRelative = freeze.marketplaceIndexPath;
2358
+ } else {
2359
+ // bundled-family: 从快照中的 bundled 索引读取
2360
+ marketplaceIndexRelative = dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
2361
+ if (!marketplaceIndexRelative) {
2362
+ throw new ReleaseError(
2363
+ GATE_FAILED,
2364
+ `unit "${unit.id}" ${dist.type} cannot determine marketplace index path: neither marketplaceIndexPath nor platform.manifestPaths.marketplace is set`,
2365
+ { unitId: unit.id, distributionType: dist.type },
2366
+ );
2367
+ }
2368
+ const marketplaceIndexPath = resolve(snapshotDir, marketplaceIndexRelative);
2369
+ let marketplaceIndexRaw;
2370
+ try {
2371
+ marketplaceIndexRaw = await readFile(marketplaceIndexPath, 'utf8');
2372
+ } catch (err) {
2373
+ throw new ReleaseError(
2374
+ GATE_FAILED,
2375
+ `unit "${unit.id}" ${dist.type} cannot read bundled marketplace index "${marketplaceIndexRelative}": ${err.message}`,
2376
+ { unitId: unit.id, distributionType: dist.type, cause: err.code },
2377
+ );
2378
+ }
2379
+ let marketplaceIndex;
2380
+ try {
2381
+ marketplaceIndex = JSON.parse(marketplaceIndexRaw);
2382
+ } catch (err) {
2383
+ throw new ReleaseError(
2384
+ GATE_FAILED,
2385
+ `unit "${unit.id}" ${dist.type} invalid JSON in bundled marketplace index "${marketplaceIndexRelative}": ${err.message}`,
2386
+ { unitId: unit.id, distributionType: dist.type },
2387
+ );
2388
+ }
2389
+ if (!marketplaceIndex || typeof marketplaceIndex !== 'object' || !Array.isArray(marketplaceIndex.plugins)) {
2390
+ throw new ReleaseError(
2391
+ GATE_FAILED,
2392
+ `unit "${unit.id}" ${dist.type} bundled marketplace index "${marketplaceIndexRelative}" must be an object with a plugins array`,
2393
+ { unitId: unit.id, distributionType: dist.type },
2394
+ );
2395
+ }
2396
+ const matchingEntries = marketplaceIndex.plugins.filter(
2397
+ (entry) => entry && entry.name === dist.plugin,
2398
+ );
2399
+ if (matchingEntries.length !== 1) {
2400
+ throw new ReleaseError(
2401
+ GATE_FAILED,
2402
+ `unit "${unit.id}" ${dist.type} bundled marketplace index must contain exactly one plugin entry named "${dist.plugin}", found ${matchingEntries.length}`,
2403
+ { unitId: unit.id, distributionType: dist.type },
2404
+ );
2405
+ }
2406
+ // 传完整解析条目给 buildInstallationContract,不得只取名字
2407
+ selectedMarketplaceEntry = matchingEntries[0];
2408
+ bundledMarketIndex = marketplaceIndex;
2409
+ }
2410
+ }
2411
+
2412
+ // 4. 从真实快照读取插件 manifest
2413
+ // bundled-family + 有市场索引:使用 resolvePluginManifestFromMarketplaceEntrySource
2414
+ // 从条目 source 安全解析插件根并读取 manifest(支持子目录布局)。
2415
+ // 其他路径:保留原有策略。
2416
+ let pluginManifestRelative;
2417
+ let pluginManifestParsed;
2418
+ if (isBundledFamily && bundledMarketIndex && platform.marketplaceSourceForm !== null) {
2419
+ // bundled-family 有市场索引且平台支持市场来源解析(Claude/Codex):
2420
+ // 通过条目 source 路径解析 manifest。
2421
+ // 计算市场根:从 marketplaceIndexRelative 推断(精确后缀匹配)。
2422
+ const mktRoot = resolveMarketplaceRoot(platform, marketplaceIndexRelative);
2423
+ try {
2424
+ const resolved = await resolvePluginManifestFromMarketplaceEntrySource(
2425
+ bundledMarketIndex, dist.plugin, platform, snapshotDir, mktRoot,
2426
+ );
2427
+ pluginManifestParsed = resolved.manifest;
2428
+ pluginManifestRelative = resolved.manifestRelativePath;
2429
+ } catch (err) {
2430
+ throw new ReleaseError(
2431
+ GATE_FAILED,
2432
+ `unit "${unit.id}" ${dist.type} cannot resolve plugin manifest from marketplace entry source: ${err.message}`,
2433
+ { unitId: unit.id, distributionType: dist.type },
2434
+ );
2435
+ }
2436
+ } else if (platform.strategy.readManifest) {
2437
+ // Kimi/Codex/CodeBuddy 有自定义 manifest 读取策略
2438
+ const readResult = await platform.strategy.readManifest(snapshotDir);
2439
+ pluginManifestParsed = readResult.manifest;
2440
+ pluginManifestRelative = readResult.manifestRelative ?? platform.manifestPaths.plugin;
2441
+ } else {
2442
+ // Claude fallback(standalone-index 或无市场索引时)
2443
+ pluginManifestRelative = platform.manifestPaths.plugin;
2444
+ const pluginManifestPath = resolve(snapshotDir, pluginManifestRelative);
2445
+ let raw;
2446
+ try {
2447
+ raw = await readFile(pluginManifestPath, 'utf8');
2448
+ } catch (err) {
2449
+ throw new ReleaseError(
2450
+ GATE_FAILED,
2451
+ `unit "${unit.id}" ${dist.type} cannot read plugin manifest "${pluginManifestRelative}": ${err.message}`,
2452
+ { unitId: unit.id, distributionType: dist.type, cause: err.code },
2453
+ );
2454
+ }
2455
+ try {
2456
+ pluginManifestParsed = JSON.parse(raw);
2457
+ } catch (err) {
2458
+ throw new ReleaseError(
2459
+ GATE_FAILED,
2460
+ `unit "${unit.id}" ${dist.type} invalid JSON in plugin manifest "${pluginManifestRelative}": ${err.message}`,
2461
+ { unitId: unit.id, distributionType: dist.type },
2462
+ );
2463
+ }
2464
+ }
2465
+
2466
+ // 静态校验 manifest 名称和版本(被摘要剔除不等于不校验)
2467
+ if (pluginManifestParsed.name !== dist.plugin) {
2468
+ throw new ReleaseError(
2469
+ GATE_FAILED,
2470
+ `unit "${unit.id}" ${dist.type} plugin manifest name "${pluginManifestParsed.name}" does not match distribution plugin "${dist.plugin}"`,
2471
+ { unitId: unit.id, distributionType: dist.type },
2472
+ );
2473
+ }
2474
+ if (typeof pluginManifestParsed.version === 'string' && pluginManifestParsed.version !== unitVersion) {
2475
+ throw new ReleaseError(
2476
+ GATE_FAILED,
2477
+ `unit "${unit.id}" ${dist.type} plugin manifest version "${pluginManifestParsed.version}" does not match target version "${unitVersion}"`,
2478
+ { unitId: unit.id, distributionType: dist.type },
2479
+ );
2480
+ }
2481
+
2482
+ // 5. 使用 buildInstallationContract 构建完整可审计契约对象
2483
+ const installationContract = buildInstallationContract({
2484
+ distributionType: dist.type,
2485
+ manifestRelativePath: pluginManifestRelative,
2486
+ manifest: pluginManifestParsed,
2487
+ marketplaceSourceType,
2488
+ includeMarketplaceEntry,
2489
+ ...(includeMarketplaceEntry ? {
2490
+ marketplaceIndexRelativePath: marketplaceIndexRelative,
2491
+ selectedMarketplaceEntry,
2492
+ } : {}),
2493
+ verificationRecipeVersion: CONSUMER_INSTALL_RECIPE_VERSION,
2494
+ });
2495
+
2496
+ // 6. 计算摘要(使用权威算法入口,保证契约对象与摘要一致)
2497
+ const installationContractDigest = computeInstallationContractDigest({
2498
+ distributionType: dist.type,
2499
+ manifestRelativePath: pluginManifestRelative,
2500
+ manifest: pluginManifestParsed,
2501
+ marketplaceSourceType,
2502
+ includeMarketplaceEntry,
2503
+ ...(includeMarketplaceEntry ? {
2504
+ marketplaceIndexRelativePath: marketplaceIndexRelative,
2505
+ selectedMarketplaceEntry,
2506
+ } : {}),
2507
+ verificationRecipeVersion: CONSUMER_INSTALL_RECIPE_VERSION,
2508
+ });
2509
+
2510
+ // 7. 构建返回对象,包含冻结的审计字段
2511
+ const frozenDist = {
2512
+ ...dist,
2513
+ marketplaceSourceType,
2514
+ installationContract,
2515
+ installationContractDigest,
2516
+ };
2517
+
2518
+ // standalone-index 审计字段:来自外部冻结结果
2519
+ if (isStandaloneIndex) {
2520
+ const freezeKey = `${unit.id} ${dist.type}`;
2521
+ const freeze = externalMarketplaceFreezes.get(freezeKey);
2522
+ if (freeze) {
2523
+ frozenDist.marketplaceRepo = freeze.marketplaceRepo;
2524
+ frozenDist.marketplaceCommitSha = freeze.marketplaceCommitSha;
2525
+ frozenDist.marketplaceRef = freeze.marketplaceRef;
2526
+ frozenDist.marketplaceIndexPath = freeze.marketplaceIndexPath;
2527
+ frozenDist.marketplaceName = freeze.marketplaceName;
2528
+ frozenDist.selectedEntry = freeze.selectedEntry;
2529
+ }
2530
+ }
2531
+
2532
+ return frozenDist;
2533
+ }));
2534
+
2112
2535
  return {
2113
2536
  id: unit.id,
2114
2537
  targetVersion: unitVersion,
@@ -2133,29 +2556,15 @@ export async function prepareRelease(options) {
2133
2556
  npm: productionAssets[idx].npm,
2134
2557
  },
2135
2558
  } : {}),
2136
- distributions: unit.distributions,
2559
+ distributions: distributionsWithSource,
2137
2560
  ...(unitBaseline ? { previousPublicBaseline: unitBaseline } : {}),
2138
2561
  };
2139
- });
2562
+ }));
2140
2563
 
2141
- // Freeze external independent marketplace HEADs (production + online only):
2142
- // for each claude/codex distribution declaring marketplaceRepo, resolve the
2143
- // external repo's HEAD sha + default branch and validate the marketplace
2144
- // index entry at that sha before freezing the add-ref. Offline production
2145
- // with a declared marketplaceRepo fails closed inside the resolver. The
2146
- // remote is only ever read (git ls-remote / gh api), never written.
2147
- const externalMarketplaceFreezes = production
2148
- ? await resolveExternalMarketplaceFreezes({
2149
- unitResults,
2150
- resolvedVersions,
2151
- offline,
2152
- evidence,
2153
- observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
2154
- fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
2155
- })
2156
- : new Map();
2564
+ // 构建冻结分发映射:unitId -> frozen distributions
2565
+ const frozenDistributionsMap = new Map(units.map((u) => [u.id, u.distributions]));
2157
2566
 
2158
- const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes);
2567
+ const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes, frozenDistributionsMap);
2159
2568
 
2160
2569
  // Compute overall snapshot digest
2161
2570
  const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));