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.
- package/.agents/plugins/marketplace.json +23 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +26 -0
- package/CHANGELOG.md +68 -0
- package/CODE_OF_CONDUCT.md +76 -0
- package/CONTRIBUTING.md +49 -0
- package/INSTALL.md +182 -0
- package/LICENSE +21 -0
- package/NOTICE +25 -0
- package/README.md +501 -0
- package/README.zh-CN.md +463 -0
- package/SECURITY.md +48 -0
- package/adapters/claude/.claude-plugin/marketplace.json +16 -0
- package/adapters/claude/.claude-plugin/plugin.json +10 -0
- package/adapters/claude/skills/release-assess/SKILL.md +52 -0
- package/adapters/claude/skills/release-help/SKILL.md +60 -0
- package/adapters/claude/skills/release-prepare/SKILL.md +71 -0
- package/adapters/claude/skills/release-publish/SKILL.md +55 -0
- package/adapters/claude/skills/release-reconcile/SKILL.md +73 -0
- package/adapters/claude/skills/release-verify/SKILL.md +70 -0
- package/adapters/codex/.codex-plugin/plugin.json +26 -0
- package/adapters/codex/skills/release-assess/SKILL.md +52 -0
- package/adapters/codex/skills/release-help/SKILL.md +60 -0
- package/adapters/codex/skills/release-prepare/SKILL.md +71 -0
- package/adapters/codex/skills/release-publish/SKILL.md +55 -0
- package/adapters/codex/skills/release-reconcile/SKILL.md +73 -0
- package/adapters/codex/skills/release-verify/SKILL.md +70 -0
- package/bin/release-skill.mjs +743 -0
- package/native/safe-write/binding.gyp +40 -0
- package/native/safe-write/prebuilds.json +4 -0
- package/native/safe-write/src/safe_write.cc +2023 -0
- package/package.json +75 -0
- package/references/.render-manifest.json +33 -0
- package/references/00-target-state.md +124 -0
- package/references/01-state-machine.md +155 -0
- package/references/02-project-config.md +217 -0
- package/references/03-readme-quality.md +136 -0
- package/references/04-supply-chain.md +147 -0
- package/references/05-evidence-and-errors.md +164 -0
- package/references/06-adapter-contract.md +178 -0
- package/schemas/.render-manifest.json +37 -0
- package/schemas/approval-record.schema.json +115 -0
- package/schemas/artifact-lock.schema.json +111 -0
- package/schemas/artifact-plan.schema.json +52 -0
- package/schemas/artifact-policy.schema.json +76 -0
- package/schemas/evidence-event.schema.json +89 -0
- package/schemas/release-plan.schema.json +369 -0
- package/schemas/release-project.schema.json +359 -0
- package/schemas/release-run.schema.json +195 -0
- package/skills/release-assess/SKILL.md +52 -0
- package/skills/release-help/SKILL.md +60 -0
- package/skills/release-prepare/SKILL.md +71 -0
- package/skills/release-publish/SKILL.md +55 -0
- package/skills/release-reconcile/SKILL.md +73 -0
- package/skills/release-verify/SKILL.md +70 -0
- package/skills-src/release-assess/SKILL.md +52 -0
- package/skills-src/release-help/SKILL.md +60 -0
- package/skills-src/release-prepare/SKILL.md +71 -0
- package/skills-src/release-publish/SKILL.md +55 -0
- package/skills-src/release-reconcile/SKILL.md +73 -0
- package/skills-src/release-verify/SKILL.md +70 -0
- package/src/adapters/contract.mjs +214 -0
- package/src/adapters/git-github.mjs +214 -0
- package/src/adapters/npm.mjs +947 -0
- package/src/adapters/plugin-marketplace.mjs +1365 -0
- package/src/adapters/push-snapshot.mjs +216 -0
- package/src/artifacts/adoption.mjs +743 -0
- package/src/artifacts/artifact-plan.mjs +162 -0
- package/src/artifacts/entry.mjs +240 -0
- package/src/artifacts/git-authority.mjs +637 -0
- package/src/artifacts/graph.mjs +189 -0
- package/src/artifacts/inspect.mjs +520 -0
- package/src/artifacts/inventory.mjs +192 -0
- package/src/artifacts/merge/binary.mjs +77 -0
- package/src/artifacts/merge/entry-merge.mjs +228 -0
- package/src/artifacts/merge/json.mjs +641 -0
- package/src/artifacts/merge/markdown.mjs +246 -0
- package/src/artifacts/merge/regions.mjs +156 -0
- package/src/artifacts/merge/text.mjs +432 -0
- package/src/artifacts/merge/tree.mjs +202 -0
- package/src/artifacts/merge/yaml.mjs +669 -0
- package/src/artifacts/path-key.mjs +94 -0
- package/src/artifacts/policy.mjs +319 -0
- package/src/artifacts/producer-registry.mjs +439 -0
- package/src/artifacts/project-lock.mjs +732 -0
- package/src/artifacts/resolution.mjs +658 -0
- package/src/artifacts/safe-fs-backend-internal.mjs +680 -0
- package/src/artifacts/safe-fs.mjs +72 -0
- package/src/artifacts/state.mjs +495 -0
- package/src/artifacts/transaction-journal.mjs +983 -0
- package/src/artifacts/transaction.mjs +1361 -0
- package/src/commands/approve.mjs +280 -0
- package/src/commands/artifacts.mjs +627 -0
- package/src/commands/assess.mjs +838 -0
- package/src/commands/prepare.mjs +1377 -0
- package/src/commands/publish.mjs +883 -0
- package/src/commands/reconcile.mjs +1255 -0
- package/src/commands/verify.mjs +915 -0
- package/src/core/approval.mjs +332 -0
- package/src/core/baseline.mjs +272 -0
- package/src/core/blackbox-hard-gates.mjs +142 -0
- package/src/core/config.mjs +448 -0
- package/src/core/digest.mjs +90 -0
- package/src/core/errors.mjs +113 -0
- package/src/core/evidence.mjs +167 -0
- package/src/core/hooks.mjs +241 -0
- package/src/core/node-version.mjs +64 -0
- package/src/core/plan.mjs +735 -0
- package/src/core/previous-public-baseline.mjs +204 -0
- package/src/core/run.mjs +681 -0
- package/src/core/state-machine.mjs +76 -0
- package/src/core/version-consistency.mjs +111 -0
- package/src/producers/build-adapters.mjs +231 -0
- package/src/producers/render-public-assets.mjs +152 -0
- package/src/producers/sync-skills.mjs +96 -0
- package/src/readme/contract.mjs +297 -0
- package/src/readme/examples.mjs +288 -0
- package/src/readme/parity.mjs +122 -0
- package/src/snapshot/export.mjs +99 -0
- package/src/snapshot/frozen.mjs +401 -0
- package/src/snapshot/manifest.mjs +207 -0
- package/src/snapshot/public-map.mjs +1459 -0
- package/src/snapshot/public-path.mjs +110 -0
- 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
|
+
}
|