release-skill 0.9.1 → 0.9.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +78 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +56 -26
- package/README.zh-CN.md +47 -23
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +7 -7
- package/adapters/claude/bin/error-codes.json +1 -1
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/registry.json +22 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +4564 -1997
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/schemas/release-project.schema.json +4 -0
- package/adapters/claude/schemas/release-run.schema.json +41 -1
- package/adapters/claude/skills/release-config/SKILL.md +5 -3
- package/adapters/claude/skills/release-docs/SKILL.md +4 -2
- package/adapters/claude/skills/release-finish/SKILL.md +2 -0
- package/adapters/claude/skills/release-help/SKILL.md +18 -3
- package/adapters/claude/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +12 -2
- package/adapters/claude/skills/release-publish/SKILL.md +17 -10
- package/adapters/claude/skills/release-verify/SKILL.md +2 -0
- package/adapters/claude/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/claude/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/claude/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/claude/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
- package/adapters/codex/bin/error-codes.json +1 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/registry.json +22 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +4564 -1997
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/schemas/release-project.schema.json +4 -0
- package/adapters/codex/schemas/release-run.schema.json +41 -1
- package/adapters/codex/skills/release-config/SKILL.md +5 -3
- package/adapters/codex/skills/release-docs/SKILL.md +4 -2
- package/adapters/codex/skills/release-finish/SKILL.md +2 -0
- package/adapters/codex/skills/release-help/SKILL.md +18 -3
- package/adapters/codex/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +12 -2
- package/adapters/codex/skills/release-publish/SKILL.md +17 -10
- package/adapters/codex/skills/release-verify/SKILL.md +2 -0
- package/adapters/codex/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/codex/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/codex/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/codex/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
- package/adapters/kimi/bin/error-codes.json +1 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/registry.json +22 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +4564 -1997
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/schemas/release-project.schema.json +4 -0
- package/adapters/kimi/schemas/release-run.schema.json +41 -1
- package/adapters/kimi/skills/release-config/SKILL.md +5 -3
- package/adapters/kimi/skills/release-docs/SKILL.md +4 -2
- package/adapters/kimi/skills/release-finish/SKILL.md +2 -0
- package/adapters/kimi/skills/release-help/SKILL.md +18 -3
- package/adapters/kimi/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +12 -2
- package/adapters/kimi/skills/release-publish/SKILL.md +17 -10
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
- package/adapters/kimi/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/kimi/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/kimi/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
- package/adapters/workbuddy/bin/error-codes.json +1 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/registry.json +22 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +4564 -1997
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/schemas/release-project.schema.json +4 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +41 -1
- package/adapters/workbuddy/skills/release-config/SKILL.md +5 -3
- package/adapters/workbuddy/skills/release-docs/SKILL.md +4 -2
- package/adapters/workbuddy/skills/release-finish/SKILL.md +2 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +18 -3
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +12 -2
- package/adapters/workbuddy/skills/release-publish/SKILL.md +17 -10
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
- package/adapters/workbuddy/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/bin/consumer-contract-vectors.json +7 -7
- package/bin/error-codes.json +1 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +58 -2
- package/bin/release-skill.bundle.mjs +4564 -1997
- package/bin/rules.json +1 -1
- package/package.json +5 -5
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +4 -4
- package/references/01-state-machine.md +34 -1
- package/references/02-project-config.md +20 -0
- package/references/05-evidence-and-errors.md +2 -2
- package/schemas/release-project.schema.json +4 -0
- package/schemas/release-run.schema.json +41 -1
- package/skills/release-config/SKILL.md +5 -3
- package/skills/release-docs/SKILL.md +4 -2
- package/skills/release-finish/SKILL.md +2 -0
- package/skills/release-help/SKILL.md +18 -3
- package/skills/release-marketplace/SKILL.md +3 -1
- package/skills/release-prepare/SKILL.md +12 -2
- package/skills/release-publish/SKILL.md +17 -10
- package/skills/release-verify/SKILL.md +2 -0
- package/skills-src/release-config/SKILL.md +5 -3
- package/skills-src/release-docs/SKILL.md +4 -2
- package/skills-src/release-finish/SKILL.md +2 -0
- package/skills-src/release-help/SKILL.md +18 -3
- package/skills-src/release-marketplace/SKILL.md +3 -1
- package/skills-src/release-prepare/SKILL.md +12 -2
- package/skills-src/release-publish/SKILL.md +17 -10
- package/skills-src/release-verify/SKILL.md +2 -0
- package/src/commands/post-release-local.mjs +169 -16
- package/src/commands/prepare.mjs +321 -11
- package/src/commands/route.mjs +597 -18
- package/src/commands/setup.mjs +2 -0
- package/src/commands/ship.mjs +183 -2
- package/src/commands/verify.mjs +21 -0
- package/src/core/adoption-assessment.mjs +59 -0
- package/src/core/foundation-plugin-verification.mjs +233 -1
- package/src/core/hook-cache.mjs +318 -26
- package/src/core/hooks.mjs +8 -1
- package/src/producers/foundation-resource-projection.mjs +3 -3
- package/src/schemas/executable-identity-observation.schema.json +162 -0
- package/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/src/schemas/plugin-verification-request.schema.json +147 -22
- package/src/schemas/plugin-verification-result.schema.json +436 -18
- package/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
package/src/commands/prepare.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { promisify } from 'node:util';
|
|
|
25
25
|
|
|
26
26
|
const execFile = promisify(execFileCb);
|
|
27
27
|
|
|
28
|
-
import { classifyPathInput, writeFileAtomic } from 'skill-family-harness-node';
|
|
28
|
+
import { classifyPathInput, writeFileAtomic, withTemporaryWorkspace } from 'skill-family-harness-node';
|
|
29
29
|
import { loadProjectConfig } from '../core/config.mjs';
|
|
30
30
|
import { captureBaseline } from '../core/baseline.mjs';
|
|
31
31
|
import { runHook } from '../core/hooks.mjs';
|
|
@@ -323,9 +323,16 @@ export async function runDeclaredHooks(config, root, evidence, hookFn = runHook,
|
|
|
323
323
|
|
|
324
324
|
// --- Incremental cache lookup (opt-in only; default zero change) ---
|
|
325
325
|
let cacheKey;
|
|
326
|
+
let cacheReceipt;
|
|
327
|
+
let cacheRootBinding;
|
|
326
328
|
if (cacheEnabled && hook.cacheable === true) {
|
|
327
329
|
try {
|
|
328
|
-
({ cacheKey } = await computeHookCacheKey(hook, root
|
|
330
|
+
({ cacheKey, receipt: cacheReceipt, projectRootBinding: cacheRootBinding } = await computeHookCacheKey(hook, root, {
|
|
331
|
+
env: options.env ?? process.env,
|
|
332
|
+
...(typeof options.observeExecutableIdentityFn === 'function'
|
|
333
|
+
? { observeExecutableIdentityFn: options.observeExecutableIdentityFn }
|
|
334
|
+
: {}),
|
|
335
|
+
}));
|
|
329
336
|
} catch (err) {
|
|
330
337
|
await evidence.append({
|
|
331
338
|
phase: 'hooks',
|
|
@@ -336,7 +343,9 @@ export async function runDeclaredHooks(config, root, evidence, hookFn = runHook,
|
|
|
336
343
|
throw err;
|
|
337
344
|
}
|
|
338
345
|
|
|
339
|
-
const cached = await readHookCache(root, name, cacheKey
|
|
346
|
+
const cached = cacheKey ? await readHookCache(root, name, cacheKey, {
|
|
347
|
+
rootBinding: cacheRootBinding,
|
|
348
|
+
}) : null;
|
|
340
349
|
if (cached) {
|
|
341
350
|
// Cache hit: skip execution. The authorization gate already passed and
|
|
342
351
|
// no GATE is bypassed — ordering and failure semantics are untouched.
|
|
@@ -407,12 +416,13 @@ export async function runDeclaredHooks(config, root, evidence, hookFn = runHook,
|
|
|
407
416
|
}
|
|
408
417
|
|
|
409
418
|
// --- Write cache on success only; failures are never cached ---
|
|
410
|
-
if (cacheEnabled && hook.cacheable === true && cacheKey) {
|
|
419
|
+
if (cacheEnabled && hook.cacheable === true && cacheKey && cacheReceipt) {
|
|
411
420
|
const written = await writeHookCache(root, name, cacheKey, {
|
|
412
421
|
exitCode: 0,
|
|
413
422
|
stdoutTail: result.stdout.slice(-4000),
|
|
414
423
|
stderrTail: result.stderr.slice(-4000),
|
|
415
|
-
|
|
424
|
+
receipt: cacheReceipt,
|
|
425
|
+
}, { rootBinding: cacheRootBinding });
|
|
416
426
|
if (!written.ok) {
|
|
417
427
|
// The cache is an optimisation, not a gate: a write failure must not
|
|
418
428
|
// abort prepare. Surface it as a warning-level evidence event.
|
|
@@ -2794,6 +2804,255 @@ async function runPrepareSkillResourceClosureGate({
|
|
|
2794
2804
|
return skillResourceClosureResults;
|
|
2795
2805
|
}
|
|
2796
2806
|
|
|
2807
|
+
/**
|
|
2808
|
+
* R93-01: optionally inspect the public surface before the first declared
|
|
2809
|
+
* hook. The staging and closure checks are deliberately ephemeral: their
|
|
2810
|
+
* result is an execution gate, not a plan/approval/publish receipt. The
|
|
2811
|
+
* normal post-hook snapshot and closure gate remains authoritative.
|
|
2812
|
+
*/
|
|
2813
|
+
async function runPreHookPublicSurfaceGate({
|
|
2814
|
+
config,
|
|
2815
|
+
root,
|
|
2816
|
+
resolvedVersions,
|
|
2817
|
+
evidence,
|
|
2818
|
+
skipDeclaredHooks,
|
|
2819
|
+
workflow,
|
|
2820
|
+
}) {
|
|
2821
|
+
if (config.policy?.preHookPublicSurfaceCheck !== true) return;
|
|
2822
|
+
|
|
2823
|
+
const declaredHookCount = Object.values(config.hooks ?? {})
|
|
2824
|
+
.filter((hook) => hook && hook.command).length;
|
|
2825
|
+
if (skipDeclaredHooks || declaredHookCount === 0) {
|
|
2826
|
+
await evidence.append({
|
|
2827
|
+
phase: 'pre-hook-public-surface',
|
|
2828
|
+
status: 'skipped',
|
|
2829
|
+
reason: skipDeclaredHooks
|
|
2830
|
+
? `workflow "${workflow}" trims code-class hooks`
|
|
2831
|
+
: 'no declared hooks',
|
|
2832
|
+
});
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
await evidence.append({ phase: 'pre-hook-public-surface', status: 'started' });
|
|
2837
|
+
let currentUnitId = null;
|
|
2838
|
+
let unitCount = 0;
|
|
2839
|
+
try {
|
|
2840
|
+
await withTemporaryWorkspace(async (workspace) => {
|
|
2841
|
+
const stagingRoot = workspace.root;
|
|
2842
|
+
const unitResults = [];
|
|
2843
|
+
for (const unit of config.releaseUnits ?? []) {
|
|
2844
|
+
currentUnitId = unit.id;
|
|
2845
|
+
const outputDir = resolveUnitScopedPath(stagingRoot, unit.id);
|
|
2846
|
+
const publicManifest = await buildPublicStaging({
|
|
2847
|
+
sourceRoot: root,
|
|
2848
|
+
unit,
|
|
2849
|
+
outputDir,
|
|
2850
|
+
});
|
|
2851
|
+
unitResults.push({
|
|
2852
|
+
unit,
|
|
2853
|
+
manifest: {
|
|
2854
|
+
entries: publicManifest.entries,
|
|
2855
|
+
files: publicManifest.entries.map((entry) => entry.path).sort(),
|
|
2856
|
+
totalSize: publicManifest.totalSize,
|
|
2857
|
+
fileCount: publicManifest.fileCount,
|
|
2858
|
+
contentHash: publicManifest.contentHash,
|
|
2859
|
+
snapshotDigest: publicManifest.contentHash,
|
|
2860
|
+
source: unit.source,
|
|
2861
|
+
outputDir: publicManifest.outputDir,
|
|
2862
|
+
},
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
const { frozenManifestByDist } = await resolveDistributionManifestFacts(
|
|
2867
|
+
unitResults,
|
|
2868
|
+
resolvedVersions,
|
|
2869
|
+
);
|
|
2870
|
+
unitCount = unitResults.length;
|
|
2871
|
+
// Reuse the production closure/host-surface implementation, but do not
|
|
2872
|
+
// append its ordinary phase events or retain its receipt in the plan.
|
|
2873
|
+
await runPrepareSkillResourceClosureGate({
|
|
2874
|
+
unitResults,
|
|
2875
|
+
frozenManifestByDist,
|
|
2876
|
+
freezeTimestamp: null,
|
|
2877
|
+
evidence: { append: async () => undefined },
|
|
2878
|
+
skipSkillResourceClosure: false,
|
|
2879
|
+
workflow: 'pre-hook-public-surface',
|
|
2880
|
+
});
|
|
2881
|
+
});
|
|
2882
|
+
await evidence.append({
|
|
2883
|
+
phase: 'pre-hook-public-surface',
|
|
2884
|
+
status: 'passed',
|
|
2885
|
+
unitCount,
|
|
2886
|
+
});
|
|
2887
|
+
} catch (error) {
|
|
2888
|
+
const details = {
|
|
2889
|
+
...(error instanceof ReleaseError ? error.details : {}),
|
|
2890
|
+
phase: 'pre-hook-public-surface',
|
|
2891
|
+
unitId: error?.details?.unitId ?? currentUnitId,
|
|
2892
|
+
};
|
|
2893
|
+
if (Array.isArray(details.findings)) {
|
|
2894
|
+
details.findings = details.findings.slice(0, 50);
|
|
2895
|
+
}
|
|
2896
|
+
await evidence.append({
|
|
2897
|
+
phase: 'pre-hook-public-surface',
|
|
2898
|
+
status: 'failed',
|
|
2899
|
+
error: { code: error.code ?? GATE_FAILED, message: error.message },
|
|
2900
|
+
});
|
|
2901
|
+
if (error instanceof ReleaseError) {
|
|
2902
|
+
throw new ReleaseError(
|
|
2903
|
+
error.code,
|
|
2904
|
+
`pre-hook public surface check failed before hooks: ${error.message}`,
|
|
2905
|
+
details,
|
|
2906
|
+
error.exitCode,
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2909
|
+
throw new ReleaseError(
|
|
2910
|
+
GATE_FAILED,
|
|
2911
|
+
`pre-hook public surface check failed before hooks: ${error.message}`,
|
|
2912
|
+
details,
|
|
2913
|
+
);
|
|
2914
|
+
} finally {
|
|
2915
|
+
// withTemporaryWorkspace disposes the Foundation-managed staging root on
|
|
2916
|
+
// both success and failure; no project run artifact is retained.
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
/**
|
|
2921
|
+
* Resolve the operator's pre-freeze release-unit request against the complete
|
|
2922
|
+
* validated project configuration. The returned units always follow config
|
|
2923
|
+
* declaration order so plan ordering and digests never depend on argv order.
|
|
2924
|
+
*/
|
|
2925
|
+
function prepareRetryPathArg(path) {
|
|
2926
|
+
return relative(process.cwd(), resolve(path)) || '.';
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
function buildPrepareRetryArgv(options, unitIds) {
|
|
2930
|
+
const argv = [
|
|
2931
|
+
'release-skill',
|
|
2932
|
+
'prepare',
|
|
2933
|
+
'--root',
|
|
2934
|
+
prepareRetryPathArg(options.root),
|
|
2935
|
+
];
|
|
2936
|
+
if (typeof options.version === 'string' && options.version.length > 0) {
|
|
2937
|
+
argv.push('--target-version', options.version);
|
|
2938
|
+
}
|
|
2939
|
+
argv.push(options.offline ? '--offline' : '--online');
|
|
2940
|
+
if (options.production) argv.push('--production');
|
|
2941
|
+
argv.push('--workflow', options.workflow);
|
|
2942
|
+
if (typeof options.testSelection === 'string') {
|
|
2943
|
+
argv.push('--test-selection', options.testSelection);
|
|
2944
|
+
}
|
|
2945
|
+
if (typeof options.output === 'string') {
|
|
2946
|
+
argv.push('--output', prepareRetryPathArg(options.output));
|
|
2947
|
+
}
|
|
2948
|
+
// A production --run-dir is single-use by contract and this failed attempt
|
|
2949
|
+
// has already created it. Let the retry allocate a fresh evidence directory.
|
|
2950
|
+
if (options.hookCache === false) argv.push('--no-hook-cache');
|
|
2951
|
+
for (const unitId of unitIds) argv.push('--unit', unitId);
|
|
2952
|
+
return argv;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Keep the deferred-unit evidence and operator guidance on one exact argv.
|
|
2957
|
+
* The command points back to the caller's project root and carries the same
|
|
2958
|
+
* prepare mode while selecting only the units that were deferred.
|
|
2959
|
+
*/
|
|
2960
|
+
function buildDeferredReleaseDiagnostics(options, deferredUnitIds) {
|
|
2961
|
+
if (deferredUnitIds.length === 0) return null;
|
|
2962
|
+
return {
|
|
2963
|
+
deferredUnitIds: [...deferredUnitIds],
|
|
2964
|
+
nextPrepareArgv: buildPrepareRetryArgv(options, deferredUnitIds),
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
function resolveRequestedReleaseUnits(config, requestedUnitIds, retryOptions) {
|
|
2969
|
+
const availableUnits = config.releaseUnits ?? [];
|
|
2970
|
+
const availableUnitIds = availableUnits.map((unit) => unit.id);
|
|
2971
|
+
if (requestedUnitIds === undefined) {
|
|
2972
|
+
return {
|
|
2973
|
+
explicit: false,
|
|
2974
|
+
selectedUnits: availableUnits,
|
|
2975
|
+
selectedUnitIds: availableUnitIds,
|
|
2976
|
+
deferredUnitIds: [],
|
|
2977
|
+
receiptSelected: Boolean(config.publicSourceAuthorityReceipt),
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
if (!Array.isArray(requestedUnitIds) || requestedUnitIds.length === 0) {
|
|
2981
|
+
throw new ReleaseError(
|
|
2982
|
+
CONFIG_INVALID,
|
|
2983
|
+
'explicit release-unit selection must contain at least one unit id',
|
|
2984
|
+
{ requestedUnitIds, availableUnitIds },
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
const invalidUnitIds = requestedUnitIds.filter(
|
|
2989
|
+
(unitId) => typeof unitId !== 'string' || unitId.length === 0,
|
|
2990
|
+
);
|
|
2991
|
+
const seen = new Set();
|
|
2992
|
+
const duplicateUnitIds = [];
|
|
2993
|
+
for (const unitId of requestedUnitIds) {
|
|
2994
|
+
if (seen.has(unitId) && !duplicateUnitIds.includes(unitId)) duplicateUnitIds.push(unitId);
|
|
2995
|
+
seen.add(unitId);
|
|
2996
|
+
}
|
|
2997
|
+
const availableSet = new Set(availableUnitIds);
|
|
2998
|
+
const unknownUnitIds = requestedUnitIds.filter(
|
|
2999
|
+
(unitId) => typeof unitId === 'string' && unitId.length > 0 && !availableSet.has(unitId),
|
|
3000
|
+
);
|
|
3001
|
+
if (invalidUnitIds.length > 0 || duplicateUnitIds.length > 0 || unknownUnitIds.length > 0) {
|
|
3002
|
+
throw new ReleaseError(
|
|
3003
|
+
CONFIG_INVALID,
|
|
3004
|
+
'release-unit selection contains empty, duplicate, or unknown unit ids',
|
|
3005
|
+
{
|
|
3006
|
+
requestedUnitIds,
|
|
3007
|
+
availableUnitIds,
|
|
3008
|
+
invalidUnitIds,
|
|
3009
|
+
duplicateUnitIds,
|
|
3010
|
+
unknownUnitIds,
|
|
3011
|
+
},
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
const selectedSet = new Set(requestedUnitIds);
|
|
3016
|
+
const selectedUnits = availableUnits.filter((unit) => selectedSet.has(unit.id));
|
|
3017
|
+
const selectedUnitIds = selectedUnits.map((unit) => unit.id);
|
|
3018
|
+
const deferredUnitIds = availableUnitIds.filter((unitId) => !selectedSet.has(unitId));
|
|
3019
|
+
const deferredReleaseDiagnostics = buildDeferredReleaseDiagnostics(retryOptions, deferredUnitIds);
|
|
3020
|
+
const receipt = config.publicSourceAuthorityReceipt;
|
|
3021
|
+
let receiptSelected = false;
|
|
3022
|
+
if (receipt) {
|
|
3023
|
+
const receiptSet = new Set([receipt.coordinatorUnitId, ...receipt.subjectUnitIds]);
|
|
3024
|
+
const dependencyUnitIds = availableUnitIds.filter((unitId) => receiptSet.has(unitId));
|
|
3025
|
+
receiptSelected = dependencyUnitIds.some((unitId) => selectedSet.has(unitId));
|
|
3026
|
+
if (receiptSelected) {
|
|
3027
|
+
const missingUnitIds = dependencyUnitIds.filter((unitId) => !selectedSet.has(unitId));
|
|
3028
|
+
if (missingUnitIds.length > 0) {
|
|
3029
|
+
const retryUnitIds = availableUnitIds.filter(
|
|
3030
|
+
(unitId) => selectedSet.has(unitId) || receiptSet.has(unitId),
|
|
3031
|
+
);
|
|
3032
|
+
throw new ReleaseError(
|
|
3033
|
+
GATE_FAILED,
|
|
3034
|
+
'release-unit selection does not include the complete public source-authority dependency closure',
|
|
3035
|
+
{
|
|
3036
|
+
selectedUnitIds,
|
|
3037
|
+
missingUnitIds,
|
|
3038
|
+
dependencyUnitIds,
|
|
3039
|
+
retryArgv: buildPrepareRetryArgv(retryOptions, retryUnitIds),
|
|
3040
|
+
},
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
return {
|
|
3047
|
+
explicit: true,
|
|
3048
|
+
selectedUnits,
|
|
3049
|
+
selectedUnitIds,
|
|
3050
|
+
deferredUnitIds,
|
|
3051
|
+
deferredReleaseDiagnostics,
|
|
3052
|
+
receiptSelected,
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
|
|
2797
3056
|
export async function prepareRelease(options) {
|
|
2798
3057
|
const {
|
|
2799
3058
|
root,
|
|
@@ -2808,6 +3067,7 @@ export async function prepareRelease(options) {
|
|
|
2808
3067
|
workflow = 'full',
|
|
2809
3068
|
observePreviousPublicBaselineFn,
|
|
2810
3069
|
testSelection = 'full',
|
|
3070
|
+
unitIds,
|
|
2811
3071
|
} = options ?? {};
|
|
2812
3072
|
|
|
2813
3073
|
// --- Workflow profile (H5) ---
|
|
@@ -2916,7 +3176,20 @@ export async function prepareRelease(options) {
|
|
|
2916
3176
|
await evidence.append({ phase: 'config', status: 'started' });
|
|
2917
3177
|
|
|
2918
3178
|
const { config, configPath, configDigest } = await loadProjectConfig({ root: realRoot });
|
|
2919
|
-
const
|
|
3179
|
+
const releaseSelection = resolveRequestedReleaseUnits(config, unitIds, {
|
|
3180
|
+
root,
|
|
3181
|
+
version,
|
|
3182
|
+
offline,
|
|
3183
|
+
production,
|
|
3184
|
+
workflow,
|
|
3185
|
+
testSelection: typeof options?.testSelection === 'string' ? testSelection : undefined,
|
|
3186
|
+
output,
|
|
3187
|
+
hookCache: options?.hookCache,
|
|
3188
|
+
});
|
|
3189
|
+
const configUnits = releaseSelection.selectedUnits;
|
|
3190
|
+
const selectedConfig = { ...config, releaseUnits: configUnits };
|
|
3191
|
+
const selectedUnitIdSet = new Set(releaseSelection.selectedUnitIds);
|
|
3192
|
+
const adoptionWarnings = collectExpectedPublicSurfaceAdoptionWarnings(selectedConfig);
|
|
2920
3193
|
// Mutable operator-facing warning list: seeded from the adoption warnings,
|
|
2921
3194
|
// appended by later gates (O5 origin-ahead). Returned as `warnings`.
|
|
2922
3195
|
const runWarnings = [...adoptionWarnings];
|
|
@@ -2934,6 +3207,18 @@ export async function prepareRelease(options) {
|
|
|
2934
3207
|
...warning,
|
|
2935
3208
|
});
|
|
2936
3209
|
}
|
|
3210
|
+
if (releaseSelection.explicit) {
|
|
3211
|
+
await evidence.append({
|
|
3212
|
+
phase: 'release-scope',
|
|
3213
|
+
status: 'selected',
|
|
3214
|
+
mode: 'selected',
|
|
3215
|
+
selectedUnitIds: releaseSelection.selectedUnitIds,
|
|
3216
|
+
deferredUnitIds: releaseSelection.deferredUnitIds,
|
|
3217
|
+
...(releaseSelection.deferredReleaseDiagnostics
|
|
3218
|
+
? { nextPrepareArgv: releaseSelection.deferredReleaseDiagnostics.nextPrepareArgv }
|
|
3219
|
+
: {}),
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
2937
3222
|
|
|
2938
3223
|
// --- Step 1-fresh: Bundle freshness gate (BUNDLE_STALE, fail-closed) ---
|
|
2939
3224
|
// 2026-08-18 investigation §4.2: a stale bin/release-skill.bundle.mjs
|
|
@@ -3009,7 +3294,6 @@ export async function prepareRelease(options) {
|
|
|
3009
3294
|
// gate, baseline, snapshot, remote check, or plan write runs. Units
|
|
3010
3295
|
// without releaseDocuments keep the exact legacy behaviour (the gate
|
|
3011
3296
|
// appends no evidence and performs no check).
|
|
3012
|
-
const configUnits = config.releaseUnits ?? [];
|
|
3013
3297
|
const resolvedVersions = await resolveAllUnitVersions(
|
|
3014
3298
|
configUnits,
|
|
3015
3299
|
realRoot,
|
|
@@ -3207,7 +3491,8 @@ export async function prepareRelease(options) {
|
|
|
3207
3491
|
});
|
|
3208
3492
|
}
|
|
3209
3493
|
|
|
3210
|
-
const declaredVerificationGates = config.verificationGates ?? []
|
|
3494
|
+
const declaredVerificationGates = (config.verificationGates ?? [])
|
|
3495
|
+
.filter((gate) => selectedUnitIdSet.has(gate.scope.unit));
|
|
3211
3496
|
if (declaredVerificationGates.length > 0) {
|
|
3212
3497
|
await evidence.append({
|
|
3213
3498
|
phase: 'verification-gate-authorization',
|
|
@@ -3309,6 +3594,17 @@ export async function prepareRelease(options) {
|
|
|
3309
3594
|
});
|
|
3310
3595
|
}
|
|
3311
3596
|
|
|
3597
|
+
// R93-01: optional ephemeral public-surface check immediately before the
|
|
3598
|
+
// first hook. Its staging and closure receipt never enter the plan.
|
|
3599
|
+
await runPreHookPublicSurfaceGate({
|
|
3600
|
+
config: selectedConfig,
|
|
3601
|
+
root: realRoot,
|
|
3602
|
+
resolvedVersions,
|
|
3603
|
+
evidence,
|
|
3604
|
+
skipDeclaredHooks,
|
|
3605
|
+
workflow,
|
|
3606
|
+
});
|
|
3607
|
+
|
|
3312
3608
|
// --- Step 3: Run declared hooks ---
|
|
3313
3609
|
let hookRecords = [];
|
|
3314
3610
|
if (skipDeclaredHooks) {
|
|
@@ -3760,7 +4056,7 @@ export async function prepareRelease(options) {
|
|
|
3760
4056
|
|
|
3761
4057
|
// --- Step 5: Build snapshots, scan, and evaluate README ---
|
|
3762
4058
|
const { unitResults, snapshotDigests } = await processSnapshots(
|
|
3763
|
-
|
|
4059
|
+
selectedConfig, realRoot, evidence, runDir, production,
|
|
3764
4060
|
);
|
|
3765
4061
|
|
|
3766
4062
|
// Snapshot gates always run on disposable writable copies. The public
|
|
@@ -4146,7 +4442,7 @@ export async function prepareRelease(options) {
|
|
|
4146
4442
|
}
|
|
4147
4443
|
|
|
4148
4444
|
let publicSourceAuthorityReceipt = null;
|
|
4149
|
-
if (config.publicSourceAuthorityReceipt) {
|
|
4445
|
+
if (config.publicSourceAuthorityReceipt && releaseSelection.receiptSelected) {
|
|
4150
4446
|
// P3 (ruling 7): config scenario A (no-publish-needed) means the public
|
|
4151
4447
|
// bytes are unchanged and nothing will be published — a configured
|
|
4152
4448
|
// public source receipt must not require or generate a nonexistent
|
|
@@ -4390,7 +4686,7 @@ export async function prepareRelease(options) {
|
|
|
4390
4686
|
},
|
|
4391
4687
|
}
|
|
4392
4688
|
: {}),
|
|
4393
|
-
verificationGates:
|
|
4689
|
+
verificationGates: declaredVerificationGates,
|
|
4394
4690
|
snapshotDigest: overallSnapshotDigest,
|
|
4395
4691
|
...(humanConsumersStrategy ? { humanConsumersStrategy } : {}),
|
|
4396
4692
|
...(production ? {
|
|
@@ -4474,6 +4770,13 @@ export async function prepareRelease(options) {
|
|
|
4474
4770
|
// carry no such warning. The prepare command's own defaults are unchanged —
|
|
4475
4771
|
// this only enriches the result, never flips offline/production.
|
|
4476
4772
|
const nextSteps = [];
|
|
4773
|
+
if (releaseSelection.explicit && releaseSelection.deferredUnitIds.length > 0) {
|
|
4774
|
+
nextSteps.push({
|
|
4775
|
+
code: 'DEFERRED_RELEASE_UNITS',
|
|
4776
|
+
message: `Prepare the deferred release units separately: ${releaseSelection.deferredUnitIds.join(', ')}`,
|
|
4777
|
+
argv: releaseSelection.deferredReleaseDiagnostics.nextPrepareArgv,
|
|
4778
|
+
});
|
|
4779
|
+
}
|
|
4477
4780
|
if (!production) {
|
|
4478
4781
|
nextSteps.push({
|
|
4479
4782
|
code: 'NON_PRODUCTION_PLAN_NOT_PUBLISHABLE',
|
|
@@ -4490,6 +4793,13 @@ export async function prepareRelease(options) {
|
|
|
4490
4793
|
evidenceDir,
|
|
4491
4794
|
warnings: runWarnings,
|
|
4492
4795
|
nextSteps,
|
|
4796
|
+
...(releaseSelection.explicit ? {
|
|
4797
|
+
releaseScope: {
|
|
4798
|
+
mode: 'selected',
|
|
4799
|
+
selectedUnitIds: releaseSelection.selectedUnitIds,
|
|
4800
|
+
deferredUnitIds: releaseSelection.deferredUnitIds,
|
|
4801
|
+
},
|
|
4802
|
+
} : {}),
|
|
4493
4803
|
};
|
|
4494
4804
|
} catch (err) {
|
|
4495
4805
|
// Record failure evidence (best effort). A broken clock can make the
|