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,439 @@
1
+ /**
2
+ * Producer registry and deterministic closure runner.
3
+ *
4
+ * Provides:
5
+ * - `buildProducerGraph(policy, registry)` — re-export from graph.mjs
6
+ * - `createBuiltInProducerRegistry()` — registry of built-in producers
7
+ * - `runProducerClosure({ registry, graph, inputSnapshot, artifactIds, tempRootFactory })`
8
+ * — runs producers in topological order with deterministic dual-run verification
9
+ *
10
+ * @module artifacts/producer-registry
11
+ */
12
+
13
+ import { readFile, readdir, stat, mkdir, writeFile, rm } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { mkdtemp } from 'node:fs/promises';
16
+ import { tmpdir } from 'node:os';
17
+ import { createHash } from 'node:crypto';
18
+ import { readFileSync } from 'node:fs';
19
+
20
+ import { canonicalJson, sha256Hex } from '../core/digest.mjs';
21
+ import {
22
+ ReleaseError,
23
+ ARTIFACT_POLICY_INVALID,
24
+ PRODUCER_NONDETERMINISTIC,
25
+ PRODUCER_SCOPE_VIOLATION,
26
+ } from '../core/errors.mjs';
27
+ import { buildProducerGraph } from './graph.mjs';
28
+ import { digestEntryManifest } from './entry.mjs';
29
+
30
+ // Re-export buildProducerGraph for convenience
31
+ export { buildProducerGraph };
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Static import collector (for implementation digest)
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Recursively collect all local (relative) static import targets from an
39
+ * ES module source file. Returns absolute file paths of all reachable
40
+ * .mjs modules in the dependency closure.
41
+ *
42
+ * @param {string} filePath - Absolute path to the entry module.
43
+ * @param {Set<string>} [visited] - Already-visited paths (cycle guard).
44
+ * @returns {string[]} Absolute paths of all reachable local modules.
45
+ */
46
+ function collectStaticImports(filePath, visited = new Set()) {
47
+ const abs = new URL(`file://${filePath}`).pathname;
48
+ if (visited.has(abs)) return [];
49
+ visited.add(abs);
50
+
51
+ let source;
52
+ try {
53
+ source = readFileSync(abs, 'utf-8');
54
+ } catch {
55
+ return [abs];
56
+ }
57
+
58
+ const results = [abs];
59
+ const importRe = /(?:import|export)\s+(?:[\s\S]*?from\s+)?['"](\.[^'"]+)['"]/g;
60
+ let match;
61
+ const dir = abs.replace(/\/[^/]*$/, '');
62
+
63
+ while ((match = importRe.exec(source)) !== null) {
64
+ const spec = match[1];
65
+ let resolved = join(dir, spec);
66
+ if (!resolved.endsWith('.mjs')) resolved += '.mjs';
67
+ results.push(...collectStaticImports(resolved, visited));
68
+ }
69
+
70
+ return results;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Built-in producer registry
75
+ // ---------------------------------------------------------------------------
76
+
77
+ const BUILTIN_PRODUCERS = Object.freeze([
78
+ Object.freeze({
79
+ id: 'sync-skills',
80
+ modulePath: '../producers/sync-skills.mjs',
81
+ produceFn: 'produceSyncSkills',
82
+ digestFn: 'computeSyncSkillsDigest',
83
+ }),
84
+ Object.freeze({
85
+ id: 'build-adapters',
86
+ modulePath: '../producers/build-adapters.mjs',
87
+ produceFn: 'produceBuildAdapters',
88
+ digestFn: 'computeBuildAdaptersDigest',
89
+ }),
90
+ Object.freeze({
91
+ id: 'render-public-assets',
92
+ modulePath: '../producers/render-public-assets.mjs',
93
+ produceFn: 'produceRenderPublicAssets',
94
+ digestFn: 'computeRenderDigest',
95
+ }),
96
+ ]);
97
+
98
+ /**
99
+ * Create a fully loaded producer registry (async).
100
+ *
101
+ * Loads all built-in producer modules and computes implementation digests.
102
+ * Implementation digest covers: producer module + all static local imports
103
+ * (full dependency closure), pnpm-lock.yaml, process.version, and fixed
104
+ * locale/timezone.
105
+ *
106
+ * @returns {Promise<ProducerRegistry>}
107
+ */
108
+ export async function createBuiltInProducerRegistry() {
109
+ const producers = new Map();
110
+
111
+ // Resolve lockfile path (one level up from package root)
112
+ const lockfilePath = join(new URL('../../..', import.meta.url).pathname, 'pnpm-lock.yaml');
113
+ let lockfileBytes;
114
+ try {
115
+ lockfileBytes = await readFile(lockfilePath);
116
+ } catch {
117
+ lockfileBytes = Buffer.from('');
118
+ }
119
+
120
+ for (const def of BUILTIN_PRODUCERS) {
121
+ const moduleUrl = new URL(def.modulePath, import.meta.url);
122
+ const mod = await import(moduleUrl.href);
123
+ const produce = mod[def.produceFn];
124
+ const computeDigest = mod[def.digestFn];
125
+
126
+ if (!produce || !computeDigest) {
127
+ throw new Error(`Producer "${def.id}" missing ${def.produceFn} or ${def.digestFn}`);
128
+ }
129
+
130
+ // Collect full static dependency closure
131
+ const modulePath = new URL(moduleUrl).pathname;
132
+ const allModulePaths = collectStaticImports(modulePath);
133
+ const allModuleBytes = await Promise.all(
134
+ allModulePaths.map(async (p) => {
135
+ try { return await readFile(p); } catch { return Buffer.from(''); }
136
+ }),
137
+ );
138
+
139
+ const implementationDigest = computeDigest([...allModuleBytes, lockfileBytes]);
140
+
141
+ producers.set(def.id, Object.freeze({ produce, implementationDigest }));
142
+ }
143
+
144
+ return Object.freeze({
145
+ get(id) { return producers.get(id); },
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Compute an implementation digest from source bytes.
151
+ *
152
+ * Covers: source bytes, process.version, fixed locale/timezone.
153
+ *
154
+ * @param {Buffer[]} sourceBytes
155
+ * @returns {string}
156
+ */
157
+ export function computeImplDigest(sourceBytes) {
158
+ const h = createHash('sha256');
159
+ for (const buf of sourceBytes) h.update(buf);
160
+ h.update(`node:${process.version}`);
161
+ h.update('locale:en-US timezone:UTC');
162
+ return `sha256:${h.digest('hex')}`;
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Internal helpers
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Read all entries from a directory (recursively), PRESERVING content.
171
+ * Returns entries with relative paths and actual file bytes.
172
+ *
173
+ * @param {string} dirPath - Absolute directory path.
174
+ * @param {string} [relBase] - Relative base for entry paths.
175
+ * @returns {Promise<Array>}
176
+ */
177
+ async function readDirEntries(dirPath, relBase = '') {
178
+ const entries = [];
179
+ const items = await readdir(dirPath, { withFileTypes: true });
180
+
181
+ for (const item of items) {
182
+ if (item.name === '.git') continue;
183
+ const absPath = join(dirPath, item.name);
184
+ const relPath = relBase ? `${relBase}/${item.name}` : item.name;
185
+ const st = await stat(absPath);
186
+
187
+ if (st.isDirectory()) {
188
+ entries.push(...await readDirEntries(absPath, relPath));
189
+ } else {
190
+ const content = await readFile(absPath);
191
+ entries.push(Object.freeze({
192
+ path: relPath,
193
+ type: 'blob',
194
+ mode: (st.mode & 0o111) ? '100755' : '100644',
195
+ content,
196
+ sha256: sha256Hex(content),
197
+ size: st.size,
198
+ }));
199
+ }
200
+ }
201
+
202
+ entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
203
+ return Object.freeze(entries);
204
+ }
205
+
206
+ /**
207
+ * Materialize entries to a directory.
208
+ * Writes content from entries to disk.
209
+ *
210
+ * @param {Array} entries - Entry objects with relative paths and content.
211
+ * @param {string} dirPath - Target directory.
212
+ */
213
+ async function materializeEntries(entries, dirPath) {
214
+ for (const entry of entries) {
215
+ if (!entry.path) continue;
216
+ const targetPath = join(dirPath, entry.path);
217
+ if (entry.type === 'tree' || entry.kind === 'tree') {
218
+ await mkdir(targetPath, { recursive: true });
219
+ } else {
220
+ await mkdir(join(targetPath, '..'), { recursive: true });
221
+ if (entry.content) {
222
+ await writeFile(targetPath, entry.content);
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Verify producer determinism by running the producer twice in separate
230
+ * temp directories and comparing output manifest digests.
231
+ *
232
+ * @param {Function} produce - The producer function.
233
+ * @param {object} runOptions - Options passed to the producer.
234
+ * @param {string} dir1 - First temp directory.
235
+ * @param {string} dir2 - Second temp directory.
236
+ * @returns {Promise<{entries: Array, outputDir: string}>} Entries from the second run.
237
+ * @throws {ReleaseError} PRODUCER_NONDETERMINISTIC if outputs differ.
238
+ */
239
+ async function verifyDeterminism(produce, runOptions, dir1, dir2) {
240
+ // Run 1
241
+ await produce({ ...runOptions, output: dir1 });
242
+ const entries1 = await readDirEntries(dir1);
243
+
244
+ // Run 2
245
+ await produce({ ...runOptions, output: dir2 });
246
+ const entries2 = await readDirEntries(dir2);
247
+
248
+ // Compare output manifest digests
249
+ const digest1 = digestEntryManifest(entries1);
250
+ const digest2 = digestEntryManifest(entries2);
251
+
252
+ if (digest1 !== digest2) {
253
+ throw new ReleaseError(
254
+ PRODUCER_NONDETERMINISTIC,
255
+ `producer produced different outputs in two runs: ${digest1} vs ${digest2}`,
256
+ { digest1, digest2 },
257
+ );
258
+ }
259
+
260
+ // Clean up dir1 (the verification copy)
261
+ await rm(dir1, { recursive: true, force: true }).catch(() => {});
262
+
263
+ return { entries: entries2, outputDir: dir2 };
264
+ }
265
+
266
+ /**
267
+ * Compute the upstream closure of a set of artifact IDs.
268
+ * For each requested ID, includes all transitive upstream dependencies.
269
+ *
270
+ * @param {string[]} ids - Requested artifact IDs.
271
+ * @param {object} graph - Producer graph.
272
+ * @returns {Set<string>} All needed artifact IDs (including originals).
273
+ */
274
+ function computeUpstreamClosure(ids, graph) {
275
+ const needed = new Set(ids);
276
+ const queue = [...ids];
277
+ while (queue.length > 0) {
278
+ const id = queue.shift();
279
+ for (const up of graph.upstreamOf(id)) {
280
+ if (!needed.has(up)) {
281
+ needed.add(up);
282
+ queue.push(up);
283
+ }
284
+ }
285
+ }
286
+ return needed;
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Public API
291
+ // ---------------------------------------------------------------------------
292
+
293
+ /**
294
+ * Run producer closure: execute producers in topological order, scoped to
295
+ * requested artifact IDs and their upstream closure.
296
+ *
297
+ * Each producer is run twice in separate temp directories to verify
298
+ * deterministic output. The second run's output is used as the
299
+ * canonical result.
300
+ *
301
+ * For declared artifacts, the input is resolved from `inputSnapshot`.
302
+ * For generated artifacts, the input is resolved from upstream producer
303
+ * outputs (if available), falling back to `inputSnapshot`.
304
+ *
305
+ * @param {object} options
306
+ * @param {ProducerRegistry} options.registry - Producer registry.
307
+ * @param {object} options.graph - Producer graph from buildProducerGraph.
308
+ * @param {Map<string, object[]>} options.inputSnapshot - Input entries per artifact ID.
309
+ * @param {string[]} options.artifactIds - Artifact IDs to produce.
310
+ * @param {Function} [options.tempRootFactory] - Factory for temp directories.
311
+ * @returns {Promise<{byArtifact: Map<string, CandidateManifest>}>}
312
+ */
313
+ export async function runProducerClosure({
314
+ registry,
315
+ graph,
316
+ inputSnapshot,
317
+ artifactIds,
318
+ tempRootFactory = async () => mkdtemp(join(tmpdir(), 'producer-')),
319
+ } = {}) {
320
+ // Validate requested artifact IDs exist in graph
321
+ const generatedSet = new Set(graph.topologicalOrder);
322
+ for (const id of artifactIds) {
323
+ if (!generatedSet.has(id)) {
324
+ throw new ReleaseError(
325
+ ARTIFACT_POLICY_INVALID,
326
+ `artifact "${id}" is not a generated artifact in the graph`,
327
+ { id, generated: [...generatedSet] },
328
+ );
329
+ }
330
+ }
331
+
332
+ // Scope: only run the upstream closure of requested artifacts
333
+ const neededSet = computeUpstreamClosure(artifactIds, graph);
334
+
335
+ const outputMap = new Map(); // artifact ID → output directory path
336
+ const byArtifact = new Map(); // artifact ID → CandidateManifest
337
+ const tempDirs = [];
338
+
339
+ try {
340
+ for (const id of graph.topologicalOrder) {
341
+ // Skip artifacts outside the needed scope
342
+ if (!neededSet.has(id)) continue;
343
+
344
+ const producerName = graph.producerOf(id);
345
+ if (!producerName) continue;
346
+ const entry = registry.get(producerName);
347
+ if (!entry) continue;
348
+
349
+ const { produce, implementationDigest } = entry;
350
+
351
+ // Resolve input path:
352
+ // For generated artifacts, prefer upstream producer output.
353
+ // For declared artifacts, use inputSnapshot.
354
+ let inputPath;
355
+
356
+ // Check if any upstream artifact produced output
357
+ const upstreamIds = graph.upstreamOf(id);
358
+ for (const upstreamId of upstreamIds) {
359
+ if (outputMap.has(upstreamId)) {
360
+ inputPath = outputMap.get(upstreamId);
361
+ break;
362
+ }
363
+ }
364
+
365
+ if (!inputPath) {
366
+ const inputEntries = inputSnapshot.get(id);
367
+ if (inputEntries) {
368
+ // For tree entries: the path field is the absolute root directory
369
+ // For file entries: the path field is the absolute file path
370
+ const first = inputEntries[0];
371
+ if (first && first.path) {
372
+ const absPath = first.path.startsWith('/') ? first.path : join(process.cwd(), first.path);
373
+ try {
374
+ const st = await stat(absPath);
375
+ if (st.isDirectory()) {
376
+ inputPath = absPath;
377
+ } else {
378
+ // File entry — materialize to temp dir
379
+ const matDir = await tempRootFactory();
380
+ tempDirs.push(matDir);
381
+ await materializeEntries(inputEntries, matDir);
382
+ inputPath = matDir;
383
+ }
384
+ } catch {
385
+ // Not a valid path — materialize entries
386
+ const matDir = await tempRootFactory();
387
+ tempDirs.push(matDir);
388
+ await materializeEntries(inputEntries, matDir);
389
+ inputPath = matDir;
390
+ }
391
+ } else {
392
+ // No path — materialize to temp dir
393
+ const matDir = await tempRootFactory();
394
+ tempDirs.push(matDir);
395
+ await materializeEntries(inputEntries, matDir);
396
+ inputPath = matDir;
397
+ }
398
+ }
399
+ }
400
+
401
+ // Create temp directories for dual-run verification
402
+ const dir1 = await tempRootFactory();
403
+ const dir2 = await tempRootFactory();
404
+ tempDirs.push(dir1, dir2);
405
+
406
+ const runOptions = { inputs: inputPath };
407
+
408
+ // Verify determinism and get entries from second run
409
+ const { entries, outputDir } = await verifyDeterminism(produce, runOptions, dir1, dir2);
410
+
411
+ // Build input manifest digest
412
+ let inputManifestDigest;
413
+ if (inputPath) {
414
+ const inputEntriesForDigest = await readDirEntries(inputPath);
415
+ inputManifestDigest = digestEntryManifest(inputEntriesForDigest);
416
+ } else {
417
+ inputManifestDigest = digestEntryManifest([]);
418
+ }
419
+
420
+ // Build CandidateManifest
421
+ const manifest = Object.freeze({
422
+ producerId: id,
423
+ implementationDigest,
424
+ inputManifestDigest,
425
+ outputManifestDigest: digestEntryManifest(entries),
426
+ outputs: entries,
427
+ outputDir,
428
+ });
429
+
430
+ outputMap.set(id, outputDir);
431
+ byArtifact.set(id, manifest);
432
+ }
433
+ } finally {
434
+ // Temp dirs containing canonical outputs are NOT cleaned here;
435
+ // they are cleaned by the caller or at process exit.
436
+ }
437
+
438
+ return Object.freeze({ byArtifact });
439
+ }