create-agdf 0.14.3 → 0.14.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 (80) hide show
  1. package/README.md +34 -2
  2. package/generated/.opencode/contracts/interaction.md +2 -2
  3. package/generated/.opencode/skills/agdf-gate-check/SKILL.md +6 -4
  4. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  5. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  6. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  7. package/generated/plugins/agdf/meta/contracts/interaction.md +2 -2
  8. package/generated/plugins/agdf/meta/copilot-payload-baseline.json +3 -3
  9. package/generated/plugins/agdf/meta/distribution-profile-history.json +125 -0
  10. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  11. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/command-registry.js +8 -1
  12. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/parse-args.js +3 -0
  13. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/doctor.js +13 -0
  14. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-check.js +64 -13
  15. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/run-state.js +6 -0
  16. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-identity.js +9 -0
  17. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-state-parser.js +4 -2
  18. package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +57 -7
  19. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +46 -3
  20. package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
  21. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  22. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +2 -0
  23. package/generated/plugins/agdf/skills/gate-check/SKILL.md +6 -4
  24. package/generated/plugins/agdf/submission/openai/availability.md +1 -1
  25. package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
  26. package/generated/plugins/agdf/submission/openai/release-notes.md +1 -1
  27. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
  28. package/generated/plugins/copilot/agdf/.agdf-payload-inventory.json +55 -35
  29. package/generated/plugins/copilot/agdf/copilot-skills/agdf-gate-check/SKILL.md +6 -4
  30. package/generated/plugins/copilot/agdf/copilot-skills/contracts/interaction.md +2 -2
  31. package/generated/plugins/copilot/agdf/meta/agdf-plugin.definition.json +1 -1
  32. package/generated/plugins/copilot/agdf/meta/distribution-profile-history.json +125 -0
  33. package/generated/plugins/copilot/agdf/plugin.json +1 -1
  34. package/generated/plugins/copilot/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  35. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/cli/command-registry.js +8 -1
  36. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/cli/parse-args.js +3 -0
  37. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/doctor.js +13 -0
  38. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/gate-check.js +64 -13
  39. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/run-state.js +6 -0
  40. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-state/run-identity.js +9 -0
  41. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-state/run-state-parser.js +4 -2
  42. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/interaction-presentation.js +57 -7
  43. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +46 -3
  44. package/generated/plugins/copilot/agdf/runtime/create-agdf/package.json +1 -1
  45. package/generated/plugins/copilot/agdf/runtime/runtime-manifest.json +2 -2
  46. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
  47. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +1 -1
  48. package/generated/submissions/openai/agdf/meta/contracts/interaction.md +2 -2
  49. package/generated/submissions/openai/agdf/meta/copilot-payload-baseline.json +3 -3
  50. package/generated/submissions/openai/agdf/meta/distribution-profile-history.json +125 -0
  51. package/generated/submissions/openai/agdf/skills/gate-check/SKILL.md +6 -4
  52. package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
  53. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
  54. package/generated/submissions/openai/agdf/submission/openai/inventory.json +16 -12
  55. package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
  56. package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
  57. package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
  58. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +1 -1
  59. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
  60. package/lib/cli/application.js +24 -6
  61. package/lib/cli/command-registry.js +8 -1
  62. package/lib/cli/parse-args.js +3 -0
  63. package/lib/control-evaluation/doctor.js +13 -0
  64. package/lib/control-evaluation/gate-check.js +64 -13
  65. package/lib/control-evaluation/run-state.js +6 -0
  66. package/lib/control-state/run-identity.js +9 -0
  67. package/lib/control-state/run-state-parser.js +4 -2
  68. package/lib/installers/claude-cache-recovery.js +76 -0
  69. package/lib/installers/copilot-settings.js +146 -16
  70. package/lib/installers/local-marketplace.js +169 -14
  71. package/lib/installers/plugin-installers.js +96 -9
  72. package/lib/interaction-presentation.js +57 -7
  73. package/lib/lifecycle/operations.js +28 -4
  74. package/lib/lifecycle/result.js +4 -4
  75. package/lib/release/profile-history.js +222 -0
  76. package/lib/release/version-bump.js +527 -0
  77. package/lib/release/version-coherence.js +92 -38
  78. package/lib/runtime/distribution-profile-history.js +173 -0
  79. package/lib/runtime/plugin-provenance.js +46 -3
  80. package/package.json +7 -3
@@ -0,0 +1,527 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ renameSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import {
13
+ canonicalDistributionProfileDigest,
14
+ canonicalDistributionProfileEntryDigest,
15
+ classifyHistoricalDistributionProfile,
16
+ validateDistributionProfileHistory,
17
+ } from "../runtime/distribution-profile-history.js";
18
+ import { WRITABLE_RELEASE_VERSION_SURFACES } from "./version-coherence.js";
19
+
20
+ const HISTORY_PATH = "plugin/meta/distribution-profile-history.json";
21
+ const JOURNAL_PATH = ".agdf/release-bump-transaction.json";
22
+ const COMMITTED_PATH = ".agdf/release-bump-transaction.committed.json";
23
+ const JOURNAL_SCHEMA_VERSION = 1;
24
+ const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
25
+
26
+ const defaultFs = {
27
+ existsSync,
28
+ lstatSync,
29
+ readFileSync,
30
+ realpathSync,
31
+ renameSync,
32
+ rmSync,
33
+ writeFileSync,
34
+ };
35
+
36
+ function failure(code, detail, extra = {}) {
37
+ const error = new Error(`${code}: ${detail}`);
38
+ error.code = code;
39
+ Object.assign(error, extra);
40
+ return error;
41
+ }
42
+
43
+ function digestBytes(content) {
44
+ return createHash("sha256").update(content).digest("hex");
45
+ }
46
+
47
+ function exactObjectKeys(value, keys) {
48
+ return value && typeof value === "object" && !Array.isArray(value)
49
+ && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
50
+ }
51
+
52
+ function sortedObject(value) {
53
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
54
+ }
55
+
56
+ function parseSemver(value) {
57
+ const match = typeof value === "string" ? value.match(SEMVER) : null;
58
+ if (!match) throw failure("release_version_bump_invalid", `invalid target version ${JSON.stringify(value)}`);
59
+ return {
60
+ raw: value,
61
+ core: match.slice(1, 4).map(Number),
62
+ prerelease: match[4]?.split(".") ?? [],
63
+ };
64
+ }
65
+
66
+ function compareSemver(leftValue, rightValue) {
67
+ const left = parseSemver(leftValue);
68
+ const right = parseSemver(rightValue);
69
+ for (let index = 0; index < 3; index += 1) {
70
+ if (left.core[index] !== right.core[index]) return left.core[index] - right.core[index];
71
+ }
72
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
73
+ return left.prerelease.length === right.prerelease.length ? 0 : (left.prerelease.length === 0 ? 1 : -1);
74
+ }
75
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
76
+ for (let index = 0; index < length; index += 1) {
77
+ const a = left.prerelease[index];
78
+ const b = right.prerelease[index];
79
+ if (a === undefined || b === undefined) return a === b ? 0 : (a === undefined ? -1 : 1);
80
+ if (a === b) continue;
81
+ const aNumeric = /^\d+$/.test(a);
82
+ const bNumeric = /^\d+$/.test(b);
83
+ if (aNumeric && bNumeric) return Number(a) - Number(b);
84
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
85
+ return a.localeCompare(b);
86
+ }
87
+ return 0;
88
+ }
89
+
90
+ function groupedWritableSurfaces() {
91
+ const grouped = new Map();
92
+ for (const surface of WRITABLE_RELEASE_VERSION_SURFACES) {
93
+ const list = grouped.get(surface.relativePath) ?? [];
94
+ list.push(surface);
95
+ grouped.set(surface.relativePath, list);
96
+ }
97
+ return grouped;
98
+ }
99
+
100
+ export function releaseBumpTargetPaths() {
101
+ return [...groupedWritableSurfaces().keys(), HISTORY_PATH].sort();
102
+ }
103
+
104
+ function readRequired(fs, repoRoot, relativePath) {
105
+ const path = join(repoRoot, relativePath);
106
+ if (!fs.existsSync(path)) throw failure("release_version_bump_invalid", `missing required target ${relativePath}`);
107
+ const stat = fs.lstatSync(path);
108
+ if (!stat.isFile() || stat.isSymbolicLink()) {
109
+ throw failure("release_version_bump_invalid", `target is not a regular owned file: ${relativePath}`);
110
+ }
111
+ return fs.readFileSync(path, "utf8");
112
+ }
113
+
114
+ function applySurfaces(content, surfaces, currentVersion, nextVersion, relativePath) {
115
+ let nextContent = content;
116
+ for (const surface of surfaces) {
117
+ let observed;
118
+ try {
119
+ observed = surface.readContent(nextContent);
120
+ } catch (error) {
121
+ throw failure("release_version_bump_invalid", `${relativePath} cannot be read: ${error.message}`);
122
+ }
123
+ if (observed !== currentVersion) {
124
+ throw failure("release_version_bump_invalid", `${relativePath} has ${observed ?? "no version"}; expected ${currentVersion}`);
125
+ }
126
+ try {
127
+ nextContent = surface.updateContent(nextContent, nextVersion);
128
+ } catch (error) {
129
+ throw failure("release_version_bump_invalid", `${relativePath} cannot be planned: ${error.message}`);
130
+ }
131
+ }
132
+ for (const surface of surfaces) {
133
+ if (surface.readContent(nextContent) !== nextVersion) {
134
+ throw failure("release_version_bump_invalid", `${relativePath} target validation failed`);
135
+ }
136
+ }
137
+ return nextContent;
138
+ }
139
+
140
+ function contractForProfiles(catalogue, distributionProfiles) {
141
+ const contractDigest = canonicalDistributionProfileDigest(distributionProfiles);
142
+ const match = Object.entries(catalogue.contracts)
143
+ .find(([, contract]) => contract.contract_digest === contractDigest);
144
+ return { contractDigest, contractId: match?.[0] ?? null };
145
+ }
146
+
147
+ function addReleaseRecord({ catalogue, version, distributionProfiles, acceptedContractDigest }) {
148
+ if (Object.hasOwn(catalogue.releases, version)) {
149
+ throw failure("release_version_bump_invalid", `release ${version} already exists in profile history`);
150
+ }
151
+ const next = structuredClone(catalogue);
152
+ const { contractDigest, contractId: existingContractId } = contractForProfiles(next, distributionProfiles);
153
+ let contractId = existingContractId;
154
+ if (!contractId) {
155
+ if (acceptedContractDigest !== contractDigest) {
156
+ throw failure(
157
+ "profile_history_contract_review_required",
158
+ `changed distribution profile contract requires exact digest ${contractDigest}`,
159
+ { proposedDigest: contractDigest },
160
+ );
161
+ }
162
+ contractId = `profile-${contractDigest.slice(0, 16)}`;
163
+ if (Object.hasOwn(next.contracts, contractId)) {
164
+ throw failure("release_version_bump_invalid", `contract identifier collision at ${contractId}`);
165
+ }
166
+ next.contracts[contractId] = {
167
+ distribution_profiles: structuredClone(distributionProfiles),
168
+ contract_digest: contractDigest,
169
+ };
170
+ }
171
+ const release = {
172
+ contract_id: contractId,
173
+ provenance_schema_version: 1,
174
+ profile_id: "runtime-plugin",
175
+ entry_digest: canonicalDistributionProfileEntryDigest({
176
+ version,
177
+ contract_id: contractId,
178
+ contract_digest: contractDigest,
179
+ }),
180
+ status: "supported",
181
+ };
182
+ next.contracts = sortedObject(next.contracts);
183
+ next.releases = sortedObject({ ...next.releases, [version]: release });
184
+ if (validateDistributionProfileHistory(next).status !== "matched") {
185
+ throw failure("release_version_bump_invalid", "planned profile history is invalid");
186
+ }
187
+ return next;
188
+ }
189
+
190
+ export function planReleaseVersionBump({
191
+ repoRoot,
192
+ nextVersion,
193
+ acceptedContractDigest,
194
+ fs = defaultFs,
195
+ } = {}) {
196
+ repoRoot = resolve(repoRoot);
197
+ parseSemver(nextVersion);
198
+ const grouped = groupedWritableSurfaces();
199
+ const originalContents = new Map();
200
+ for (const relativePath of [...grouped.keys(), HISTORY_PATH]) {
201
+ originalContents.set(relativePath, readRequired(fs, repoRoot, relativePath));
202
+ }
203
+
204
+ const definitionPath = "plugin/meta/agdf-plugin.definition.json";
205
+ const currentDefinition = JSON.parse(originalContents.get(definitionPath));
206
+ const currentVersion = currentDefinition.version;
207
+ parseSemver(currentVersion);
208
+ const comparison = compareSemver(nextVersion, currentVersion);
209
+ if (comparison < 0) {
210
+ throw failure("release_version_bump_invalid", `target ${nextVersion} is older than current ${currentVersion}`);
211
+ }
212
+
213
+ let catalogue;
214
+ try {
215
+ catalogue = JSON.parse(originalContents.get(HISTORY_PATH));
216
+ } catch {
217
+ throw failure("release_version_bump_invalid", "profile history is not valid JSON");
218
+ }
219
+ if (validateDistributionProfileHistory(catalogue).status !== "matched") {
220
+ throw failure("release_version_bump_invalid", "existing profile history is invalid");
221
+ }
222
+
223
+ const currentClassification = classifyHistoricalDistributionProfile({
224
+ catalogue,
225
+ version: currentVersion,
226
+ distributionProfiles: currentDefinition.distributionProfiles,
227
+ });
228
+ if (comparison === 0) {
229
+ if (currentClassification.reason !== "historical_contract_unsupported") {
230
+ throw failure("release_version_bump_invalid", `same-version target ${nextVersion} is not a missing-record repair`);
231
+ }
232
+ } else if (!Object.hasOwn(catalogue.releases, currentVersion)) {
233
+ throw failure("release_version_bump_invalid", `current release ${currentVersion} must have a catalogue record before advancing`);
234
+ }
235
+
236
+ const plannedContents = new Map();
237
+ for (const [relativePath, surfaces] of grouped) {
238
+ const originalContent = originalContents.get(relativePath);
239
+ if (comparison === 0) {
240
+ for (const surface of surfaces) {
241
+ if (surface.readContent(originalContent) !== currentVersion) {
242
+ throw failure("release_version_bump_invalid", `${relativePath} is not coherent for same-version repair`);
243
+ }
244
+ }
245
+ plannedContents.set(relativePath, originalContent);
246
+ } else {
247
+ plannedContents.set(
248
+ relativePath,
249
+ applySurfaces(originalContent, surfaces, currentVersion, nextVersion, relativePath),
250
+ );
251
+ }
252
+ }
253
+ const plannedDefinition = JSON.parse(plannedContents.get(definitionPath));
254
+ const nextCatalogue = addReleaseRecord({
255
+ catalogue,
256
+ version: nextVersion,
257
+ distributionProfiles: plannedDefinition.distributionProfiles,
258
+ acceptedContractDigest,
259
+ });
260
+ plannedContents.set(HISTORY_PATH, `${JSON.stringify(nextCatalogue, null, 2)}\n`);
261
+
262
+ const plannedClassification = classifyHistoricalDistributionProfile({
263
+ catalogue: nextCatalogue,
264
+ version: nextVersion,
265
+ distributionProfiles: plannedDefinition.distributionProfiles,
266
+ });
267
+ if (plannedClassification.status !== "matched") {
268
+ throw failure("release_version_bump_invalid", "planned current release snapshot does not match");
269
+ }
270
+
271
+ const entries = [...plannedContents.entries()]
272
+ .filter(([relativePath, targetContent]) => targetContent !== originalContents.get(relativePath))
273
+ .sort(([left], [right]) => left.localeCompare(right))
274
+ .map(([relativePath, targetContent]) => ({
275
+ relativePath,
276
+ originalContent: originalContents.get(relativePath),
277
+ targetContent,
278
+ originalDigest: digestBytes(originalContents.get(relativePath)),
279
+ targetDigest: digestBytes(targetContent),
280
+ }));
281
+ return {
282
+ repoRoot,
283
+ currentVersion,
284
+ nextVersion,
285
+ mode: comparison === 0 ? "reconcile_current" : "forward_bump",
286
+ contractDigest: plannedClassification.contract_digest,
287
+ contractId: plannedClassification.contract_id,
288
+ entries,
289
+ };
290
+ }
291
+
292
+ function ownedArtifactPaths(relativePath) {
293
+ return {
294
+ stagePath: `${relativePath}.agdf-release-bump-stage`,
295
+ backupPath: `${relativePath}.agdf-release-bump-backup`,
296
+ };
297
+ }
298
+
299
+ function containedRealParent(fs, repoRoot, relativePath) {
300
+ if (isAbsolute(relativePath) || relativePath.split(/[\\/]/).includes("..")) return false;
301
+ const root = fs.realpathSync(repoRoot);
302
+ const parent = fs.realpathSync(dirname(join(repoRoot, relativePath)));
303
+ const offset = relative(root, parent);
304
+ return offset === "" || (!offset.startsWith(`..${sep}`) && offset !== ".." && !isAbsolute(offset));
305
+ }
306
+
307
+ function assertRegularIfPresent(fs, absolutePath, label) {
308
+ if (!fs.existsSync(absolutePath)) return;
309
+ const stat = fs.lstatSync(absolutePath);
310
+ if (!stat.isFile() || stat.isSymbolicLink()) {
311
+ throw failure("release_version_bump_recovery_invalid", `${label} is not a regular file`);
312
+ }
313
+ }
314
+
315
+ function journalPaths(repoRoot) {
316
+ return {
317
+ journal: join(repoRoot, JOURNAL_PATH),
318
+ committed: join(repoRoot, COMMITTED_PATH),
319
+ temporary: join(repoRoot, `${JOURNAL_PATH}.tmp`),
320
+ };
321
+ }
322
+
323
+ function validateJournal({ repoRoot, journal, fs }) {
324
+ if (!exactObjectKeys(journal, ["schema_version", "transaction_id", "entries"])
325
+ || journal.schema_version !== JOURNAL_SCHEMA_VERSION
326
+ || !/^[a-f0-9-]{36}$/.test(journal.transaction_id ?? "")
327
+ || !Array.isArray(journal.entries)) {
328
+ throw failure("release_version_bump_recovery_invalid", "journal schema is invalid");
329
+ }
330
+ const allowedTargets = releaseBumpTargetPaths();
331
+ const observedTargets = journal.entries.map((entry) => entry.relative_path);
332
+ if (observedTargets.length === 0
333
+ || JSON.stringify(observedTargets) !== JSON.stringify([...new Set(observedTargets)].sort())
334
+ || observedTargets.some((target) => !allowedTargets.includes(target))) {
335
+ throw failure("release_version_bump_recovery_invalid", "journal target set is invalid");
336
+ }
337
+ for (const entry of journal.entries) {
338
+ if (!exactObjectKeys(entry, [
339
+ "relative_path", "stage_path", "backup_path", "original_digest", "target_digest",
340
+ ])) throw failure("release_version_bump_recovery_invalid", "journal entry schema is invalid");
341
+ const derived = ownedArtifactPaths(entry.relative_path);
342
+ if (entry.stage_path !== derived.stagePath || entry.backup_path !== derived.backupPath
343
+ || !/^[a-f0-9]{64}$/.test(entry.original_digest)
344
+ || !/^[a-f0-9]{64}$/.test(entry.target_digest)
345
+ || !containedRealParent(fs, repoRoot, entry.relative_path)) {
346
+ throw failure("release_version_bump_recovery_invalid", `journal entry is unsafe: ${entry.relative_path}`);
347
+ }
348
+ for (const [path, label] of [
349
+ [entry.relative_path, "target"], [entry.stage_path, "stage"], [entry.backup_path, "backup"],
350
+ ]) assertRegularIfPresent(fs, join(repoRoot, path), `${label} ${path}`);
351
+ }
352
+ return journal;
353
+ }
354
+
355
+ function readJournal({ repoRoot, path, fs }) {
356
+ assertRegularIfPresent(fs, path, "journal");
357
+ let value;
358
+ try {
359
+ value = JSON.parse(fs.readFileSync(path, "utf8"));
360
+ } catch {
361
+ throw failure("release_version_bump_recovery_invalid", "journal is not valid JSON");
362
+ }
363
+ return validateJournal({ repoRoot, journal: value, fs });
364
+ }
365
+
366
+ function removeOwnedFile(fs, path) {
367
+ if (fs.existsSync(path)) {
368
+ assertRegularIfPresent(fs, path, path);
369
+ fs.rmSync(path);
370
+ }
371
+ }
372
+
373
+ function recoverJournal({ repoRoot, journal, committed, fs }) {
374
+ for (const entry of journal.entries) {
375
+ const target = join(repoRoot, entry.relative_path);
376
+ const stage = join(repoRoot, entry.stage_path);
377
+ const backup = join(repoRoot, entry.backup_path);
378
+ if (committed) {
379
+ if (!fs.existsSync(target) || digestBytes(fs.readFileSync(target)) !== entry.target_digest) {
380
+ throw failure("release_version_bump_recovery_invalid", `committed target digest mismatch: ${entry.relative_path}`);
381
+ }
382
+ if (fs.existsSync(backup) && digestBytes(fs.readFileSync(backup)) !== entry.original_digest) {
383
+ throw failure("release_version_bump_recovery_invalid", `backup digest mismatch: ${entry.relative_path}`);
384
+ }
385
+ removeOwnedFile(fs, backup);
386
+ removeOwnedFile(fs, stage);
387
+ continue;
388
+ }
389
+ if (fs.existsSync(backup)) {
390
+ if (digestBytes(fs.readFileSync(backup)) !== entry.original_digest) {
391
+ throw failure("release_version_bump_recovery_invalid", `backup digest mismatch: ${entry.relative_path}`);
392
+ }
393
+ removeOwnedFile(fs, target);
394
+ fs.renameSync(backup, target);
395
+ } else if (!fs.existsSync(target) || digestBytes(fs.readFileSync(target)) !== entry.original_digest) {
396
+ throw failure("release_version_bump_recovery_invalid", `original target evidence missing: ${entry.relative_path}`);
397
+ }
398
+ removeOwnedFile(fs, stage);
399
+ }
400
+ }
401
+
402
+ export function recoverReleaseVersionBump({ repoRoot, fs = defaultFs, requireFreshInvocation = true } = {}) {
403
+ repoRoot = resolve(repoRoot);
404
+ const paths = journalPaths(repoRoot);
405
+ if (fs.existsSync(paths.journal) && fs.existsSync(paths.committed)) {
406
+ throw failure("release_version_bump_recovery_invalid", "both active and committed journals exist");
407
+ }
408
+ const committed = fs.existsSync(paths.committed);
409
+ const journalPath = committed ? paths.committed : paths.journal;
410
+ if (!fs.existsSync(journalPath)) {
411
+ if (fs.existsSync(paths.temporary)) removeOwnedFile(fs, paths.temporary);
412
+ for (const relativePath of releaseBumpTargetPaths()) {
413
+ const { stagePath, backupPath } = ownedArtifactPaths(relativePath);
414
+ const stage = join(repoRoot, stagePath);
415
+ const backup = join(repoRoot, backupPath);
416
+ if (fs.existsSync(backup)) {
417
+ throw failure("release_version_bump_recovery_invalid", `orphan backup without journal: ${backupPath}`);
418
+ }
419
+ removeOwnedFile(fs, stage);
420
+ }
421
+ return { recovered: false };
422
+ }
423
+ const journal = readJournal({ repoRoot, path: journalPath, fs });
424
+ recoverJournal({ repoRoot, journal, committed, fs });
425
+ removeOwnedFile(fs, journalPath);
426
+ removeOwnedFile(fs, paths.temporary);
427
+ if (requireFreshInvocation) {
428
+ throw failure(
429
+ "release_version_bump_recovery_required",
430
+ `recovered interrupted ${committed ? "committed" : "active"} transaction; rerun the command`,
431
+ { recovered: true },
432
+ );
433
+ }
434
+ return { recovered: true };
435
+ }
436
+
437
+ function writeJournal({ repoRoot, journal, fs }) {
438
+ const paths = journalPaths(repoRoot);
439
+ removeOwnedFile(fs, paths.temporary);
440
+ fs.writeFileSync(paths.temporary, `${JSON.stringify(journal, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
441
+ fs.renameSync(paths.temporary, paths.journal);
442
+ }
443
+
444
+ export function executeReleaseVersionBump({
445
+ repoRoot,
446
+ nextVersion,
447
+ acceptedContractDigest,
448
+ fs = defaultFs,
449
+ hooks = {},
450
+ } = {}) {
451
+ repoRoot = resolve(repoRoot);
452
+ recoverReleaseVersionBump({ repoRoot, fs });
453
+ const plan = planReleaseVersionBump({ repoRoot, nextVersion, acceptedContractDigest, fs });
454
+ const transactionId = randomUUID();
455
+ const entries = plan.entries.map((entry) => ({
456
+ relative_path: entry.relativePath,
457
+ ...Object.fromEntries(Object.entries(ownedArtifactPaths(entry.relativePath)).map(([key, value]) => [
458
+ key === "stagePath" ? "stage_path" : "backup_path", value,
459
+ ])),
460
+ original_digest: entry.originalDigest,
461
+ target_digest: entry.targetDigest,
462
+ }));
463
+ const journal = { schema_version: JOURNAL_SCHEMA_VERSION, transaction_id: transactionId, entries };
464
+ const paths = journalPaths(repoRoot);
465
+
466
+ try {
467
+ for (const entry of plan.entries) {
468
+ if (!containedRealParent(fs, repoRoot, entry.relativePath)) {
469
+ throw failure("release_version_bump_write_failed", `unsafe target parent: ${entry.relativePath}`);
470
+ }
471
+ const { stagePath, backupPath } = ownedArtifactPaths(entry.relativePath);
472
+ const stage = join(repoRoot, stagePath);
473
+ const backup = join(repoRoot, backupPath);
474
+ if (fs.existsSync(stage) || fs.existsSync(backup)) {
475
+ throw failure("release_version_bump_write_failed", `owned transaction file already exists for ${entry.relativePath}`);
476
+ }
477
+ fs.writeFileSync(stage, entry.targetContent, { encoding: "utf8", flag: "wx" });
478
+ if (digestBytes(fs.readFileSync(stage)) !== entry.targetDigest) {
479
+ throw failure("release_version_bump_write_failed", `staged digest mismatch: ${entry.relativePath}`);
480
+ }
481
+ }
482
+ for (const entry of plan.entries) {
483
+ const target = join(repoRoot, entry.relativePath);
484
+ assertRegularIfPresent(fs, target, `target ${entry.relativePath}`);
485
+ if (!fs.existsSync(target) || digestBytes(fs.readFileSync(target)) !== entry.originalDigest) {
486
+ throw failure("release_version_bump_write_failed", `target changed after planning: ${entry.relativePath}`);
487
+ }
488
+ }
489
+ writeJournal({ repoRoot, journal, fs });
490
+ for (const [index, entry] of plan.entries.entries()) {
491
+ const target = join(repoRoot, entry.relativePath);
492
+ const { stagePath, backupPath } = ownedArtifactPaths(entry.relativePath);
493
+ assertRegularIfPresent(fs, target, `target ${entry.relativePath}`);
494
+ if (!fs.existsSync(target) || digestBytes(fs.readFileSync(target)) !== entry.originalDigest) {
495
+ throw failure("release_version_bump_write_failed", `target changed during replacement: ${entry.relativePath}`);
496
+ }
497
+ fs.renameSync(target, join(repoRoot, backupPath));
498
+ fs.renameSync(join(repoRoot, stagePath), target);
499
+ hooks.afterReplacement?.({ index, entry });
500
+ }
501
+ for (const entry of plan.entries) {
502
+ const target = join(repoRoot, entry.relativePath);
503
+ if (digestBytes(fs.readFileSync(target)) !== entry.targetDigest) {
504
+ throw failure("release_version_bump_write_failed", `post-write digest mismatch: ${entry.relativePath}`);
505
+ }
506
+ }
507
+ fs.renameSync(paths.journal, paths.committed);
508
+ recoverReleaseVersionBump({ repoRoot, fs, requireFreshInvocation: false });
509
+ return plan;
510
+ } catch (error) {
511
+ try {
512
+ if (fs.existsSync(paths.journal) || fs.existsSync(paths.committed)) {
513
+ recoverReleaseVersionBump({ repoRoot, fs, requireFreshInvocation: false });
514
+ } else {
515
+ for (const entry of plan.entries) {
516
+ removeOwnedFile(fs, join(repoRoot, ownedArtifactPaths(entry.relativePath).stagePath));
517
+ }
518
+ }
519
+ } catch (recoveryError) {
520
+ if (recoveryError.code === "release_version_bump_recovery_invalid") throw recoveryError;
521
+ throw failure("release_version_bump_write_failed", `${error.message}; recovery failed: ${recoveryError.message}`);
522
+ }
523
+ if (error.code === "profile_history_contract_review_required"
524
+ || error.code === "release_version_bump_invalid") throw error;
525
+ throw failure("release_version_bump_write_failed", error.message);
526
+ }
527
+ }
@@ -1,34 +1,78 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
 
4
- function jsonVersion(relativePath, select = (value) => value.version) {
5
- return {
4
+ function valueAtPath(value, path) {
5
+ return path.reduce((current, key) => current?.[key], value);
6
+ }
7
+
8
+ function setValueAtPath(value, path, nextVersion) {
9
+ let current = value;
10
+ for (const key of path.slice(0, -1)) {
11
+ if (!current?.[key] || typeof current[key] !== "object" || Array.isArray(current[key])) {
12
+ throw new Error(`missing JSON version path ${path.join(".")}`);
13
+ }
14
+ current = current[key];
15
+ }
16
+ const key = path.at(-1);
17
+ if (typeof current?.[key] !== "string") {
18
+ throw new Error(`missing JSON version path ${path.join(".")}`);
19
+ }
20
+ current[key] = nextVersion;
21
+ }
22
+
23
+ function jsonVersion(relativePath, path = ["version"], options = {}) {
24
+ return Object.freeze({
6
25
  relativePath,
7
- read: (repoRoot) => select(JSON.parse(readFileSync(join(repoRoot, relativePath), "utf8"))),
8
- };
26
+ writable: options.writable ?? false,
27
+ evidence: options.evidence ?? true,
28
+ readContent(content) {
29
+ return valueAtPath(JSON.parse(content), path);
30
+ },
31
+ updateContent(content, nextVersion) {
32
+ const value = JSON.parse(content);
33
+ setValueAtPath(value, path, nextVersion);
34
+ return `${JSON.stringify(value, null, 2)}\n`;
35
+ },
36
+ });
9
37
  }
10
38
 
11
- function textVersion(relativePath, pattern) {
12
- return {
39
+ function textVersion(relativePath, pattern, replacement, options = {}) {
40
+ return Object.freeze({
13
41
  relativePath,
14
- read: (repoRoot) => readFileSync(join(repoRoot, relativePath), "utf8").match(pattern)?.[1],
15
- };
42
+ writable: options.writable ?? false,
43
+ evidence: options.evidence ?? true,
44
+ readContent(content) {
45
+ return content.match(pattern)?.[1];
46
+ },
47
+ updateContent(content, nextVersion) {
48
+ const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
49
+ const matches = [...content.matchAll(new RegExp(pattern.source, flags))];
50
+ if (matches.length !== 1) {
51
+ throw new Error(`expected exactly one version field in ${relativePath}; found ${matches.length}`);
52
+ }
53
+ return content.replace(pattern, replacement(nextVersion));
54
+ },
55
+ });
16
56
  }
17
57
 
18
- const VERSION_SURFACES = [
19
- jsonVersion("plugin/.codex-plugin/plugin.json"),
20
- jsonVersion("plugin/.claude-plugin/plugin.json"),
21
- jsonVersion("create-agdf/package.json"),
22
- jsonVersion("agdf/package.json"),
23
- jsonVersion("agdf/package.json", (value) => value.dependencies?.["create-agdf"]),
24
- jsonVersion("pages/package.json"),
25
- jsonVersion("pages/package-lock.json"),
26
- jsonVersion("pages/package-lock.json", (value) => value.packages?.[""]?.version),
27
- textVersion("pages/src/data/site.ts", /version:\s*"([^"]+)"/),
28
- jsonVersion("plugin/submission/openai/capability-matrix.json", (value) => value.releaseVersion),
29
- jsonVersion("plugin/submission/openai/reviewer-cases.json", (value) => value.releaseVersion),
30
- textVersion("plugin/submission/openai/release-notes.md", /^# AGDF (\S+) —/m),
31
- textVersion("plugin/submission/openai/availability.md", /^- release: `([^`]+)`$/m),
58
+ const writable = Object.freeze({ writable: true });
59
+ const authority = Object.freeze({ writable: true, evidence: false });
60
+
61
+ export const RELEASE_VERSION_SURFACES = Object.freeze([
62
+ jsonVersion("plugin/meta/agdf-plugin.definition.json", ["version"], authority),
63
+ jsonVersion("plugin/.codex-plugin/plugin.json", ["version"], writable),
64
+ jsonVersion("plugin/.claude-plugin/plugin.json", ["version"], writable),
65
+ jsonVersion("create-agdf/package.json", ["version"], writable),
66
+ jsonVersion("agdf/package.json", ["version"], writable),
67
+ jsonVersion("agdf/package.json", ["dependencies", "create-agdf"], writable),
68
+ jsonVersion("pages/package.json", ["version"], writable),
69
+ jsonVersion("pages/package-lock.json", ["version"], writable),
70
+ jsonVersion("pages/package-lock.json", ["packages", "", "version"], writable),
71
+ textVersion("pages/src/data/site.ts", /version:\s*"([^"]+)"/, (version) => `version: "${version}"`, writable),
72
+ jsonVersion("plugin/submission/openai/capability-matrix.json", ["releaseVersion"], writable),
73
+ jsonVersion("plugin/submission/openai/reviewer-cases.json", ["releaseVersion"], writable),
74
+ textVersion("plugin/submission/openai/release-notes.md", /^# AGDF (\S+) —/m, (version) => `# AGDF ${version} —`, writable),
75
+ textVersion("plugin/submission/openai/availability.md", /^- release: `([^`]+)`$/m, (version) => `- release: \`${version}\``, writable),
32
76
  jsonVersion("create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json"),
33
77
  jsonVersion("create-agdf/generated/plugins/agdf/.codex-plugin/plugin.json"),
34
78
  jsonVersion("create-agdf/generated/plugins/agdf/.claude-plugin/plugin.json"),
@@ -40,25 +84,35 @@ const VERSION_SURFACES = [
40
84
  jsonVersion("create-agdf/generated/plugins/copilot/agdf/runtime/create-agdf/package.json"),
41
85
  jsonVersion("create-agdf/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json"),
42
86
  jsonVersion("create-agdf/generated/submissions/openai/agdf/.codex-plugin/plugin.json"),
43
- jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/capability-matrix.json", (value) => value.releaseVersion),
44
- jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json", (value) => value.releaseVersion),
45
- jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/listing.json", (value) => value.releaseVersion),
46
- jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/inventory.json", (value) => value.releaseVersion),
47
- jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/readiness.json", (value) => value.releaseVersion),
48
- textVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/release-notes.md", /^# AGDF (\S+) —/m),
49
- textVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/availability.md", /^- release: `([^`]+)`$/m),
50
- textVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/readiness.md", /^- release: `([^`]+)`$/m),
51
- ];
87
+ jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/capability-matrix.json", ["releaseVersion"]),
88
+ jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json", ["releaseVersion"]),
89
+ jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/listing.json", ["releaseVersion"]),
90
+ jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/inventory.json", ["releaseVersion"]),
91
+ jsonVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/readiness.json", ["releaseVersion"]),
92
+ textVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/release-notes.md", /^# AGDF (\S+) —/m, () => ""),
93
+ textVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/availability.md", /^- release: `([^`]+)`$/m, () => ""),
94
+ textVersion("create-agdf/generated/submissions/openai/agdf/submission/openai/readiness.md", /^- release: `([^`]+)`$/m, () => ""),
95
+ ]);
96
+
97
+ export const WRITABLE_RELEASE_VERSION_SURFACES = Object.freeze(
98
+ RELEASE_VERSION_SURFACES.filter((surface) => surface.writable),
99
+ );
52
100
 
53
101
  export function collectReleaseVersionEvidence({ repoRoot }) {
54
102
  repoRoot = resolve(repoRoot);
55
- const definitionPath = join(repoRoot, "plugin", "meta", "agdf-plugin.definition.json");
56
- const expectedVersion = JSON.parse(readFileSync(definitionPath, "utf8")).version;
57
- const entries = VERSION_SURFACES.map(({ relativePath, read }) => {
58
- const absolutePath = join(repoRoot, relativePath);
59
- if (!existsSync(absolutePath)) return { relativePath, actualVersion: undefined };
60
- return { relativePath, actualVersion: read(repoRoot) };
61
- });
103
+ const authoritySurface = RELEASE_VERSION_SURFACES[0];
104
+ const definitionPath = join(repoRoot, authoritySurface.relativePath);
105
+ const expectedVersion = authoritySurface.readContent(readFileSync(definitionPath, "utf8"));
106
+ const entries = RELEASE_VERSION_SURFACES
107
+ .filter((surface) => surface.evidence)
108
+ .map((surface) => {
109
+ const absolutePath = join(repoRoot, surface.relativePath);
110
+ if (!existsSync(absolutePath)) return { relativePath: surface.relativePath, actualVersion: undefined };
111
+ return {
112
+ relativePath: surface.relativePath,
113
+ actualVersion: surface.readContent(readFileSync(absolutePath, "utf8")),
114
+ };
115
+ });
62
116
  return { expectedVersion, entries };
63
117
  }
64
118