release-skill 0.2.3 → 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 (53) 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 +22 -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 +264 -913
  11. package/README.zh-CN.md +222 -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 +19054 -17573
  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 +19054 -17573
  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 +19054 -17573
  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 +19054 -17573
  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 +19054 -17573
  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 +1190 -435
  39. package/src/commands/prepare.mjs +380 -40
  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 +304 -20
  44. package/src/core/baseline.mjs +8 -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 +129 -6
  50. package/src/platforms/codebuddy.mjs +193 -280
  51. package/src/platforms/codex.mjs +369 -0
  52. package/src/platforms/kimi.mjs +164 -119
  53. package/src/platforms/registry.mjs +24 -6
@@ -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;
@@ -1261,11 +1298,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1261
1298
  // Normalized marketplace form: explicit mutually exclusive declaration.
1262
1299
  // bundled-family: marketplace and plugin live in the same repo.
1263
1300
  // 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;
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;
1269
1305
  const sourceDescriptor = marketplaceForm === 'standalone-index'
1270
1306
  ? Object.freeze({
1271
1307
  form: 'standalone-index',
@@ -1314,6 +1350,14 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1314
1350
  ...(externalMarketplace ? { marketplaceLocation: 'external' } : {}),
1315
1351
  ...(marketplaceForm ? { marketplaceForm } : {}),
1316
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 不携带这三个字段(未冻结时无真实值可用)。
1317
1361
  },
1318
1362
  expected: {
1319
1363
  installed: true,
@@ -1336,6 +1380,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1336
1380
  const asset = productionAssets[index];
1337
1381
  const tagTemplate = unit.version?.tagTemplate ?? `${unit.id}-v{version}`;
1338
1382
  const resolvedTag = asset.tag;
1383
+ const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
1339
1384
 
1340
1385
  // Push snapshot
1341
1386
  actions.push({
@@ -1474,7 +1519,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1474
1519
  // entrySkillFound/manifestDigest expected). Marketplace identity follows
1475
1520
  // the registry's schema required fields — kimi carries none (MINOR-1).
1476
1521
  for (const platform of PLATFORMS) {
1477
- 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);
1478
1525
  if (!dist) continue;
1479
1526
  const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1480
1527
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
@@ -1486,9 +1533,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1486
1533
  const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1487
1534
  const freeze = externalMarketplace ? externalFreezes.get(`${unit.id} ${dist.type}`) : null;
1488
1535
  // Normalized marketplace form: explicit mutually exclusive declaration.
1489
- const marketplaceForm = requiresMarketplace
1490
- ? (externalMarketplace ? 'standalone-index' : 'bundled-family')
1491
- : null;
1536
+ // All four platforms carry marketplaceForm and sourceDescriptor.
1537
+ const marketplaceSourceType = dist.marketplaceSourceType ?? (externalMarketplace ? 'standalone-index' : 'bundled-family');
1538
+ const marketplaceForm = marketplaceSourceType;
1492
1539
  const sourceDescriptor = marketplaceForm === 'standalone-index'
1493
1540
  ? Object.freeze({
1494
1541
  form: 'standalone-index',
@@ -1543,6 +1590,18 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1543
1590
  // bundled-family: sourceDescriptor.commit 绑定到此值。
1544
1591
  // standalone-index: 通过此值绑定插件载荷来源。
1545
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
+ } : {}),
1546
1605
  },
1547
1606
  expected: {
1548
1607
  installed: true,
@@ -2175,9 +2234,304 @@ export async function prepareRelease(options) {
2175
2234
  )
2176
2235
  : null;
2177
2236
 
2178
- 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) => {
2179
2270
  const unitVersion = resolvedVersions[idx];
2180
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
+
2181
2535
  return {
2182
2536
  id: unit.id,
2183
2537
  targetVersion: unitVersion,
@@ -2202,29 +2556,15 @@ export async function prepareRelease(options) {
2202
2556
  npm: productionAssets[idx].npm,
2203
2557
  },
2204
2558
  } : {}),
2205
- distributions: unit.distributions,
2559
+ distributions: distributionsWithSource,
2206
2560
  ...(unitBaseline ? { previousPublicBaseline: unitBaseline } : {}),
2207
2561
  };
2208
- });
2562
+ }));
2209
2563
 
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();
2564
+ // 构建冻结分发映射:unitId -> frozen distributions
2565
+ const frozenDistributionsMap = new Map(units.map((u) => [u.id, u.distributions]));
2226
2566
 
2227
- const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes);
2567
+ const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes, frozenDistributionsMap);
2228
2568
 
2229
2569
  // Compute overall snapshot digest
2230
2570
  const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));