release-skill 0.1.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 (125) hide show
  1. package/.agents/plugins/marketplace.json +23 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +26 -0
  5. package/CHANGELOG.md +68 -0
  6. package/CODE_OF_CONDUCT.md +76 -0
  7. package/CONTRIBUTING.md +49 -0
  8. package/INSTALL.md +182 -0
  9. package/LICENSE +21 -0
  10. package/NOTICE +25 -0
  11. package/README.md +501 -0
  12. package/README.zh-CN.md +463 -0
  13. package/SECURITY.md +48 -0
  14. package/adapters/claude/.claude-plugin/marketplace.json +16 -0
  15. package/adapters/claude/.claude-plugin/plugin.json +10 -0
  16. package/adapters/claude/skills/release-assess/SKILL.md +52 -0
  17. package/adapters/claude/skills/release-help/SKILL.md +60 -0
  18. package/adapters/claude/skills/release-prepare/SKILL.md +71 -0
  19. package/adapters/claude/skills/release-publish/SKILL.md +55 -0
  20. package/adapters/claude/skills/release-reconcile/SKILL.md +73 -0
  21. package/adapters/claude/skills/release-verify/SKILL.md +70 -0
  22. package/adapters/codex/.codex-plugin/plugin.json +26 -0
  23. package/adapters/codex/skills/release-assess/SKILL.md +52 -0
  24. package/adapters/codex/skills/release-help/SKILL.md +60 -0
  25. package/adapters/codex/skills/release-prepare/SKILL.md +71 -0
  26. package/adapters/codex/skills/release-publish/SKILL.md +55 -0
  27. package/adapters/codex/skills/release-reconcile/SKILL.md +73 -0
  28. package/adapters/codex/skills/release-verify/SKILL.md +70 -0
  29. package/bin/release-skill.mjs +743 -0
  30. package/native/safe-write/binding.gyp +40 -0
  31. package/native/safe-write/prebuilds.json +4 -0
  32. package/native/safe-write/src/safe_write.cc +2023 -0
  33. package/package.json +75 -0
  34. package/references/.render-manifest.json +33 -0
  35. package/references/00-target-state.md +124 -0
  36. package/references/01-state-machine.md +155 -0
  37. package/references/02-project-config.md +217 -0
  38. package/references/03-readme-quality.md +136 -0
  39. package/references/04-supply-chain.md +147 -0
  40. package/references/05-evidence-and-errors.md +164 -0
  41. package/references/06-adapter-contract.md +178 -0
  42. package/schemas/.render-manifest.json +37 -0
  43. package/schemas/approval-record.schema.json +115 -0
  44. package/schemas/artifact-lock.schema.json +111 -0
  45. package/schemas/artifact-plan.schema.json +52 -0
  46. package/schemas/artifact-policy.schema.json +76 -0
  47. package/schemas/evidence-event.schema.json +89 -0
  48. package/schemas/release-plan.schema.json +369 -0
  49. package/schemas/release-project.schema.json +359 -0
  50. package/schemas/release-run.schema.json +195 -0
  51. package/skills/release-assess/SKILL.md +52 -0
  52. package/skills/release-help/SKILL.md +60 -0
  53. package/skills/release-prepare/SKILL.md +71 -0
  54. package/skills/release-publish/SKILL.md +55 -0
  55. package/skills/release-reconcile/SKILL.md +73 -0
  56. package/skills/release-verify/SKILL.md +70 -0
  57. package/skills-src/release-assess/SKILL.md +52 -0
  58. package/skills-src/release-help/SKILL.md +60 -0
  59. package/skills-src/release-prepare/SKILL.md +71 -0
  60. package/skills-src/release-publish/SKILL.md +55 -0
  61. package/skills-src/release-reconcile/SKILL.md +73 -0
  62. package/skills-src/release-verify/SKILL.md +70 -0
  63. package/src/adapters/contract.mjs +214 -0
  64. package/src/adapters/git-github.mjs +214 -0
  65. package/src/adapters/npm.mjs +947 -0
  66. package/src/adapters/plugin-marketplace.mjs +1365 -0
  67. package/src/adapters/push-snapshot.mjs +216 -0
  68. package/src/artifacts/adoption.mjs +743 -0
  69. package/src/artifacts/artifact-plan.mjs +162 -0
  70. package/src/artifacts/entry.mjs +240 -0
  71. package/src/artifacts/git-authority.mjs +637 -0
  72. package/src/artifacts/graph.mjs +189 -0
  73. package/src/artifacts/inspect.mjs +520 -0
  74. package/src/artifacts/inventory.mjs +192 -0
  75. package/src/artifacts/merge/binary.mjs +77 -0
  76. package/src/artifacts/merge/entry-merge.mjs +228 -0
  77. package/src/artifacts/merge/json.mjs +641 -0
  78. package/src/artifacts/merge/markdown.mjs +246 -0
  79. package/src/artifacts/merge/regions.mjs +156 -0
  80. package/src/artifacts/merge/text.mjs +432 -0
  81. package/src/artifacts/merge/tree.mjs +202 -0
  82. package/src/artifacts/merge/yaml.mjs +669 -0
  83. package/src/artifacts/path-key.mjs +94 -0
  84. package/src/artifacts/policy.mjs +319 -0
  85. package/src/artifacts/producer-registry.mjs +439 -0
  86. package/src/artifacts/project-lock.mjs +732 -0
  87. package/src/artifacts/resolution.mjs +658 -0
  88. package/src/artifacts/safe-fs-backend-internal.mjs +680 -0
  89. package/src/artifacts/safe-fs.mjs +72 -0
  90. package/src/artifacts/state.mjs +495 -0
  91. package/src/artifacts/transaction-journal.mjs +983 -0
  92. package/src/artifacts/transaction.mjs +1361 -0
  93. package/src/commands/approve.mjs +280 -0
  94. package/src/commands/artifacts.mjs +627 -0
  95. package/src/commands/assess.mjs +838 -0
  96. package/src/commands/prepare.mjs +1377 -0
  97. package/src/commands/publish.mjs +883 -0
  98. package/src/commands/reconcile.mjs +1255 -0
  99. package/src/commands/verify.mjs +915 -0
  100. package/src/core/approval.mjs +332 -0
  101. package/src/core/baseline.mjs +272 -0
  102. package/src/core/blackbox-hard-gates.mjs +142 -0
  103. package/src/core/config.mjs +448 -0
  104. package/src/core/digest.mjs +90 -0
  105. package/src/core/errors.mjs +113 -0
  106. package/src/core/evidence.mjs +167 -0
  107. package/src/core/hooks.mjs +241 -0
  108. package/src/core/node-version.mjs +64 -0
  109. package/src/core/plan.mjs +735 -0
  110. package/src/core/previous-public-baseline.mjs +204 -0
  111. package/src/core/run.mjs +681 -0
  112. package/src/core/state-machine.mjs +76 -0
  113. package/src/core/version-consistency.mjs +111 -0
  114. package/src/producers/build-adapters.mjs +231 -0
  115. package/src/producers/render-public-assets.mjs +152 -0
  116. package/src/producers/sync-skills.mjs +96 -0
  117. package/src/readme/contract.mjs +297 -0
  118. package/src/readme/examples.mjs +288 -0
  119. package/src/readme/parity.mjs +122 -0
  120. package/src/snapshot/export.mjs +99 -0
  121. package/src/snapshot/frozen.mjs +401 -0
  122. package/src/snapshot/manifest.mjs +207 -0
  123. package/src/snapshot/public-map.mjs +1459 -0
  124. package/src/snapshot/public-path.mjs +110 -0
  125. package/src/snapshot/scan.mjs +419 -0
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Approve command: record approval for a frozen release plan.
3
+ *
4
+ * Reads a frozen release plan, validates its digest against the expected value,
5
+ * constructs an approval record, validates it, and writes it to disk.
6
+ *
7
+ * Approval invariants:
8
+ * - `planDigest` must match the actual plan digest (otherwise PLAN_DIGEST_MISMATCH).
9
+ * - `approvedActions` must be explicit (no wildcards).
10
+ * - Approval expires after a default of 24 hours.
11
+ * - If the plan's baseline has changed since freeze, approval is invalidated.
12
+ *
13
+ * @module commands/approve
14
+ */
15
+
16
+ import { readFile, writeFile } from 'node:fs/promises';
17
+ import { resolve, dirname, basename } from 'node:path';
18
+
19
+ import {
20
+ assertAuthorityFileTarget,
21
+ assertImmutablePlanAuthority,
22
+ computePlanDigest,
23
+ prepareAuthorityDirectory,
24
+ validatePlan,
25
+ validatePlanActionCompleteness,
26
+ } from '../core/plan.mjs';
27
+ import { ReleaseError, PLAN_DIGEST_MISMATCH, GATE_FAILED } from '../core/errors.mjs';
28
+ import { computeApprovalDigest, validateApprovalRecordSchema } from '../core/approval.mjs';
29
+ import { WORKSPACE_DIGEST_ALGORITHM } from '../core/baseline.mjs';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Default clock
33
+ // ---------------------------------------------------------------------------
34
+
35
+ function defaultClock() {
36
+ return new Date().toISOString();
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Public API
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Approve a frozen release plan.
45
+ *
46
+ * Steps:
47
+ * 1. Read the plan from `planPath`.
48
+ * 2. Compute the plan's actual digest.
49
+ * 3. Compare against `expectedDigest`; throw PLAN_DIGEST_MISMATCH if different.
50
+ * 4. Validate the plan against the release-plan schema.
51
+ * 5. Build the approval record with explicit action IDs, actor, and timestamps.
52
+ * 6. Validate the approval record against the approval-record schema.
53
+ * 7. Write the approval record to disk.
54
+ *
55
+ * @param {Object} options
56
+ * @param {string} options.planPath - Absolute path to the frozen release plan.
57
+ * @param {string} options.expectedDigest - Expected SHA-256 digest of the plan.
58
+ * @param {string} options.actor - Identity of the approver.
59
+ * @param {number} [options.expiresInMs=86400000] - Approval validity in ms (default 24h).
60
+ * @param {() => string} [options.clock] - Clock function returning ISO-8601 strings.
61
+ * @param {string} [options.outputPath] - Path to write the approval record.
62
+ * Defaults to `<releaseDir>/approval-record.json`; a digest-addressed copy is
63
+ * preserved at `<releaseDir>/approvals/<planDigest>/<approvalDigest>.json`.
64
+ *
65
+ * @returns {Promise<object>} The validated ApprovalRecord.
66
+ *
67
+ * @throws {ReleaseError} PLAN_DIGEST_MISMATCH if the plan digest does not match.
68
+ * @throws {ReleaseError} GATE_FAILED on schema validation or other gate failures.
69
+ */
70
+ export async function approvePlan(options) {
71
+ const {
72
+ planPath,
73
+ expectedDigest,
74
+ actor,
75
+ expiresInMs = 24 * 60 * 60 * 1000, // 24 hours
76
+ clock,
77
+ outputPath,
78
+ } = options ?? {};
79
+
80
+ const clockFn = typeof clock === 'function' ? clock : defaultClock;
81
+
82
+ // --- Validate required parameters ---
83
+ if (!planPath || typeof planPath !== 'string') {
84
+ throw new ReleaseError(PLAN_DIGEST_MISMATCH, 'planPath must be a non-empty string');
85
+ }
86
+ if (!expectedDigest || typeof expectedDigest !== 'string') {
87
+ throw new ReleaseError(PLAN_DIGEST_MISMATCH, 'expectedDigest must be a non-empty string');
88
+ }
89
+ if (!actor || typeof actor !== 'string') {
90
+ throw new ReleaseError(GATE_FAILED, 'actor must be a non-empty string');
91
+ }
92
+
93
+ // --- Step 1: Read the plan ---
94
+ let planRaw;
95
+ try {
96
+ planRaw = await readFile(planPath, 'utf8');
97
+ } catch (err) {
98
+ throw new ReleaseError(
99
+ GATE_FAILED,
100
+ `cannot read release plan: ${err.message}`,
101
+ { planPath, cause: err.code },
102
+ );
103
+ }
104
+
105
+ let plan;
106
+ try {
107
+ plan = JSON.parse(planRaw);
108
+ } catch (err) {
109
+ throw new ReleaseError(
110
+ GATE_FAILED,
111
+ `release plan is not valid JSON: ${err.message}`,
112
+ { planPath },
113
+ );
114
+ }
115
+
116
+ // --- Step 2: Compute actual plan digest ---
117
+ // The plan was written by writePlanAtomic, which embeds the digest.
118
+ // We recompute from the stored content minus the digest field.
119
+ const actualDigest = computePlanDigest(plan);
120
+ assertImmutablePlanAuthority(planPath, plan);
121
+
122
+ // --- Step 3: Compare digests ---
123
+ if (actualDigest !== expectedDigest) {
124
+ throw new ReleaseError(
125
+ PLAN_DIGEST_MISMATCH,
126
+ `plan digest mismatch: expected ${expectedDigest.slice(0, 16)}..., got ${actualDigest.slice(0, 16)}...`,
127
+ { expectedDigest, actualDigest },
128
+ );
129
+ }
130
+
131
+ // --- Step 4: Validate the plan ---
132
+ validatePlan(plan);
133
+
134
+ if (
135
+ plan.production?.mode === 'github-npm-v1' &&
136
+ plan.baseline?.workspaceDigestAlgorithm !== WORKSPACE_DIGEST_ALGORITHM
137
+ ) {
138
+ throw new ReleaseError(
139
+ GATE_FAILED,
140
+ `production plan workspace digest algorithm is missing or obsolete; re-run prepare (expected ${WORKSPACE_DIGEST_ALGORITHM})`,
141
+ { expected: WORKSPACE_DIGEST_ALGORITHM, actual: plan.baseline?.workspaceDigestAlgorithm ?? null },
142
+ );
143
+ }
144
+
145
+ // --- Step 4b: Validate action completeness ---
146
+ const completenessResult = validatePlanActionCompleteness(plan);
147
+ if (!completenessResult.passed) {
148
+ throw new ReleaseError(
149
+ GATE_FAILED,
150
+ `plan action completeness gate failed: ${completenessResult.details.failures.join('; ')}`,
151
+ { failures: completenessResult.details.failures },
152
+ );
153
+ }
154
+
155
+ // --- Step 5a: Validate expiresInMs ---
156
+ if (typeof expiresInMs !== 'number' || !Number.isFinite(expiresInMs) || expiresInMs <= 0) {
157
+ throw new ReleaseError(
158
+ GATE_FAILED,
159
+ 'expiresInMs must be a positive finite number',
160
+ { expiresInMs },
161
+ );
162
+ }
163
+ const MAX_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
164
+ if (expiresInMs > MAX_EXPIRY_MS) {
165
+ throw new ReleaseError(
166
+ GATE_FAILED,
167
+ `expiresInMs must not exceed 24 hours (${MAX_EXPIRY_MS}ms), got ${expiresInMs}ms`,
168
+ { expiresInMs },
169
+ );
170
+ }
171
+
172
+ // --- Step 5b: Materialize every unit version into the approval authority ---
173
+ const units = plan.units ?? [];
174
+ const versions = new Set();
175
+ const unitVersions = {};
176
+ for (const unit of units) {
177
+ if (!unit.targetVersion || typeof unit.targetVersion !== 'string' || unit.targetVersion.trim() === '') {
178
+ throw new ReleaseError(
179
+ GATE_FAILED,
180
+ `unit "${unit.id ?? '(unknown)'}" is missing targetVersion; all units must have a non-empty targetVersion`,
181
+ { unitId: unit.id },
182
+ );
183
+ }
184
+ versions.add(unit.targetVersion);
185
+ unitVersions[unit.id] = unit.targetVersion;
186
+ }
187
+ const targetVersion = versions.size === 1 ? units[0]?.targetVersion : undefined;
188
+
189
+ // --- Step 5c: Build approval record ---
190
+ const approvedAt = clockFn();
191
+ const approvedAtDate = new Date(approvedAt);
192
+ if (Number.isNaN(approvedAtDate.getTime())) {
193
+ throw new ReleaseError(
194
+ GATE_FAILED,
195
+ `invalid approvedAt timestamp: "${approvedAt}"`,
196
+ { approvedAt },
197
+ );
198
+ }
199
+ const expiresAt = new Date(approvedAtDate.getTime() + expiresInMs).toISOString();
200
+
201
+ // Collect all external action IDs (explicit, no wildcards)
202
+ const approvedActions = (plan.externalActions ?? []).map((a) => a.id);
203
+
204
+ // Build baseline with workspaceDigest when plan has it
205
+ const baseline = {
206
+ gitTreeHash: plan.baseline.gitTreeHash,
207
+ };
208
+ if (plan.baseline.workspaceDigest) {
209
+ baseline.workspaceDigest = plan.baseline.workspaceDigest;
210
+ }
211
+ if (plan.baseline.workspaceDigestAlgorithm) {
212
+ baseline.workspaceDigestAlgorithm = plan.baseline.workspaceDigestAlgorithm;
213
+ }
214
+
215
+ const approvalRecord = {
216
+ planDigest: actualDigest,
217
+ baseline,
218
+ unitVersions,
219
+ ...(targetVersion ? { targetVersion } : {}),
220
+ approvedActions,
221
+ actor,
222
+ approvedAt,
223
+ expiresAt,
224
+ };
225
+
226
+ // --- Step 6: Validate approval record ---
227
+ validateApprovalRecordSchema(approvalRecord);
228
+
229
+ // --- Step 7: Preserve a digest-addressed authority and update convenience copy ---
230
+ const planDir = dirname(resolve(planPath));
231
+ const releaseDir = basename(planDir) === 'plans' && basename(planPath) === `${actualDigest}.json`
232
+ ? dirname(planDir)
233
+ : planDir;
234
+ if (
235
+ plan.production?.mode === 'github-npm-v1' &&
236
+ outputPath &&
237
+ resolve(outputPath) !== resolve(releaseDir, 'approval-record.json')
238
+ ) {
239
+ throw new ReleaseError(
240
+ GATE_FAILED,
241
+ 'production approve requires the canonical approval-record.json alias next to the immutable plan authority; custom --output is supported only outside production',
242
+ { outputPath: resolve(outputPath), expected: resolve(releaseDir, 'approval-record.json') },
243
+ );
244
+ }
245
+ const json = JSON.stringify(approvalRecord, null, 2);
246
+ const approvalDigest = computeApprovalDigest(json);
247
+ const immutableApprovalPath = resolve(
248
+ releaseDir,
249
+ 'approvals',
250
+ actualDigest,
251
+ `${approvalDigest}.json`,
252
+ );
253
+ await prepareAuthorityDirectory(dirname(immutableApprovalPath));
254
+ await assertAuthorityFileTarget(immutableApprovalPath);
255
+ try {
256
+ await writeFile(immutableApprovalPath, json, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
257
+ } catch (error) {
258
+ if (error.code !== 'EEXIST') throw error;
259
+ const existing = await readFile(immutableApprovalPath, 'utf8');
260
+ if (existing !== json) {
261
+ throw new ReleaseError(
262
+ GATE_FAILED,
263
+ 'approval authority digest collision: existing bytes differ',
264
+ { planDigest: actualDigest, approvalDigest, immutableApprovalPath },
265
+ );
266
+ }
267
+ }
268
+
269
+ const writePath = outputPath ?? resolve(releaseDir, 'approval-record.json');
270
+ await prepareAuthorityDirectory(dirname(writePath));
271
+ await assertAuthorityFileTarget(writePath);
272
+ await writeFile(writePath, json, 'utf8');
273
+
274
+ return Object.freeze({
275
+ ...approvalRecord,
276
+ approvalDigest,
277
+ approvalPath: immutableApprovalPath,
278
+ latestApprovalPath: writePath,
279
+ });
280
+ }