release-skill 0.9.23 → 0.9.25
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/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.qoder-plugin/plugin.json +1 -1
- package/CHANGELOG.md +51 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +19 -32
- package/README.zh-CN.md +18 -28
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +840 -1030
- package/adapters/claude/skills/release-help/SKILL.md +2 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +840 -1030
- package/adapters/codex/skills/release-help/SKILL.md +2 -2
- package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
- package/adapters/cursor/bin/release-skill.bundle.mjs +840 -1030
- package/adapters/cursor/skills/release-help/SKILL.md +2 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +840 -1030
- package/adapters/kimi/skills/release-help/SKILL.md +2 -2
- package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/qoder/bin/release-skill.bundle.mjs +840 -1030
- package/adapters/qoder/skills/release-help/SKILL.md +2 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +840 -1030
- package/adapters/workbuddy/skills/release-help/SKILL.md +2 -2
- package/bin/release-skill.bundle.mjs +840 -1030
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/05-evidence-and-errors.md +2 -2
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/sync-public-files.mjs +1 -1
- package/skills/release-help/SKILL.md +2 -2
- package/skills-src/release-help/SKILL.md +2 -2
- package/src/artifacts/producer-registry.mjs +4 -3
- package/src/commands/prepare.mjs +5 -30
- package/src/core/baseline.mjs +9 -1
- package/src/core/public-surface.mjs +1 -1
- package/src/core/redact.mjs +12 -5
- package/src/core/source-authority.mjs +28 -3
- package/src/npm/npm-entry-closure.mjs +71 -9
- package/src/producers/build-adapters.mjs +2 -1
- package/src/producers/render-public-assets.mjs +2 -1
- package/src/producers/sync-skills.mjs +2 -1
- package/src/snapshot/frozen.mjs +19 -14
- package/src/core/run-retention.mjs +0 -227
package/package.json
CHANGED
package/platform-manifest.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
3
|
"platformId": "release-skill",
|
|
4
4
|
"platformName": "release-skill Plugin Platform",
|
|
5
|
-
"version": "0.9.
|
|
5
|
+
"version": "0.9.25",
|
|
6
6
|
"logicalRoot": "skills-src/",
|
|
7
|
-
"logicalRootDigest": "
|
|
8
|
-
"projectionDigest": "
|
|
9
|
-
"projectionId": "release-skill:platform:0.9.
|
|
7
|
+
"logicalRootDigest": "7b62f9ded9aea774298ad86a781d58ad83f4092f3c3220b4a75dfe11ad19f809",
|
|
8
|
+
"projectionDigest": "bb92d4a3f57842000aa404a0698c5e38c111f05408027491e0904912af81d716",
|
|
9
|
+
"projectionId": "release-skill:platform:0.9.25",
|
|
10
10
|
"manifestRequired": true,
|
|
11
11
|
"projectionSources": {
|
|
12
12
|
"skills-src-root": {
|
|
@@ -207,8 +207,8 @@ v2 的顶层结构封闭(`additionalProperties: false`),只接受 Schema
|
|
|
207
207
|
|
|
208
208
|
### 5.1 文件保留
|
|
209
209
|
|
|
210
|
-
-
|
|
211
|
-
-
|
|
210
|
+
- 不得手工删除运行证据。生产 prepare 保留 `.release-skill/runs/` 中已有的运行目录,包括封口失败的 prepare 和已经结束的谱系。
|
|
211
|
+
- 生产 prepare 保留的是这些运行目录,不保证目录内每个历史文件的字节永久不变。文件如何写入仍按本节其余证据合同执行。
|
|
212
212
|
- `evidence.jsonl` 为追加写入,不得在执行过程中被截断。
|
|
213
213
|
- `summary.json` 在执行结束时原子写入(writer 单次封口:`finish()` 只能成功一次,封口后 append/finish 均为 no-op;正常命令的成功与失败路径都必须封口)。
|
|
214
214
|
- `states/` 台账为不可变写集:外部检查点成功后立即记账,失败时保留 `PARTIAL` 状态,reconcile 据此跳过已成功步骤。
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
23
23
|
import { existsSync } from 'node:fs';
|
|
24
24
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
25
26
|
import { createHash } from 'node:crypto';
|
|
26
27
|
import { computeBundleSourceDigest } from '../src/core/bundle-freshness.mjs';
|
|
27
28
|
import {
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
|
|
33
34
|
const CHECK_MODE = process.argv.includes('--check');
|
|
34
35
|
|
|
35
|
-
const SCRIPT_DIR = dirname(
|
|
36
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
36
37
|
const PKG_ROOT = join(SCRIPT_DIR, '..');
|
|
37
38
|
const ENTRY = join(PKG_ROOT, 'bin', 'release-skill-cli.mjs');
|
|
38
39
|
const OUTFILE = join(PKG_ROOT, 'bin', 'release-skill.bundle.mjs');
|
|
@@ -36,7 +36,7 @@ import { PLATFORMS } from '../src/platforms/registry.mjs';
|
|
|
36
36
|
// Constants
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
|
|
39
|
-
const SCRIPT_DIR = new URL('.', import.meta.url)
|
|
39
|
+
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
40
40
|
const PKG_ROOT = process.env.RELEASE_SKILL_PKG_ROOT ?? join(SCRIPT_DIR, '..');
|
|
41
41
|
const PROJECT_YAML = join(PKG_ROOT, '..', '..', '.release-skill', 'project.yaml');
|
|
42
42
|
const SOURCE_PREFIX = 'packages/release-skill/';
|
|
@@ -29,9 +29,9 @@ description: "Discoverable entry point for release-skill: dependency and environ
|
|
|
29
29
|
|
|
30
30
|
治理诊断只运行 release-skill 自己的只读检查程序,不运行目标 Skill、业务脚本、构建、hook 或发布动作,也不把 `prepare`、`verify`、`release-finish` 当作静态治理入口。用户要求实际接入或发布时,把已有授权带到对应原业务入口;各入口继续执行原有确认、副作用和状态机合同。
|
|
31
31
|
|
|
32
|
-
## 0.9.
|
|
32
|
+
## 0.9.25 候选边界
|
|
33
33
|
|
|
34
|
-
当前 0.9.
|
|
34
|
+
当前 0.9.25 候选精确消费 Foundation 0.22.0 的公开包根 API。0.9.25 仍是源码候选,不能从本说明推断已批准、发布或验证。
|
|
35
35
|
|
|
36
36
|
冻结前,`prepare` 或新建 `ship` 状态可重复传入 `--unit <id>`;不传则选择全部单元。延期单元不进入计划,也不获得发布状态。完整配置、生成物新鲜度和顶层 Hook 仍覆盖全项目;`publicSourceAuthorityReceipt` 的 coordinator 与 subjects 必须共同选择。冻结后以 `plan.units` 为唯一范围,publish、reconcile、verify、distribute 不再接受 `--unit`。
|
|
37
37
|
|
|
@@ -29,9 +29,9 @@ description: "Discoverable entry point for release-skill: dependency and environ
|
|
|
29
29
|
|
|
30
30
|
治理诊断只运行 release-skill 自己的只读检查程序,不运行目标 Skill、业务脚本、构建、hook 或发布动作,也不把 `prepare`、`verify`、`release-finish` 当作静态治理入口。用户要求实际接入或发布时,把已有授权带到对应原业务入口;各入口继续执行原有确认、副作用和状态机合同。
|
|
31
31
|
|
|
32
|
-
## 0.9.
|
|
32
|
+
## 0.9.25 候选边界
|
|
33
33
|
|
|
34
|
-
当前 0.9.
|
|
34
|
+
当前 0.9.25 候选精确消费 Foundation 0.22.0 的公开包根 API。0.9.25 仍是源码候选,不能从本说明推断已批准、发布或验证。
|
|
35
35
|
|
|
36
36
|
冻结前,`prepare` 或新建 `ship` 状态可重复传入 `--unit <id>`;不传则选择全部单元。延期单元不进入计划,也不获得发布状态。完整配置、生成物新鲜度和顶层 Hook 仍覆盖全项目;`publicSourceAuthorityReceipt` 的 coordinator 与 subjects 必须共同选择。冻结后以 `plan.units` 为唯一范围,publish、reconcile、verify、distribute 不再接受 `--unit`。
|
|
37
37
|
|
|
@@ -15,6 +15,7 @@ import { join } from 'node:path';
|
|
|
15
15
|
import { mkdtemp } from 'node:fs/promises';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
19
|
|
|
19
20
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
20
21
|
import { digestBytes } from 'skill-family-harness-node';
|
|
@@ -44,7 +45,7 @@ export { buildProducerGraph };
|
|
|
44
45
|
* @returns {string[]} Absolute paths of all reachable local modules.
|
|
45
46
|
*/
|
|
46
47
|
function collectStaticImports(filePath, visited = new Set()) {
|
|
47
|
-
const abs =
|
|
48
|
+
const abs = fileURLToPath(pathToFileURL(filePath));
|
|
48
49
|
if (visited.has(abs)) return [];
|
|
49
50
|
visited.add(abs);
|
|
50
51
|
|
|
@@ -109,7 +110,7 @@ export async function createBuiltInProducerRegistry() {
|
|
|
109
110
|
const producers = new Map();
|
|
110
111
|
|
|
111
112
|
// Resolve lockfile path (one level up from package root)
|
|
112
|
-
const lockfilePath = join(new URL('../../..', import.meta.url)
|
|
113
|
+
const lockfilePath = join(fileURLToPath(new URL('../../..', import.meta.url)), 'pnpm-lock.yaml');
|
|
113
114
|
let lockfileBytes;
|
|
114
115
|
try {
|
|
115
116
|
lockfileBytes = await readFile(lockfilePath);
|
|
@@ -128,7 +129,7 @@ export async function createBuiltInProducerRegistry() {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
// Collect full static dependency closure
|
|
131
|
-
const modulePath =
|
|
132
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
132
133
|
const allModulePaths = collectStaticImports(modulePath);
|
|
133
134
|
const allModuleBytes = await Promise.all(
|
|
134
135
|
allModulePaths.map(async (p) => {
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -78,7 +78,6 @@ import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
|
78
78
|
import { observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
|
|
79
79
|
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
80
80
|
import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
81
|
-
import { cleanupRunRetention } from '../core/run-retention.mjs';
|
|
82
81
|
import {
|
|
83
82
|
PLATFORMS,
|
|
84
83
|
normalizeHostId,
|
|
@@ -279,7 +278,9 @@ import { boundedOutputTail } from '../core/bounded-output.mjs';
|
|
|
279
278
|
* already captures child stdout/stderr on non-zero exit; on failure this
|
|
280
279
|
* layer writes bounded tails into the hooks evidence event AND echoes them to
|
|
281
280
|
* the current process' stderr, so a failing hook is diagnosable on the
|
|
282
|
-
* terminal without opening evidence.jsonl.
|
|
281
|
+
* terminal without opening evidence.jsonl. Fresh success and non-zero exit
|
|
282
|
+
* events also retain the returned full streams through the evidence writer's
|
|
283
|
+
* redaction. Success events carry no tails; cache hits have no fresh streams.
|
|
283
284
|
* Exit-code semantics are unchanged.
|
|
284
285
|
*
|
|
285
286
|
* @param {object} config - The loaded project config.
|
|
@@ -413,6 +414,7 @@ export async function runDeclaredHooks(config, root, evidence, hookFn = runHook,
|
|
|
413
414
|
// streams instead of the noisy compiler prelude at the beginning.
|
|
414
415
|
stdoutTail,
|
|
415
416
|
stderrTail,
|
|
417
|
+
details: { stdout: result.stdout, stderr: result.stderr },
|
|
416
418
|
...selectionField,
|
|
417
419
|
});
|
|
418
420
|
throw new ReleaseError(
|
|
@@ -448,6 +450,7 @@ export async function runDeclaredHooks(config, root, evidence, hookFn = runHook,
|
|
|
448
450
|
status: 'completed',
|
|
449
451
|
hookName: name,
|
|
450
452
|
exitCode: 0,
|
|
453
|
+
details: { stdout: result.stdout, stderr: result.stderr },
|
|
451
454
|
...selectionField,
|
|
452
455
|
});
|
|
453
456
|
records.push({ name, completed: true, cached: false, testSelection: selectionField.testSelection });
|
|
@@ -3404,34 +3407,6 @@ export async function prepareRelease(options) {
|
|
|
3404
3407
|
const evidence = createEvidenceWriter({ runDir, command: 'prepare', clock });
|
|
3405
3408
|
|
|
3406
3409
|
try {
|
|
3407
|
-
// R14-01: production prepare performs one best-effort retention pass only
|
|
3408
|
-
// after the current run/evidence authority exists and before high-cost
|
|
3409
|
-
// config, snapshot, and hook work begins. It never blocks prepare.
|
|
3410
|
-
if (production) {
|
|
3411
|
-
try {
|
|
3412
|
-
const retention = await (options.runRetentionFn ?? cleanupRunRetention)({
|
|
3413
|
-
releaseDir,
|
|
3414
|
-
currentRunDir: runDir,
|
|
3415
|
-
});
|
|
3416
|
-
await evidence.append({
|
|
3417
|
-
phase: 'run-retention',
|
|
3418
|
-
status: 'completed',
|
|
3419
|
-
scanned: retention.scanned,
|
|
3420
|
-
deleted: retention.deleted,
|
|
3421
|
-
errors: retention.errors,
|
|
3422
|
-
});
|
|
3423
|
-
} catch (error) {
|
|
3424
|
-
await evidence.append({
|
|
3425
|
-
phase: 'run-retention',
|
|
3426
|
-
status: 'warning',
|
|
3427
|
-
scanned: 0,
|
|
3428
|
-
deleted: 0,
|
|
3429
|
-
errors: 1,
|
|
3430
|
-
error: { code: error?.code ?? 'RUN_RETENTION_FAILED', message: error?.message ?? String(error) },
|
|
3431
|
-
});
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3435
3410
|
// --- Step 1: Load and validate config ---
|
|
3436
3411
|
await evidence.append({ phase: 'config', status: 'started' });
|
|
3437
3412
|
|
package/src/core/baseline.mjs
CHANGED
|
@@ -292,7 +292,15 @@ async function computeWorkspaceDigest(root) {
|
|
|
292
292
|
* @returns {Promise<{ gitHead: string, gitTreeHash: string, workspaceDigest: string, statusEntries: string[], capturedAt: string }>}
|
|
293
293
|
*/
|
|
294
294
|
export async function captureBaseline(root) {
|
|
295
|
-
|
|
295
|
+
// Same 64 MiB bound as computeWorkspaceDigest: `git status --porcelain=v1
|
|
296
|
+
// -z --untracked-files=all` lists every untracked file and exceeds Node's
|
|
297
|
+
// default 1 MiB execFile maxBuffer once that listing grows past ~1 MiB.
|
|
298
|
+
const opts = {
|
|
299
|
+
cwd: root,
|
|
300
|
+
shell: false,
|
|
301
|
+
encoding: 'utf8',
|
|
302
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
303
|
+
};
|
|
296
304
|
|
|
297
305
|
const [{ stdout: headOut }, { stdout: treeOut }, { stdout: statusOut }] =
|
|
298
306
|
await Promise.all([
|
|
@@ -437,7 +437,7 @@ export async function inspectExpectedPublicSurface({ root, unit } = {}) {
|
|
|
437
437
|
let skippedNonRegularCount = 0;
|
|
438
438
|
for (const rule of resolvedRoots) {
|
|
439
439
|
const observed = await scanRoot({ workspaceRoot, unit, rule });
|
|
440
|
-
|
|
440
|
+
for (const file of observed.files) observedFiles.push(file);
|
|
441
441
|
skippedNonRegularCount += observed.skippedNonRegularCount;
|
|
442
442
|
}
|
|
443
443
|
|
package/src/core/redact.mjs
CHANGED
|
@@ -54,12 +54,19 @@ export const REDACTED_PATH_PLACEHOLDER = '<redacted-path>';
|
|
|
54
54
|
// (b) UNC paths (\\server\share...);
|
|
55
55
|
// (c) POSIX absolute paths: a '/' anchored at the start of the string or
|
|
56
56
|
// preceded by a delimiter (whitespace, quote, '=', ':', ',', '(', '[',
|
|
57
|
-
// '{', '<')
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
57
|
+
// '{', '<'). Non-final '/'-separated segments may contain spaces (a
|
|
58
|
+
// volume name such as /Volumes/Samsung 990 pro 4T/src/file), so the
|
|
59
|
+
// token continues across those spaces and stops before trailing prose
|
|
60
|
+
// ('plan.json rejected: ...'). The left boundary keeps redaction from
|
|
61
|
+
// firing inside relative fragments ('src/core/x.mjs'), fragment-anchored
|
|
62
|
+
// JSON pointers ('#/required'), or protocol-relative URLs ('https://x/y').
|
|
63
|
+
// Windows drive and UNC alternatives still stop at whitespace; only the
|
|
64
|
+
// POSIX alternative crosses spaces, and only in non-final segments.
|
|
65
|
+
// A segment starting with a space right after '/' (e.g. /Volumes/ 990/x)
|
|
66
|
+
// and Windows drive/UNC tokens still truncate at that whitespace (known
|
|
67
|
+
// limitation).
|
|
61
68
|
const PATH_TOKEN_RE =
|
|
62
|
-
/(?<=^|[\s'"=:,([{<])(?:[A-Za-z]:[\\/][^\s'"]*|\\\\[^\s'"]+|\/[
|
|
69
|
+
/(?<=^|[\s'"=:,([{<])(?:[A-Za-z]:[\\/][^\s'"]*|\\\\[^\s'"]+|\/(?:[^/\s'"]+(?:[ ]+[^/\s'"]+)*\/)*[^/\s'"]*)/g;
|
|
63
70
|
|
|
64
71
|
// Trailing prose punctuation that may cling to a token and must survive.
|
|
65
72
|
const TRAILING_PUNCT_RE = /^(.*?)([.!,;:)\]}>]*)$/;
|
|
@@ -272,6 +272,21 @@ function normalizeLocalGitMode(mode) {
|
|
|
272
272
|
return (mode & 0o111) === 0 ? '100644' : '100755';
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
function gitPathSegments(value) {
|
|
276
|
+
return String(value).replaceAll('\\', '/').replace(/\/+$/u, '').split('/').filter(Boolean);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function sharesDirectoryBoundary(recordPath, requestedPath) {
|
|
280
|
+
const recordSegments = gitPathSegments(recordPath);
|
|
281
|
+
const requestedSegments = gitPathSegments(requestedPath);
|
|
282
|
+
const limit = Math.min(recordSegments.length, requestedSegments.length);
|
|
283
|
+
if (limit === 0) return false;
|
|
284
|
+
for (let index = 0; index < limit; index += 1) {
|
|
285
|
+
if (recordSegments[index] !== requestedSegments[index]) return false;
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
275
290
|
/**
|
|
276
291
|
* Check only frozen source-input paths for staged, unstaged or untracked
|
|
277
292
|
* changes. Unrelated dirty files remain allowed.
|
|
@@ -310,11 +325,21 @@ export async function checkSourceInputDirty({ closure, root, execFn = execFile }
|
|
|
310
325
|
if (record.length < 4) continue;
|
|
311
326
|
const status = record.slice(0, 2);
|
|
312
327
|
const recordPath = record.slice(3);
|
|
328
|
+
let renamedPath = '';
|
|
329
|
+
if (status.startsWith('R') || status.startsWith('C')) {
|
|
330
|
+
renamedPath = records[index + 1] ?? '';
|
|
331
|
+
index += 1;
|
|
332
|
+
}
|
|
313
333
|
// Ignored inputs are still untracked inputs. A file explicitly selected
|
|
314
334
|
// for publication must not escape the prepare gate merely because a
|
|
315
|
-
// .gitignore rule hides it from ordinary status output.
|
|
316
|
-
|
|
317
|
-
|
|
335
|
+
// .gitignore rule hides it from ordinary status output. A porcelain row
|
|
336
|
+
// counts only when its path shares a full directory segment with a
|
|
337
|
+
// requested closure path. A character prefix is not a selection.
|
|
338
|
+
for (const candidate of [recordPath, renamedPath]) {
|
|
339
|
+
if (candidate && paths.some((requested) => sharesDirectoryBoundary(candidate, requested))) {
|
|
340
|
+
dirtyPaths.push(candidate);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
318
343
|
}
|
|
319
344
|
return {
|
|
320
345
|
dirty: dirtyPaths.length > 0,
|
|
@@ -84,6 +84,37 @@ function checkTarget({ field, target, fileIndex, requireDotSlash = false }) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Identity mapping whose only star is the final character, for example
|
|
88
|
+
// "./schemas/*" -> "./schemas/*". Node replaces that star with any non-empty
|
|
89
|
+
// string, including "/". Other shapes stay rejected.
|
|
90
|
+
function isDirectTrailingStarMapping(key, value) {
|
|
91
|
+
return typeof value === 'string'
|
|
92
|
+
&& key === value
|
|
93
|
+
&& key.startsWith('./')
|
|
94
|
+
&& key.indexOf('*') === key.length - 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateTrailingStarPrefix(pattern) {
|
|
98
|
+
const head = pattern.slice(0, -1);
|
|
99
|
+
if (head === './') return {};
|
|
100
|
+
const staticTarget = head.endsWith('/') ? head.slice(0, -1) : head;
|
|
101
|
+
const validated = validateRelativeTarget(staticTarget, 'exports', { requireDotSlash: true });
|
|
102
|
+
if (validated.error) return validated;
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function matchingIndexPaths(pattern, fileIndex) {
|
|
107
|
+
const prefix = pattern.slice(0, -1);
|
|
108
|
+
const matched = [];
|
|
109
|
+
for (const path of fileIndex.keys()) {
|
|
110
|
+
if (typeof path !== 'string') continue;
|
|
111
|
+
const subpath = `./${path}`;
|
|
112
|
+
if (subpath.startsWith(prefix) && subpath.length >= pattern.length) matched.push(path);
|
|
113
|
+
}
|
|
114
|
+
matched.sort();
|
|
115
|
+
return matched;
|
|
116
|
+
}
|
|
117
|
+
|
|
87
118
|
function collectExports(value, location, result) {
|
|
88
119
|
if (value === null) return;
|
|
89
120
|
if (typeof value === 'string') {
|
|
@@ -117,14 +148,18 @@ function collectExports(value, location, result) {
|
|
|
117
148
|
for (const [key, child] of Object.entries(value)) {
|
|
118
149
|
const childLocation = `${location}.${key}`;
|
|
119
150
|
if (key.includes('*')) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
151
|
+
if (isDirectTrailingStarMapping(key, child)) {
|
|
152
|
+
result.patterns.push({ location: childLocation, pattern: key });
|
|
153
|
+
} else {
|
|
154
|
+
result.errors.push(
|
|
155
|
+
entryError(
|
|
156
|
+
'exports',
|
|
157
|
+
key,
|
|
158
|
+
'unsupported_entry_shape',
|
|
159
|
+
`${childLocation} uses a wildcard subpath that the static gate cannot verify`,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
128
163
|
continue;
|
|
129
164
|
}
|
|
130
165
|
collectExports(child, childLocation, result);
|
|
@@ -166,7 +201,7 @@ export function checkNpmEntryClosure(manifest, fileIndex) {
|
|
|
166
201
|
}
|
|
167
202
|
|
|
168
203
|
if (manifest.exports !== undefined) {
|
|
169
|
-
const exportsResult = { targets: [], errors: [], diagnostics: [] };
|
|
204
|
+
const exportsResult = { targets: [], patterns: [], errors: [], diagnostics: [] };
|
|
170
205
|
collectExports(manifest.exports, 'exports', exportsResult);
|
|
171
206
|
result.errors.push(...exportsResult.errors);
|
|
172
207
|
result.diagnostics.push(...exportsResult.diagnostics);
|
|
@@ -180,6 +215,33 @@ export function checkNpmEntryClosure(manifest, fileIndex) {
|
|
|
180
215
|
result.entries.push(...checked.entries);
|
|
181
216
|
result.errors.push(...checked.errors);
|
|
182
217
|
}
|
|
218
|
+
for (const pattern of exportsResult.patterns) {
|
|
219
|
+
const validated = validateTrailingStarPrefix(pattern.pattern);
|
|
220
|
+
if (validated.error) {
|
|
221
|
+
result.errors.push(validated.error);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const matched = matchingIndexPaths(pattern.pattern, fileIndex);
|
|
225
|
+
if (matched.length === 0) {
|
|
226
|
+
result.errors.push(entryError(
|
|
227
|
+
'exports',
|
|
228
|
+
pattern.pattern,
|
|
229
|
+
'empty_pattern_match',
|
|
230
|
+
`${pattern.location} matched no files in the package file index`,
|
|
231
|
+
));
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
for (const path of matched) {
|
|
235
|
+
const checked = checkTarget({
|
|
236
|
+
field: 'exports',
|
|
237
|
+
target: `./${path}`,
|
|
238
|
+
fileIndex,
|
|
239
|
+
requireDotSlash: true,
|
|
240
|
+
});
|
|
241
|
+
result.entries.push(...checked.entries);
|
|
242
|
+
result.errors.push(...checked.errors);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
183
245
|
}
|
|
184
246
|
|
|
185
247
|
return result;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { readdir, mkdir, lstat, realpath, open, rm } from 'node:fs/promises';
|
|
23
23
|
import { constants as fsConstants } from 'node:fs';
|
|
24
24
|
import { join, dirname, relative, isAbsolute, sep, resolve, posix as pathPosix } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
25
26
|
|
|
26
27
|
import { sha256Hex } from '../core/digest.mjs';
|
|
27
28
|
import { digestDocument } from 'skill-family-contracts';
|
|
@@ -713,7 +714,7 @@ async function generateAdapterFiles(platform, skills, templateJson, root, market
|
|
|
713
714
|
* @param {string} [options.platformFilter] - Only generate for this platform name.
|
|
714
715
|
*/
|
|
715
716
|
export async function produceBuildAdapters({ inputs, output, outputRoots, platformFilter } = {}) {
|
|
716
|
-
const defaultRoot = new URL('../..', import.meta.url)
|
|
717
|
+
const defaultRoot = fileURLToPath(new URL('../..', import.meta.url));
|
|
717
718
|
const requestedRoot = inputs ?? defaultRoot;
|
|
718
719
|
|
|
719
720
|
// ── Phase 1: Validate input root is not a symlink ──
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { readFile, writeFile, readdir, stat, mkdir } from 'node:fs/promises';
|
|
15
15
|
import { join, relative } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
16
17
|
|
|
17
18
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
18
19
|
import { digestDocument } from 'skill-family-contracts';
|
|
@@ -111,7 +112,7 @@ async function renderDir(srcDir, outDir, pathPrefix) {
|
|
|
111
112
|
* @returns {Promise<{outputs: Array, outputManifestDigest: string}>}
|
|
112
113
|
*/
|
|
113
114
|
export async function produceRenderPublicAssets({ inputs, output } = {}) {
|
|
114
|
-
const defaultRoot = new URL('../../..', import.meta.url)
|
|
115
|
+
const defaultRoot = fileURLToPath(new URL('../../..', import.meta.url));
|
|
115
116
|
const root = inputs ?? defaultRoot;
|
|
116
117
|
|
|
117
118
|
const srcDirs = [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
15
16
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
16
17
|
import { digestDocument } from 'skill-family-contracts';
|
|
@@ -44,7 +45,7 @@ export function computeSyncSkillsDigest(sourceBytes) {
|
|
|
44
45
|
* @param {string} options.output - Output directory path.
|
|
45
46
|
*/
|
|
46
47
|
export async function produceSyncSkills({ inputs, output } = {}) {
|
|
47
|
-
const defaultSrc = new URL('../../skills-src', import.meta.url)
|
|
48
|
+
const defaultSrc = fileURLToPath(new URL('../../skills-src', import.meta.url));
|
|
48
49
|
const srcDir = inputs ?? defaultSrc;
|
|
49
50
|
|
|
50
51
|
const skillDirs = [];
|
package/src/snapshot/frozen.mjs
CHANGED
|
@@ -199,9 +199,10 @@ export async function verifyFrozenDirectoryStructure(directory, label = 'frozen
|
|
|
199
199
|
*
|
|
200
200
|
* Consumption only: this never rebuilds objects and never reads the wall
|
|
201
201
|
* clock. When `commitTimestamp` (the plan's `frozenSnapshot.commitTimestamp`)
|
|
202
|
-
* is provided, the
|
|
203
|
-
*
|
|
204
|
-
*
|
|
202
|
+
* is provided, the plan canonical form remains `+00:00`. Git may read the
|
|
203
|
+
* same UTC instant back as `Z` via `%aI`/`%cI`. Both sides pass through
|
|
204
|
+
* `normalizeGitTimestamp` before comparison, so a commit carrying any other
|
|
205
|
+
* author/committer instant can never pass pre-publish verification.
|
|
205
206
|
*/
|
|
206
207
|
export async function verifyFrozenGitRepository({
|
|
207
208
|
root,
|
|
@@ -225,13 +226,15 @@ export async function verifyFrozenGitRepository({
|
|
|
225
226
|
['--git-dir', gitDir, 'show', '-s', '--format=%aI%n%cI', commit],
|
|
226
227
|
{ shell: false },
|
|
227
228
|
);
|
|
228
|
-
const [
|
|
229
|
+
const [rawAuthorDate, rawCommitterDate] = datesOut.trim().split('\n');
|
|
230
|
+
const authorDate = normalizeGitTimestamp(rawAuthorDate, 'frozen commit author date');
|
|
231
|
+
const committerDate = normalizeGitTimestamp(rawCommitterDate, 'frozen commit committer date');
|
|
229
232
|
if (authorDate !== expectedTimestamp || committerDate !== expectedTimestamp) {
|
|
230
233
|
throw frozenError('frozen git commit dates do not match the plan freeze timestamp', {
|
|
231
234
|
commit,
|
|
232
235
|
expectedTimestamp,
|
|
233
|
-
observedAuthorDate:
|
|
234
|
-
observedCommitterDate:
|
|
236
|
+
observedAuthorDate: rawAuthorDate,
|
|
237
|
+
observedCommitterDate: rawCommitterDate,
|
|
235
238
|
});
|
|
236
239
|
}
|
|
237
240
|
}
|
|
@@ -314,9 +317,10 @@ const GIT_ISO8601_STRICT_RE =
|
|
|
314
317
|
*
|
|
315
318
|
* The value must be a non-empty, strict ISO 8601 timestamp with an explicit UTC
|
|
316
319
|
* offset and a parseable calendar date. It is normalized to UTC with second
|
|
317
|
-
* precision and a `+00:00` offset
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
+
* precision and a `+00:00` offset. The plan canonical form remains `+00:00`.
|
|
321
|
+
* Git may read the same UTC instant back as `Z` via `%aI`/`%cI`; both sides
|
|
322
|
+
* pass through `normalizeGitTimestamp` before comparison. Missing, empty,
|
|
323
|
+
* malformed, or out-of-range values throw a closed `GATE_FAILED` error.
|
|
320
324
|
*
|
|
321
325
|
* Normalization is idempotent: re-normalizing an already-canonical value returns
|
|
322
326
|
* the identical string, so prepare and the Git builder always agree byte-for-byte.
|
|
@@ -399,11 +403,12 @@ function publicGitRemote({ repo, githubHost = 'github.com' }) {
|
|
|
399
403
|
* `commitTimestamp` is the plan freeze timestamp sampled exactly once during
|
|
400
404
|
* production prepare. It is strictly validated BEFORE any filesystem or Git
|
|
401
405
|
* write (missing, empty, malformed, or impossible values fail closed), then
|
|
402
|
-
* written verbatim to both GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
|
|
403
|
-
*
|
|
404
|
-
* `
|
|
405
|
-
* yield identical tree and commit
|
|
406
|
-
*
|
|
406
|
+
* written verbatim to both GIT_AUTHOR_DATE and GIT_COMMITTER_DATE. The plan
|
|
407
|
+
* canonical form remains `+00:00`. Git may read the same UTC instant back as
|
|
408
|
+
* `Z` via `%aI`/`%cI`; both sides pass through `normalizeGitTimestamp` before
|
|
409
|
+
* comparison. Identical inputs therefore yield identical tree and commit
|
|
410
|
+
* identifiers, and a different freeze timestamp alone yields a different
|
|
411
|
+
* commit with an identical tree.
|
|
407
412
|
*/
|
|
408
413
|
export async function buildFrozenGitRepository({
|
|
409
414
|
snapshotDir,
|