release-skill 0.9.0 → 0.9.1

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 (92) 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 +32 -0
  7. package/INSTALL.md +22 -9
  8. package/INSTALL.zh-CN.md +15 -8
  9. package/README.md +37 -30
  10. package/README.zh-CN.md +32 -27
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/consumer-contract-vectors.json +110 -0
  14. package/adapters/claude/bin/error-codes.json +8 -1
  15. package/adapters/claude/bin/foundation-resource-binding.json +1 -1
  16. package/adapters/claude/bin/registry.json +1 -1
  17. package/adapters/claude/bin/release-skill.bundle.mjs +1719 -1291
  18. package/adapters/claude/bin/rules.json +1 -1
  19. package/adapters/claude/bin/schemas/consumer-contract-vector.schema.json +149 -0
  20. package/adapters/claude/skills/release-finish/SKILL.md +4 -4
  21. package/adapters/claude/skills/release-help/SKILL.md +3 -2
  22. package/adapters/claude/skills/release-prepare/SKILL.md +2 -2
  23. package/adapters/claude/skills/release-publish/SKILL.md +3 -1
  24. package/adapters/claude/src/schemas/migration-manifest.schema.json +271 -7
  25. package/adapters/claude/src/schemas/profile-adoption-declaration.schema.json +8 -0
  26. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  27. package/adapters/codex/bin/consumer-contract-vectors.json +110 -0
  28. package/adapters/codex/bin/error-codes.json +8 -1
  29. package/adapters/codex/bin/foundation-resource-binding.json +1 -1
  30. package/adapters/codex/bin/registry.json +1 -1
  31. package/adapters/codex/bin/release-skill.bundle.mjs +1719 -1291
  32. package/adapters/codex/bin/rules.json +1 -1
  33. package/adapters/codex/bin/schemas/consumer-contract-vector.schema.json +149 -0
  34. package/adapters/codex/skills/release-finish/SKILL.md +4 -4
  35. package/adapters/codex/skills/release-help/SKILL.md +3 -2
  36. package/adapters/codex/skills/release-prepare/SKILL.md +2 -2
  37. package/adapters/codex/skills/release-publish/SKILL.md +3 -1
  38. package/adapters/codex/src/schemas/migration-manifest.schema.json +271 -7
  39. package/adapters/codex/src/schemas/profile-adoption-declaration.schema.json +8 -0
  40. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  41. package/adapters/kimi/bin/consumer-contract-vectors.json +110 -0
  42. package/adapters/kimi/bin/error-codes.json +8 -1
  43. package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
  44. package/adapters/kimi/bin/registry.json +1 -1
  45. package/adapters/kimi/bin/release-skill.bundle.mjs +1719 -1291
  46. package/adapters/kimi/bin/rules.json +1 -1
  47. package/adapters/kimi/bin/schemas/consumer-contract-vector.schema.json +149 -0
  48. package/adapters/kimi/skills/release-finish/SKILL.md +4 -4
  49. package/adapters/kimi/skills/release-help/SKILL.md +3 -2
  50. package/adapters/kimi/skills/release-prepare/SKILL.md +2 -2
  51. package/adapters/kimi/skills/release-publish/SKILL.md +3 -1
  52. package/adapters/kimi/src/schemas/migration-manifest.schema.json +271 -7
  53. package/adapters/kimi/src/schemas/profile-adoption-declaration.schema.json +8 -0
  54. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  55. package/adapters/workbuddy/bin/consumer-contract-vectors.json +110 -0
  56. package/adapters/workbuddy/bin/error-codes.json +8 -1
  57. package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
  58. package/adapters/workbuddy/bin/registry.json +1 -1
  59. package/adapters/workbuddy/bin/release-skill.bundle.mjs +1719 -1291
  60. package/adapters/workbuddy/bin/rules.json +1 -1
  61. package/adapters/workbuddy/bin/schemas/consumer-contract-vector.schema.json +149 -0
  62. package/adapters/workbuddy/skills/release-finish/SKILL.md +4 -4
  63. package/adapters/workbuddy/skills/release-help/SKILL.md +3 -2
  64. package/adapters/workbuddy/skills/release-prepare/SKILL.md +2 -2
  65. package/adapters/workbuddy/skills/release-publish/SKILL.md +3 -1
  66. package/adapters/workbuddy/src/schemas/migration-manifest.schema.json +271 -7
  67. package/adapters/workbuddy/src/schemas/profile-adoption-declaration.schema.json +8 -0
  68. package/bin/consumer-contract-vectors.json +110 -0
  69. package/bin/error-codes.json +8 -1
  70. package/bin/foundation-resource-binding.json +1 -1
  71. package/bin/registry.json +1 -1
  72. package/bin/release-skill-cli.mjs +40 -4
  73. package/bin/release-skill.bundle.mjs +1719 -1291
  74. package/bin/rules.json +1 -1
  75. package/bin/schemas/consumer-contract-vector.schema.json +149 -0
  76. package/package.json +4 -4
  77. package/platform-manifest.json +5 -5
  78. package/skills/release-finish/SKILL.md +4 -4
  79. package/skills/release-help/SKILL.md +3 -2
  80. package/skills/release-prepare/SKILL.md +2 -2
  81. package/skills/release-publish/SKILL.md +3 -1
  82. package/skills-src/release-finish/SKILL.md +4 -4
  83. package/skills-src/release-help/SKILL.md +3 -2
  84. package/skills-src/release-prepare/SKILL.md +2 -2
  85. package/skills-src/release-publish/SKILL.md +3 -1
  86. package/src/commands/post-release-local.mjs +249 -58
  87. package/src/commands/prepare.mjs +39 -4
  88. package/src/commands/ship.mjs +124 -16
  89. package/src/core/derived-artifact-gates.mjs +32 -5
  90. package/src/producers/foundation-resource-projection.mjs +16 -3
  91. package/src/schemas/migration-manifest.schema.json +271 -7
  92. package/src/schemas/profile-adoption-declaration.schema.json +8 -0
@@ -2,6 +2,7 @@ import { readFile, lstat, mkdir } from 'node:fs/promises';
2
2
  import { basename, dirname, resolve } from 'node:path';
3
3
  import { publishFileOrReplace } from 'skill-family-harness-node';
4
4
 
5
+ import { MAX_APPROVAL_MS } from '../core/approval.mjs';
5
6
  import { canonicalJson, sha256Hex } from '../core/digest.mjs';
6
7
  import {
7
8
  effectiveHookRequiresApproval,
@@ -88,6 +89,7 @@ async function defaultDependencies() {
88
89
  postVerifyRelease: postverifyModule.postVerifyRelease,
89
90
  preflightGitTransports: transportModule.preflightGitTransports,
90
91
  updatePreviousPublicBaselines: metadataModule.updatePreviousPublicBaselines,
92
+ readMaxApprovalMs: () => MAX_APPROVAL_MS,
91
93
  };
92
94
  }
93
95
 
@@ -116,34 +118,135 @@ function publicState(state) {
116
118
  ...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
117
119
  ...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
118
120
  ...(state.postRelease ? { postRelease: state.postRelease } : {}),
119
- verificationGateAuthorizationIncludedInPlanApproval: true,
120
- postVerifyMetadataUpdateIncludedInPlanApproval: true,
121
+ externalActionsIncludedInPlanApproval: true,
122
+ verificationGatesIncludedInPlanApproval: true,
123
+ postPublishCheckpointApprovalsIncludedInPlanApproval: false,
121
124
  };
122
125
  }
123
126
 
127
+ const HOUR_MS = 60 * 60 * 1000;
128
+
129
+ const ACTION_TARGET_FIELDS = Object.freeze({
130
+ 'push-commit': Object.freeze(['remote', 'branch']),
131
+ 'push-snapshot': Object.freeze(['repo', 'branch', 'branchStrategy', 'commit']),
132
+ 'create-tag': Object.freeze(['repo', 'tag', 'commit']),
133
+ 'npm-publish': Object.freeze(['package', 'version', 'registry', 'tag', 'access', 'provenance']),
134
+ 'github-release': Object.freeze(['repo', 'tag', 'name', 'commit']),
135
+ 'claude-marketplace-install': Object.freeze(['consumer', 'plugin', 'marketplace', 'repo', 'ref', 'version', 'entrySkill']),
136
+ 'codex-marketplace-install': Object.freeze(['consumer', 'plugin', 'marketplace', 'repo', 'ref', 'version', 'entrySkill']),
137
+ 'kimi-marketplace-install': Object.freeze(['consumer', 'plugin', 'repo', 'ref', 'version', 'entrySkill']),
138
+ 'codebuddy-marketplace-install': Object.freeze(['consumer', 'plugin', 'repo', 'ref', 'version', 'entrySkill']),
139
+ 'set-default-branch': Object.freeze(['repo', 'oldBranch', 'newBranch', 'expectedNewBranchCommit']),
140
+ });
141
+
142
+ function deriveApprovalWindowHours(maxApprovalMs) {
143
+ if (
144
+ !Number.isSafeInteger(maxApprovalMs)
145
+ || maxApprovalMs <= 0
146
+ || maxApprovalMs % HOUR_MS !== 0
147
+ ) {
148
+ throw new ReleaseError(
149
+ GATE_FAILED,
150
+ 'core approval window must be a positive whole number of hours',
151
+ { maxApprovalMs },
152
+ );
153
+ }
154
+ return maxApprovalMs / HOUR_MS;
155
+ }
156
+
157
+ function projectActionTarget(action) {
158
+ const parameters = action?.parameters ?? {};
159
+ return Object.fromEntries(
160
+ (ACTION_TARGET_FIELDS[action?.type] ?? [])
161
+ .filter((field) => parameters[field] !== undefined)
162
+ .map((field) => [field, parameters[field]]),
163
+ );
164
+ }
165
+
124
166
  /**
125
167
  * Build a human-readable approval summary from the frozen plan.
126
- * Lists each unit's version and every external action's id/type/unitId.
127
- * Returns an empty summary when the plan file is not yet available (e.g., in tests).
168
+ * Lists every publish target and every checkpoint approval separately. All
169
+ * fields are mechanically projected from the frozen plan; unreadable plans
170
+ * fail closed instead of producing an empty or partial summary.
128
171
  *
129
172
  * @param {string} planPath - Path to the frozen release plan.
130
173
  * @returns {Promise<object>} The approval summary.
131
174
  */
132
- async function buildApprovalSummary(planPath) {
175
+ async function buildApprovalSummary(planPath, maxApprovalMs = MAX_APPROVAL_MS) {
133
176
  try {
134
177
  const plan = JSON.parse(await readFile(planPath, 'utf8'));
135
- const units = (plan.units ?? []).map((unit) => ({
136
- id: unit.id,
137
- targetVersion: unit.targetVersion ?? unit.version,
138
- }));
178
+ const approvalWindowHours = deriveApprovalWindowHours(maxApprovalMs);
139
179
  const actions = (plan.externalActions ?? []).map((action) => ({
140
180
  id: action.id,
141
181
  type: action.type,
142
182
  unitId: action.unitId,
183
+ target: projectActionTarget(action),
143
184
  }));
144
- return { units, actions };
145
- } catch {
146
- return { units: [], actions: [] };
185
+ const units = (plan.units ?? []).map((unit) => {
186
+ const targetVersion = unit.targetVersion ?? unit.version;
187
+ const githubRelease = (plan.externalActions ?? [])
188
+ .find((action) => action.unitId === unit.id && action.type === 'github-release');
189
+ return {
190
+ id: unit.id,
191
+ targetVersion,
192
+ publicRepo: unit.publicRepo,
193
+ branch: unit.frozenSnapshot?.branch
194
+ ?? unit.productionConfig?.branchTemplate
195
+ ?.replaceAll('{tag}', unit.tagTemplate?.replace('{version}', targetVersion) ?? '')
196
+ .replaceAll('{version}', targetVersion)
197
+ .replaceAll('{unit}', unit.id),
198
+ branchStrategy: unit.frozenSnapshot?.branchStrategy ?? unit.productionConfig?.branchStrategy,
199
+ tag: unit.tagTemplate?.replace('{version}', targetVersion),
200
+ npm: (unit.distributions ?? [])
201
+ .filter((distribution) => distribution.type === 'npm')
202
+ .map((distribution) => ({
203
+ package: distribution.package,
204
+ version: targetVersion,
205
+ registry: distribution.registry,
206
+ distTag: distribution.tag,
207
+ access: distribution.access,
208
+ provenance: distribution.provenance,
209
+ })),
210
+ ...(githubRelease ? {
211
+ githubRelease: {
212
+ repo: githubRelease.parameters?.repo ?? githubRelease.parameters?.publicRepo,
213
+ tag: githubRelease.parameters?.tag,
214
+ name: githubRelease.parameters?.name,
215
+ },
216
+ } : {}),
217
+ };
218
+ });
219
+ const postPublishCheckpointApprovals = normalizePostPublishView(plan)
220
+ .flatMap((declaration) => (declaration.hooks ?? [])
221
+ .filter((hook) => effectiveHookRequiresApproval(hook))
222
+ .map((hook) => ({
223
+ unitId: declaration.unitId,
224
+ hookId: hook.id,
225
+ actionId: postPublishActionId({
226
+ planVersion: plan.planVersion,
227
+ unitId: declaration.unitId,
228
+ localId: hook.id,
229
+ }),
230
+ phase: hook.phase ?? 'distribute',
231
+ ...(hook.preset !== undefined ? { preset: hook.preset } : {}),
232
+ ...(hook.config?.delivery !== undefined ? { delivery: hook.config.delivery } : {}),
233
+ requiresApproval: true,
234
+ includedInPlanApproval: false,
235
+ approvalWindowHours,
236
+ binding: { planDigest: plan.digest, hookId: hook.id },
237
+ })));
238
+ return {
239
+ units,
240
+ actions,
241
+ waivers: Array.isArray(plan.waivers) ? plan.waivers : [],
242
+ postPublishCheckpointApprovals,
243
+ };
244
+ } catch (error) {
245
+ throw new ReleaseError(
246
+ GATE_FAILED,
247
+ `cannot build ship approval summary from frozen plan: ${error.message}`,
248
+ { planPath },
249
+ );
147
250
  }
148
251
  }
149
252
 
@@ -197,9 +300,11 @@ async function allUnclosedPostVerifyHooksUngated(state, postVerifyHooks, planVer
197
300
  * resumes instead of reconstructing authority from terminal/chat output.
198
301
  *
199
302
  * New flow (v0.4+): ship directly runs configured hooks and verification gates
200
- * without a separate hook authorization step. The only human gate is plan
201
- * approval. Kimi/CodeBuddy installations are non-blocking manual follow-up
202
- * tasks when the plan declares humanConsumersStrategy: 'manualFollowUps'.
303
+ * without a separate hook authorization step. Plan approval is the only
304
+ * normal release-level gate. A postPublish hook whose effective
305
+ * requiresApproval is true still needs its independent checkpoint approval.
306
+ * Kimi/CodeBuddy installations are non-blocking manual follow-up tasks when
307
+ * the plan declares humanConsumersStrategy: 'manualFollowUps'.
203
308
  */
204
309
  export async function advanceShip(options = {}, injected = {}) {
205
310
  const root = resolve(options.root ?? process.cwd());
@@ -268,7 +373,10 @@ export async function advanceShip(options = {}, injected = {}) {
268
373
  transportPreflight = await deps.preflightGitTransports(frozenPlan);
269
374
  process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
270
375
  }
271
- const approvalSummary = await buildApprovalSummary(prepared.planPath);
376
+ const maxApprovalMs = typeof deps.readMaxApprovalMs === 'function'
377
+ ? await deps.readMaxApprovalMs()
378
+ : MAX_APPROVAL_MS;
379
+ const approvalSummary = await buildApprovalSummary(prepared.planPath, maxApprovalMs);
272
380
  state = {
273
381
  ...state,
274
382
  status: 'NEEDS_PLAN_APPROVAL',
@@ -5,17 +5,20 @@
5
5
  * In the 0.6.1 cycle two prepares burned the full ~80s test hook before
6
6
  * surfacing drift that a sub-second check could have caught: adapter trees
7
7
  * out of sync with skills-src/, and self-bootstrap fact pins still bound to
8
- * the previous version. This module promotes those two exact checks to
9
- * prepare's earliest stage, alongside the bundle freshness gate:
8
+ * the previous version. This module promotes those checks and the platform
9
+ * manifest freshness check to prepare's earliest stage, alongside the bundle
10
+ * freshness gate:
10
11
  *
11
12
  * - adapter gate: runs `scripts/build-adapters.mjs --check` (drift list,
12
13
  * exit 1 on drift — the same supported check the scripts surface offers);
14
+ * - platform manifest gate: runs
15
+ * `scripts/generate-platform-manifest.mjs --check`;
13
16
  * - fact-pin gate: runs the version fact pins of
14
17
  * `test/release-docs-self-bootstrap.test.mjs` — exactly its hermetic
15
18
  * section 1 (`[self-bootstrap 1*]`: byte-level version assertions plus the
16
19
  * in-process read-only planner), scoped via `--test-name-pattern`.
17
20
  *
18
- * Both promote the CANONICAL check logic — the gate and the full pipeline
21
+ * All three promote the CANONICAL check logic — the gates and the full pipeline
19
22
  * can never disagree about what "in sync" means. The facts gate is scoped to
20
23
  * the suite's hermetic fact-pin section deliberately: the suite's remaining
21
24
  * sections shell out to npm/git and drive fixture prepares, which would make
@@ -88,6 +91,15 @@ const GATES = Object.freeze({
88
91
  'Rebuild the existing adapters with: node scripts/build-adapters.mjs --apply ' +
89
92
  `(or run the one-click derived-artifact sync from the workspace root: ${DERIVED_SYNC_COMMAND}).`,
90
93
  }),
94
+ 'platform-manifest': Object.freeze({
95
+ artifact: 'platform-manifest',
96
+ marker: join('scripts', 'generate-platform-manifest.mjs'),
97
+ argv: (pkgRoot) => [join(pkgRoot, 'scripts', 'generate-platform-manifest.mjs'), '--check'],
98
+ timeoutMs: 120000,
99
+ remediation:
100
+ 'Regenerate the platform manifest with: node scripts/generate-platform-manifest.mjs ' +
101
+ `(or run the one-click derived-artifact sync from the workspace root: ${DERIVED_SYNC_COMMAND}).`,
102
+ }),
91
103
  'self-bootstrap-facts': Object.freeze({
92
104
  artifact: 'self-bootstrap-facts',
93
105
  marker: join('test', 'release-docs-self-bootstrap.test.mjs'),
@@ -118,7 +130,7 @@ async function isFile(path) {
118
130
  /**
119
131
  * Run one derived-artifact check (pure decision, never throws).
120
132
  *
121
- * @param {'adapters' | 'self-bootstrap-facts'} kind - Gate to run.
133
+ * @param {'adapters' | 'platform-manifest' | 'self-bootstrap-facts'} kind - Gate to run.
122
134
  * @param {string} pkgRoot - Absolute package root of the running checkout.
123
135
  * @param {object} [options]
124
136
  * @param {Function} [options.execFileFn] - execFile seam (tests).
@@ -202,6 +214,11 @@ export function checkAdapterFreshness(pkgRoot, options = {}) {
202
214
  return checkDerivedArtifactGate('adapters', pkgRoot, options);
203
215
  }
204
216
 
217
+ /** Platform manifest freshness decision (generate-platform-manifest --check). */
218
+ export function checkPlatformManifestFreshness(pkgRoot, options = {}) {
219
+ return checkDerivedArtifactGate('platform-manifest', pkgRoot, options);
220
+ }
221
+
205
222
  /** Self-bootstrap fact-pin decision (single-file test). */
206
223
  export function checkSelfBootstrapFacts(pkgRoot, options = {}) {
207
224
  return checkDerivedArtifactGate('self-bootstrap-facts', pkgRoot, options);
@@ -215,7 +232,9 @@ async function assertGate(kind, pkgRoot, options = {}) {
215
232
  const gate = GATES[kind];
216
233
  const subject = kind === 'adapters'
217
234
  ? 'adapters/ is out of sync with its sources (build-adapters --check reported drift)'
218
- : 'the release-docs-self-bootstrap fact pins are stale (the hermetic fact-pin check failed)';
235
+ : kind === 'platform-manifest'
236
+ ? 'platform-manifest.json is out of sync with the current package tree (generate-platform-manifest --check reported drift)'
237
+ : 'the release-docs-self-bootstrap fact pins are stale (the hermetic fact-pin check failed)';
219
238
  throw new ReleaseError(
220
239
  DERIVED_ARTIFACT_STALE,
221
240
  `${subject}. ${DERIVED_ARTIFACT_PREGATE_NOTE} ${gate.remediation}`,
@@ -243,6 +262,14 @@ export function assertAdapterFreshness(pkgRoot, options = {}) {
243
262
  return assertGate('adapters', pkgRoot, options);
244
263
  }
245
264
 
265
+ /**
266
+ * Fail-closed platform manifest pre-gate used by prepare's earliest stage.
267
+ * Not-applicable layouts return quietly; drift throws DERIVED_ARTIFACT_STALE.
268
+ */
269
+ export function assertPlatformManifestFreshness(pkgRoot, options = {}) {
270
+ return assertGate('platform-manifest', pkgRoot, options);
271
+ }
272
+
246
273
  /**
247
274
  * Fail-closed self-bootstrap fact-pin pre-gate used by prepare's earliest
248
275
  * stage. Not-applicable layouts return quietly; drift throws
@@ -55,9 +55,9 @@ import { digestBytes, publishFileOrReplace, readFileStrict, withTemporaryWorkspa
55
55
  import { canonicalJson } from '../core/digest.mjs';
56
56
 
57
57
  export const FOUNDATION_PACKAGES = Object.freeze({
58
- 'skill-family-contracts': '0.13.0',
59
- 'skill-family-engineering-kit': '0.13.0',
60
- 'skill-family-harness-node': '0.13.0',
58
+ 'skill-family-contracts': '0.14.0',
59
+ 'skill-family-engineering-kit': '0.14.0',
60
+ 'skill-family-harness-node': '0.14.0',
61
61
  });
62
62
 
63
63
  export const BINDING_RECORD_PATH = 'bin/foundation-resource-binding.json';
@@ -295,6 +295,19 @@ export async function computeFoundationProjection({ pkgRoot, bundleContent }) {
295
295
  continue;
296
296
  }
297
297
  }
298
+ // Foundation 0.14 keeps this example behind a capability-selection
299
+ // branch that esbuild removes from the release-skill bundle. The
300
+ // source scanner still sees the directory URL in the marked module,
301
+ // so admit this one exact directory only when its path survives in
302
+ // the built bundle.
303
+ if (
304
+ pkg === 'skill-family-engineering-kit'
305
+ && toPosix(relative(packages[pkg].root, sourceDir)) === 'data/examples/minimal-plugin-payload'
306
+ && !bundleContent.includes('data/examples/minimal-plugin-payload')
307
+ ) {
308
+ scopeOf(targetPosix);
309
+ continue;
310
+ }
298
311
  const files = await walkTree(sourceDir, `${pkg} directory read "${rel}"`);
299
312
  const members = [];
300
313
  for (const file of files) {
@@ -13,6 +13,138 @@
13
13
  "kind": {
14
14
  "const": "skill-family.migration-manifest"
15
15
  },
16
+ "capabilityUseContext": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "required": ["scope", "locale"],
20
+ "properties": {
21
+ "scope": {
22
+ "type": "string",
23
+ "enum": ["contracts", "harness", "engineering-kit", "profile", "all"]
24
+ },
25
+ "locale": {
26
+ "type": "string",
27
+ "enum": ["en", "zh-CN"]
28
+ }
29
+ }
30
+ },
31
+ "capabilityUses": {
32
+ "type": "array",
33
+ "minItems": 1,
34
+ "maxItems": 200,
35
+ "items": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["useId", "problemStatement", "constraints", "retainedGuarantees"],
39
+ "properties": {
40
+ "useId": {
41
+ "type": "string",
42
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
43
+ "maxLength": 80
44
+ },
45
+ "problemStatement": {
46
+ "allOf": [
47
+ { "$ref": "#/$defs/trimmedCapabilityUseText" },
48
+ { "type": "string", "maxLength": 1000 }
49
+ ]
50
+ },
51
+ "constraints": {
52
+ "type": "array",
53
+ "maxItems": 20,
54
+ "uniqueItems": true,
55
+ "items": {
56
+ "allOf": [
57
+ { "$ref": "#/$defs/trimmedCapabilityUseText" },
58
+ { "type": "string", "maxLength": 500 }
59
+ ]
60
+ }
61
+ },
62
+ "retainedGuarantees": {
63
+ "type": "array",
64
+ "minItems": 1,
65
+ "maxItems": 20,
66
+ "uniqueItems": true,
67
+ "items": {
68
+ "allOf": [
69
+ { "$ref": "#/$defs/trimmedCapabilityUseText" },
70
+ { "type": "string", "maxLength": 500 }
71
+ ]
72
+ }
73
+ },
74
+ "catalogFilters": {
75
+ "type": "array",
76
+ "maxItems": 20,
77
+ "uniqueItems": true,
78
+ "items": {
79
+ "allOf": [
80
+ { "$ref": "#/$defs/trimmedCapabilityUseText" },
81
+ { "type": "string", "maxLength": 200 }
82
+ ]
83
+ }
84
+ }
85
+ }
86
+ }
87
+ },
88
+ "capabilityDecisions": {
89
+ "type": "array",
90
+ "items": {
91
+ "type": "object",
92
+ "additionalProperties": false,
93
+ "required": ["useId", "disposition", "reason"],
94
+ "properties": {
95
+ "useId": {
96
+ "type": "string",
97
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
98
+ "maxLength": 80
99
+ },
100
+ "disposition": {
101
+ "type": "string",
102
+ "enum": ["direct-adoption", "compatibility-layer", "keep-business", "foundation-gap"]
103
+ },
104
+ "targetCapability": {
105
+ "type": "string",
106
+ "pattern": "^foundation\\.[a-z0-9\\-]+(\\.[a-z0-9\\-]+)+$"
107
+ },
108
+ "reason": {
109
+ "type": "string",
110
+ "minLength": 1,
111
+ "maxLength": 1000
112
+ }
113
+ },
114
+ "allOf": [
115
+ {
116
+ "if": {
117
+ "required": ["disposition"],
118
+ "properties": {
119
+ "disposition": {
120
+ "enum": ["direct-adoption", "compatibility-layer"]
121
+ }
122
+ }
123
+ },
124
+ "then": {
125
+ "required": ["targetCapability"],
126
+ "properties": { "targetCapability": { "type": "string" } }
127
+ }
128
+ },
129
+ {
130
+ "if": {
131
+ "required": ["disposition"],
132
+ "properties": {
133
+ "disposition": {
134
+ "enum": ["keep-business", "foundation-gap"]
135
+ }
136
+ }
137
+ },
138
+ "then": {
139
+ "not": {
140
+ "required": ["targetCapability"],
141
+ "properties": { "targetCapability": { "type": "string" } }
142
+ }
143
+ }
144
+ }
145
+ ]
146
+ }
147
+ },
16
148
  "manifestVersion": {
17
149
  "type": "integer",
18
150
  "minimum": 1,
@@ -31,11 +163,34 @@
31
163
  "description": "Relative POSIX path of one legacy implementation. Shape only: absolute paths, traversal, UNC/drive forms and symlink escapes are rejected by harness containment at assessment time, fail-closed."
32
164
  },
33
165
  "replacedBy": {
34
- "type": "string",
35
- "minLength": 1,
166
+ "oneOf": [
167
+ { "type": "string", "minLength": 1 },
168
+ { "type": "null" }
169
+ ],
36
170
  "description": "The replacement facility (tool/command/module) that supersedes this legacy entry. An empty replacement is never a valid exit claim."
171
+ },
172
+ "useId": {
173
+ "type": "string",
174
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
175
+ "maxLength": 80
176
+ },
177
+ "recoveryRef": {
178
+ "type": "string",
179
+ "minLength": 1
37
180
  }
38
- }
181
+ },
182
+ "allOf": [
183
+ {
184
+ "if": {
185
+ "required": ["replacedBy"],
186
+ "properties": { "replacedBy": { "type": "null" } }
187
+ },
188
+ "then": {
189
+ "required": ["useId"],
190
+ "properties": { "useId": { "type": "string" } }
191
+ }
192
+ }
193
+ ]
39
194
  }
40
195
  },
41
196
  "legacyReferences": {
@@ -57,11 +212,30 @@
57
212
  "description": "Exact literal text that must be absent from the file. No regex; byte-exact UTF-8 match."
58
213
  },
59
214
  "replacedBy": {
60
- "type": "string",
61
- "minLength": 1,
215
+ "oneOf": [
216
+ { "type": "string", "minLength": 1 },
217
+ { "type": "null" }
218
+ ],
62
219
  "description": "The replacement facility that supersedes this old reference. Empty is never valid."
220
+ },
221
+ "useId": {
222
+ "type": "string",
223
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
224
+ "maxLength": 80
63
225
  }
64
- }
226
+ },
227
+ "allOf": [
228
+ {
229
+ "if": {
230
+ "required": ["replacedBy"],
231
+ "properties": { "replacedBy": { "type": "null" } }
232
+ },
233
+ "then": {
234
+ "required": ["useId"],
235
+ "properties": { "useId": { "type": "string" } }
236
+ }
237
+ }
238
+ ]
65
239
  }
66
240
  },
67
241
  "exceptions": {
@@ -144,5 +318,95 @@
144
318
  }
145
319
  }
146
320
  }
147
- }
321
+ },
322
+ "$defs": {
323
+ "trimmedCapabilityUseText": {
324
+ "type": "string",
325
+ "minLength": 1,
326
+ "pattern": "^(?!\\s)(?![\\s\\S]*\\s$)(?![\\s\\S]*\\u0000)[\\s\\S]+$"
327
+ }
328
+ },
329
+ "allOf": [
330
+ {
331
+ "if": {
332
+ "required": ["capabilityUses"],
333
+ "properties": { "capabilityUses": { "type": "array" } }
334
+ },
335
+ "then": {
336
+ "required": ["capabilityUseContext"],
337
+ "properties": { "capabilityUseContext": { "type": "object" } }
338
+ },
339
+ "else": {
340
+ "not": {
341
+ "anyOf": [
342
+ {
343
+ "required": ["capabilityUseContext"],
344
+ "properties": { "capabilityUseContext": { "type": "object" } }
345
+ },
346
+ {
347
+ "required": ["capabilityDecisions"],
348
+ "properties": { "capabilityDecisions": { "type": "array" } }
349
+ }
350
+ ]
351
+ }
352
+ }
353
+ },
354
+ {
355
+ "if": {
356
+ "required": ["capabilityDecisions"],
357
+ "properties": { "capabilityDecisions": { "type": "array" } }
358
+ },
359
+ "then": {
360
+ "required": ["capabilityUses", "capabilityUseContext"],
361
+ "properties": {
362
+ "capabilityUses": { "type": "array" },
363
+ "capabilityUseContext": { "type": "object" }
364
+ }
365
+ }
366
+ },
367
+ {
368
+ "if": {
369
+ "required": ["legacyInfra"],
370
+ "properties": {
371
+ "legacyInfra": {
372
+ "type": "array",
373
+ "contains": {
374
+ "type": "object",
375
+ "required": ["useId"],
376
+ "properties": { "useId": { "type": "string" } }
377
+ }
378
+ }
379
+ }
380
+ },
381
+ "then": {
382
+ "required": ["capabilityUses", "capabilityUseContext"],
383
+ "properties": {
384
+ "capabilityUses": { "type": "array" },
385
+ "capabilityUseContext": { "type": "object" }
386
+ }
387
+ }
388
+ },
389
+ {
390
+ "if": {
391
+ "required": ["legacyReferences"],
392
+ "properties": {
393
+ "legacyReferences": {
394
+ "type": "array",
395
+ "contains": {
396
+ "type": "object",
397
+ "required": ["useId"],
398
+ "properties": { "useId": { "type": "string" } }
399
+ }
400
+ }
401
+ }
402
+ },
403
+ "then": {
404
+ "required": ["capabilityUses", "capabilityUseContext"],
405
+ "properties": {
406
+ "capabilityUses": { "type": "array" },
407
+ "capabilityUseContext": { "type": "object" }
408
+ }
409
+ }
410
+ }
411
+ ]
148
412
  }
@@ -54,6 +54,14 @@
54
54
  "stability": { "enum": ["stable", "candidate-only"] }
55
55
  }
56
56
  },
57
+ "capabilities": {
58
+ "type": "array",
59
+ "uniqueItems": true,
60
+ "items": {
61
+ "type": "string",
62
+ "pattern": "^foundation\\.[a-z0-9\\-]+(?:\\.[a-z0-9\\-]+)+$"
63
+ }
64
+ },
57
65
  "foundation_pin": {
58
66
  "type": "object",
59
67
  "additionalProperties": false,