release-skill 0.6.1 → 0.6.2
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 +17 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +12 -9
- package/README.zh-CN.md +12 -9
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +326 -70
- package/adapters/claude/schemas/release-plan.schema.json +9 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +326 -70
- package/adapters/codex/schemas/release-plan.schema.json +9 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +326 -70
- package/adapters/kimi/schemas/release-plan.schema.json +9 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +326 -70
- package/adapters/workbuddy/schemas/release-plan.schema.json +9 -0
- package/bin/release-skill.bundle.mjs +326 -70
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +9 -0
- package/src/commands/lineage.mjs +101 -32
- package/src/commands/prepare.mjs +59 -1
- package/src/commands/publish.mjs +10 -1
- package/src/core/skill-resource-closure.mjs +240 -10
- package/src/platforms/registry.mjs +12 -0
|
@@ -14,7 +14,7 @@ import { canonicalJson, digestDocument } from 'skill-family-contracts';
|
|
|
14
14
|
import { GATE_FAILED, ReleaseError } from './errors.mjs';
|
|
15
15
|
import { computeFrozenSnapshot } from '../snapshot/frozen.mjs';
|
|
16
16
|
|
|
17
|
-
export const CHECKER_VERSION = 'skill-resource-closure-
|
|
17
|
+
export const CHECKER_VERSION = 'skill-resource-closure-v2';
|
|
18
18
|
|
|
19
19
|
const BARE_PREFIXES = ['references/', 'assets/', 'schemas/', 'examples/', 'scripts/'];
|
|
20
20
|
const EXPLICIT_PREFIXES = [
|
|
@@ -27,7 +27,20 @@ const EXPLICIT_PREFIXES = [
|
|
|
27
27
|
];
|
|
28
28
|
const SOURCE_TREE_PATTERN = /(?:^|\/)packages\/[A-Za-z0-9._-]+(?:\/|$)/u;
|
|
29
29
|
const MACHINE_ABSOLUTE_PATTERN = /^(?:\/(?:Users|home|root|tmp|var|etc|opt)(?:\/|$)|[A-Za-z]:[\\/])/u;
|
|
30
|
+
// G2: user-home search paths (`~/...`, `$HOME/...`, `${HOME}/...`) with at
|
|
31
|
+
// least one trailing path character. Only code-span/fence/link tokens reach
|
|
32
|
+
// isPathToken, so prose words are never matched; fail-closed over silent miss.
|
|
33
|
+
const HOME_DIRECTORY_PATTERN = /^(?:~|\$HOME|\$\{HOME\})\/.+/u;
|
|
34
|
+
const ADAPTER_SURFACE_PATTERN = /^adapters\/([^/]+)$/u;
|
|
35
|
+
// G3: skill-local resource closure directories. Regular files inside these
|
|
36
|
+
// directories (under a skill directory) must be referenced by a SKILL.md of
|
|
37
|
+
// the same surface or they are reported as stale on adapter surfaces.
|
|
38
|
+
const CLOSURE_RESOURCE_DIRS = Object.freeze(['references', 'assets', 'schemas', 'examples', 'scripts']);
|
|
30
39
|
const GLOB_PATTERN = /[*?\[]/u;
|
|
40
|
+
// D3: the source-only exemption is scoped to the code snippet that carries the
|
|
41
|
+
// marker — the same fence line, inline code span, or link target from which
|
|
42
|
+
// the token was extracted. A prose marker elsewhere on the line exempts
|
|
43
|
+
// nothing (fail-closed over a line-wide false pass).
|
|
31
44
|
const SOURCE_ONLY_MARKER = /(?:\bsource-only\b|仅源码包)/iu;
|
|
32
45
|
const TRANSPORT_EXCLUSIONS = Object.freeze([
|
|
33
46
|
'.git',
|
|
@@ -41,7 +54,10 @@ export const CLASSIFICATION = Object.freeze({
|
|
|
41
54
|
PLUGIN_ROOT: 'plugin_root',
|
|
42
55
|
SOURCE_BACKJUMP: 'source_backjump',
|
|
43
56
|
MACHINE_ABSOLUTE: 'machine_absolute',
|
|
57
|
+
HOME_DIRECTORY: 'home_directory',
|
|
44
58
|
OUT_OF_BOUNDS: 'out_of_bounds',
|
|
59
|
+
RESOURCE_DRIFT: 'resource_drift',
|
|
60
|
+
STALE_RESOURCE: 'stale_resource',
|
|
45
61
|
});
|
|
46
62
|
|
|
47
63
|
export const FINDING_CODE = Object.freeze({
|
|
@@ -50,8 +66,11 @@ export const FINDING_CODE = Object.freeze({
|
|
|
50
66
|
RESOURCE_NOT_REGULAR_FILE: 'RESOURCE_NOT_REGULAR_FILE',
|
|
51
67
|
SOURCE_BACKJUMP: 'SOURCE_BACKJUMP',
|
|
52
68
|
MACHINE_ABSOLUTE_PATH: 'MACHINE_ABSOLUTE_PATH',
|
|
69
|
+
HOME_DIRECTORY_SEARCH: 'HOME_DIRECTORY_SEARCH',
|
|
53
70
|
OUT_OF_BOUNDS: 'OUT_OF_BOUNDS',
|
|
54
71
|
SYMLINK_NOT_ALLOWED: 'SYMLINK_NOT_ALLOWED',
|
|
72
|
+
RESOURCE_DRIFT: 'RESOURCE_DRIFT',
|
|
73
|
+
STALE_RESOURCE: 'STALE_RESOURCE',
|
|
55
74
|
});
|
|
56
75
|
|
|
57
76
|
function toPosix(value) {
|
|
@@ -115,6 +134,7 @@ function isPathToken(value) {
|
|
|
115
134
|
if (!token || GLOB_PATTERN.test(token)) return false;
|
|
116
135
|
return BARE_PREFIXES.some((prefix) => token.startsWith(prefix))
|
|
117
136
|
|| EXPLICIT_PREFIXES.some((prefix) => token.startsWith(prefix))
|
|
137
|
+
|| HOME_DIRECTORY_PATTERN.test(token)
|
|
118
138
|
|| SOURCE_TREE_PATTERN.test(token)
|
|
119
139
|
|| MACHINE_ABSOLUTE_PATTERN.test(token);
|
|
120
140
|
}
|
|
@@ -144,16 +164,21 @@ export function extractPathTokens(content) {
|
|
|
144
164
|
inFence = !inFence;
|
|
145
165
|
continue;
|
|
146
166
|
}
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
// D3: the source-only marker is evaluated per code snippet, never per
|
|
168
|
+
// line. Only tokens extracted from the very snippet text that contains
|
|
169
|
+
// the marker (this fence line, this inline code span, this link target)
|
|
170
|
+
// are exempt; unrelated tokens elsewhere on the line stay fail-closed.
|
|
171
|
+
if (inFence) scanSnippet(line, lineNumber, SOURCE_ONLY_MARKER.test(line), results, seen);
|
|
149
172
|
|
|
150
173
|
for (const match of line.matchAll(/`([^`]+)`/gu)) {
|
|
151
|
-
scanSnippet(match[1], lineNumber,
|
|
174
|
+
scanSnippet(match[1], lineNumber, SOURCE_ONLY_MARKER.test(match[1]), results, seen);
|
|
152
175
|
}
|
|
153
176
|
for (const match of line.matchAll(/\]\(([^)]+)\)/gu)) {
|
|
154
177
|
const target = match[1].trim().split(/[?#]/u)[0];
|
|
155
178
|
if (!/^(?:https?:|#)/iu.test(target)) {
|
|
156
|
-
|
|
179
|
+
// The whole link-target syntax is the code context for the marker
|
|
180
|
+
// (query/fragment are stripped only for path resolution).
|
|
181
|
+
scanSnippet(target, lineNumber, SOURCE_ONLY_MARKER.test(match[1]), results, seen);
|
|
157
182
|
}
|
|
158
183
|
}
|
|
159
184
|
}
|
|
@@ -176,6 +201,14 @@ export function classifyReference(token, skillRoot, pluginRoot, scanRoot) {
|
|
|
176
201
|
absoluteTarget: token,
|
|
177
202
|
};
|
|
178
203
|
}
|
|
204
|
+
if (HOME_DIRECTORY_PATTERN.test(token)) {
|
|
205
|
+
return {
|
|
206
|
+
classification: CLASSIFICATION.HOME_DIRECTORY,
|
|
207
|
+
resolutionRoot: '(home)',
|
|
208
|
+
resolvedTarget: token,
|
|
209
|
+
absoluteTarget: token,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
179
212
|
if (SOURCE_TREE_PATTERN.test(token)) {
|
|
180
213
|
return {
|
|
181
214
|
classification: CLASSIFICATION.SOURCE_BACKJUMP,
|
|
@@ -226,8 +259,29 @@ async function inspectRegularFile(root, target) {
|
|
|
226
259
|
return { code: null };
|
|
227
260
|
}
|
|
228
261
|
|
|
262
|
+
async function collectRegularFiles(dir, results) {
|
|
263
|
+
let entries;
|
|
264
|
+
try {
|
|
265
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error.code === 'ENOENT') return;
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
271
|
+
// Symlinks are owned by the snapshot safety gate (SNAPSHOT_UNSAFE); the
|
|
272
|
+
// stale scan never follows or reports them.
|
|
273
|
+
if (entry.isSymbolicLink()) continue;
|
|
274
|
+
const absolute = join(dir, entry.name);
|
|
275
|
+
if (entry.isDirectory()) {
|
|
276
|
+
await collectRegularFiles(absolute, results);
|
|
277
|
+
} else if (entry.isFile()) {
|
|
278
|
+
results.push(absolute);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
229
283
|
function inferSurfaceHost(surfaceId, defaultHost) {
|
|
230
|
-
const match =
|
|
284
|
+
const match = ADAPTER_SURFACE_PATTERN.exec(surfaceId);
|
|
231
285
|
return match?.[1] ?? defaultHost;
|
|
232
286
|
}
|
|
233
287
|
|
|
@@ -239,6 +293,7 @@ function receiptProjection(result) {
|
|
|
239
293
|
skillCount: result.skillCount,
|
|
240
294
|
referenceCount: result.referenceCount,
|
|
241
295
|
sourceOnlyCount: result.sourceOnlyCount,
|
|
296
|
+
sourceOnlyReferences: result.sourceOnlyReferences,
|
|
242
297
|
findingCount: result.findings.length,
|
|
243
298
|
};
|
|
244
299
|
}
|
|
@@ -328,9 +383,31 @@ export async function checkSkillResourceClosure({
|
|
|
328
383
|
const skillPaths = await discoverSkills(scanRoot);
|
|
329
384
|
const findings = [];
|
|
330
385
|
const surfaceMap = new Map();
|
|
386
|
+
const skillsBySurface = new Map();
|
|
387
|
+
// G3: successfully resolved targets per surface. resolvedResources maps the
|
|
388
|
+
// surface-relative resource path to its absolute target (drift comparison);
|
|
389
|
+
// referencedTargets holds every absolute target some SKILL.md of the surface
|
|
390
|
+
// resolved (stale exemption). resourceReferences records, per surface and
|
|
391
|
+
// surface-relative resource path, the skill/line that referenced it (D4:
|
|
392
|
+
// localizes RESOURCE_DRIFT findings). All stay out of the receipt projection.
|
|
393
|
+
const resolvedResourcesBySurface = new Map();
|
|
394
|
+
const referencedTargetsBySurface = new Map();
|
|
395
|
+
const resourceReferencesBySurface = new Map();
|
|
396
|
+
const sourceOnlyReferences = [];
|
|
331
397
|
let referenceCount = 0;
|
|
332
398
|
let sourceOnlyCount = 0;
|
|
333
399
|
|
|
400
|
+
const recordSourceOnlyExemption = (surface, skill, pathToken) => {
|
|
401
|
+
sourceOnlyCount += 1;
|
|
402
|
+
surface.sourceOnlyCount += 1;
|
|
403
|
+
sourceOnlyReferences.push({
|
|
404
|
+
skill,
|
|
405
|
+
line: pathToken.line,
|
|
406
|
+
reference: pathToken.token,
|
|
407
|
+
surface: surface.id,
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
|
|
334
411
|
for (const skill of skillPaths) {
|
|
335
412
|
const skillRoot = resolveSkillRoot(skill, scanRoot);
|
|
336
413
|
const pluginRoot = resolvePluginRoot(skill, scanRoot);
|
|
@@ -345,6 +422,8 @@ export async function checkSkillResourceClosure({
|
|
|
345
422
|
};
|
|
346
423
|
surface.skillCount += 1;
|
|
347
424
|
surfaceMap.set(surfaceId, surface);
|
|
425
|
+
if (!skillsBySurface.has(surfaceId)) skillsBySurface.set(surfaceId, []);
|
|
426
|
+
skillsBySurface.get(surfaceId).push(skill);
|
|
348
427
|
|
|
349
428
|
const content = await readFile(resolve(scanRoot, skill), 'utf8');
|
|
350
429
|
for (const pathToken of extractPathTokens(content)) {
|
|
@@ -369,8 +448,7 @@ export async function checkSkillResourceClosure({
|
|
|
369
448
|
|
|
370
449
|
if (classification.classification === CLASSIFICATION.SOURCE_BACKJUMP) {
|
|
371
450
|
if (pathToken.sourceOnly) {
|
|
372
|
-
|
|
373
|
-
surface.sourceOnlyCount += 1;
|
|
451
|
+
recordSourceOnlyExemption(surface, skill, pathToken);
|
|
374
452
|
} else {
|
|
375
453
|
findings.push({ ...findingBase, code: FINDING_CODE.SOURCE_BACKJUMP });
|
|
376
454
|
}
|
|
@@ -380,6 +458,10 @@ export async function checkSkillResourceClosure({
|
|
|
380
458
|
findings.push({ ...findingBase, code: FINDING_CODE.MACHINE_ABSOLUTE_PATH });
|
|
381
459
|
continue;
|
|
382
460
|
}
|
|
461
|
+
if (classification.classification === CLASSIFICATION.HOME_DIRECTORY) {
|
|
462
|
+
findings.push({ ...findingBase, code: FINDING_CODE.HOME_DIRECTORY_SEARCH });
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
383
465
|
if (classification.classification === CLASSIFICATION.OUT_OF_BOUNDS) {
|
|
384
466
|
findings.push({ ...findingBase, code: FINDING_CODE.OUT_OF_BOUNDS });
|
|
385
467
|
continue;
|
|
@@ -394,15 +476,138 @@ export async function checkSkillResourceClosure({
|
|
|
394
476
|
);
|
|
395
477
|
if (inspection.code) {
|
|
396
478
|
if (pathToken.sourceOnly && inspection.code === FINDING_CODE.RESOURCE_MISSING) {
|
|
397
|
-
|
|
398
|
-
surface.sourceOnlyCount += 1;
|
|
479
|
+
recordSourceOnlyExemption(surface, skill, pathToken);
|
|
399
480
|
} else {
|
|
400
481
|
findings.push({ ...findingBase, code: inspection.code });
|
|
401
482
|
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const relativeResource = relativeStable(pluginRoot, classification.absoluteTarget);
|
|
487
|
+
if (!resolvedResourcesBySurface.has(surfaceId)) {
|
|
488
|
+
resolvedResourcesBySurface.set(surfaceId, new Map());
|
|
489
|
+
referencedTargetsBySurface.set(surfaceId, new Set());
|
|
490
|
+
resourceReferencesBySurface.set(surfaceId, new Map());
|
|
491
|
+
}
|
|
492
|
+
resolvedResourcesBySurface.get(surfaceId).set(relativeResource, classification.absoluteTarget);
|
|
493
|
+
referencedTargetsBySurface.get(surfaceId).add(classification.absoluteTarget);
|
|
494
|
+
const resourceReferences = resourceReferencesBySurface.get(surfaceId);
|
|
495
|
+
if (!resourceReferences.has(relativeResource)) resourceReferences.set(relativeResource, []);
|
|
496
|
+
resourceReferences.get(relativeResource).push({
|
|
497
|
+
surface: surfaceId,
|
|
498
|
+
skill,
|
|
499
|
+
line: pathToken.line,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// G3 RESOURCE_DRIFT: the same surface-relative resource path resolved on two
|
|
505
|
+
// surfaces must be byte-identical (typical case: root vs adapters/<name>
|
|
506
|
+
// projections of the same skill resource). Content digests are cached per
|
|
507
|
+
// absolute target.
|
|
508
|
+
const contentDigestCache = new Map();
|
|
509
|
+
const digestOfTarget = async (absoluteTarget) => {
|
|
510
|
+
if (!contentDigestCache.has(absoluteTarget)) {
|
|
511
|
+
contentDigestCache.set(
|
|
512
|
+
absoluteTarget,
|
|
513
|
+
digestDocument({ content: await readFile(absoluteTarget, 'utf8') }),
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return contentDigestCache.get(absoluteTarget);
|
|
517
|
+
};
|
|
518
|
+
const surfaceIds = [...resolvedResourcesBySurface.keys()].sort((a, b) => a.localeCompare(b));
|
|
519
|
+
for (let i = 0; i < surfaceIds.length; i += 1) {
|
|
520
|
+
for (let j = i + 1; j < surfaceIds.length; j += 1) {
|
|
521
|
+
const [leftId, rightId] = [surfaceIds[i], surfaceIds[j]];
|
|
522
|
+
const left = resolvedResourcesBySurface.get(leftId);
|
|
523
|
+
const right = resolvedResourcesBySurface.get(rightId);
|
|
524
|
+
const commonPaths = [...left.keys()]
|
|
525
|
+
.filter((path) => right.has(path))
|
|
526
|
+
.sort((a, b) => a.localeCompare(b));
|
|
527
|
+
for (const resourcePath of commonPaths) {
|
|
528
|
+
const [leftDigest, rightDigest] = await Promise.all([
|
|
529
|
+
digestOfTarget(left.get(resourcePath)),
|
|
530
|
+
digestOfTarget(right.get(resourcePath)),
|
|
531
|
+
]);
|
|
532
|
+
if (leftDigest === rightDigest) continue;
|
|
533
|
+
const rightSurface = surfaceMap.get(rightId);
|
|
534
|
+
// D4: localize the drift — list every skill/line on both surfaces that
|
|
535
|
+
// resolved this resource path (deduped, deterministically sorted).
|
|
536
|
+
// Findings never enter the receipt projection, so this is digest-safe.
|
|
537
|
+
const driftReferences = [];
|
|
538
|
+
const seenReference = new Set();
|
|
539
|
+
for (const surfaceId of [leftId, rightId]) {
|
|
540
|
+
for (const ref of resourceReferencesBySurface.get(surfaceId)?.get(resourcePath) ?? []) {
|
|
541
|
+
const key = `${ref.surface}\u0000${ref.skill}\u0000${ref.line}`;
|
|
542
|
+
if (seenReference.has(key)) continue;
|
|
543
|
+
seenReference.add(key);
|
|
544
|
+
driftReferences.push({ surface: ref.surface, skill: ref.skill, line: ref.line });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
driftReferences.sort((a, b) => (
|
|
548
|
+
a.surface.localeCompare(b.surface)
|
|
549
|
+
|| a.skill.localeCompare(b.skill)
|
|
550
|
+
|| (a.line - b.line)
|
|
551
|
+
));
|
|
552
|
+
findings.push({
|
|
553
|
+
host: rightSurface.host,
|
|
554
|
+
surface: rightId,
|
|
555
|
+
skill: null,
|
|
556
|
+
line: null,
|
|
557
|
+
reference: resourcePath,
|
|
558
|
+
classification: CLASSIFICATION.RESOURCE_DRIFT,
|
|
559
|
+
resolutionRoot: rightId,
|
|
560
|
+
resolvedTarget: resourcePath,
|
|
561
|
+
surfaces: [leftId, rightId],
|
|
562
|
+
references: driftReferences,
|
|
563
|
+
code: FINDING_CODE.RESOURCE_DRIFT,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// G3 STALE_RESOURCE: on adapter surfaces only, every regular file inside a
|
|
570
|
+
// skill's resource closure directories must be referenced by some SKILL.md
|
|
571
|
+
// of the same surface (bare or explicit plugin-root form). The root surface
|
|
572
|
+
// is exempt (repository roots carry many legitimate unreferenced files).
|
|
573
|
+
for (const surfaceId of [...skillsBySurface.keys()].sort((a, b) => a.localeCompare(b))) {
|
|
574
|
+
if (!ADAPTER_SURFACE_PATTERN.test(surfaceId)) continue;
|
|
575
|
+
const surface = surfaceMap.get(surfaceId);
|
|
576
|
+
const surfaceRootAbsolute = resolve(scanRoot, surfaceId);
|
|
577
|
+
const referencedTargets = referencedTargetsBySurface.get(surfaceId) ?? new Set();
|
|
578
|
+
const stale = [];
|
|
579
|
+
for (const skill of skillsBySurface.get(surfaceId)) {
|
|
580
|
+
const skillDir = resolve(scanRoot, dirname(skill));
|
|
581
|
+
for (const closureDir of CLOSURE_RESOURCE_DIRS) {
|
|
582
|
+
const files = [];
|
|
583
|
+
await collectRegularFiles(join(skillDir, closureDir), files);
|
|
584
|
+
for (const absolute of files) {
|
|
585
|
+
if (referencedTargets.has(absolute)) continue;
|
|
586
|
+
const relativeResource = relativeStable(surfaceRootAbsolute, absolute);
|
|
587
|
+
stale.push({
|
|
588
|
+
host: surface.host,
|
|
589
|
+
surface: surfaceId,
|
|
590
|
+
skill,
|
|
591
|
+
line: null,
|
|
592
|
+
reference: relativeResource,
|
|
593
|
+
classification: CLASSIFICATION.STALE_RESOURCE,
|
|
594
|
+
resolutionRoot: surfaceId,
|
|
595
|
+
resolvedTarget: relativeResource,
|
|
596
|
+
code: FINDING_CODE.STALE_RESOURCE,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
402
599
|
}
|
|
403
600
|
}
|
|
601
|
+
stale.sort((a, b) => (a.skill.localeCompare(b.skill) || a.resolvedTarget.localeCompare(b.resolvedTarget)));
|
|
602
|
+
findings.push(...stale);
|
|
404
603
|
}
|
|
405
604
|
|
|
605
|
+
sourceOnlyReferences.sort((a, b) => (
|
|
606
|
+
a.skill.localeCompare(b.skill)
|
|
607
|
+
|| (a.line - b.line)
|
|
608
|
+
|| a.reference.localeCompare(b.reference)
|
|
609
|
+
));
|
|
610
|
+
|
|
406
611
|
if (snapshotError) {
|
|
407
612
|
findings.push({
|
|
408
613
|
host,
|
|
@@ -426,8 +631,33 @@ export async function checkSkillResourceClosure({
|
|
|
426
631
|
skillCount: skillPaths.length,
|
|
427
632
|
referenceCount,
|
|
428
633
|
sourceOnlyCount,
|
|
634
|
+
sourceOnlyReferences,
|
|
429
635
|
findings,
|
|
430
636
|
};
|
|
431
637
|
result.receiptDigest = digestDocument(receiptProjection(result));
|
|
432
638
|
return result;
|
|
433
639
|
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* G4: reconcile declared plugin distributions against observed surfaces.
|
|
643
|
+
*
|
|
644
|
+
* Every declared plugin host must be present among the receipt surfaces with
|
|
645
|
+
* at least one skill; a host whose adapter tree was dropped from publicFiles
|
|
646
|
+
* (or whose projection produced no skills) must fail closed at prepare.
|
|
647
|
+
* `expectedHosts` are adapter directory names (e.g. `codebuddy-plugin`
|
|
648
|
+
* expects the historical `workbuddy` adapter directory).
|
|
649
|
+
*
|
|
650
|
+
* @param {string[]} expectedHosts - declared plugin host surface names
|
|
651
|
+
* @param {Array<{ id: string, host: string, skillCount: number }>} surfaces
|
|
652
|
+
* @returns {{ passed: boolean, missing: Array<{ host: string, skillCount: number }> }}
|
|
653
|
+
*/
|
|
654
|
+
export function evaluateDeclaredHostSurfaceCoverage(expectedHosts, surfaces) {
|
|
655
|
+
const missing = [];
|
|
656
|
+
for (const expectedHost of [...new Set(expectedHosts ?? [])].sort((a, b) => a.localeCompare(b))) {
|
|
657
|
+
const surface = (surfaces ?? []).find((item) => item.host === expectedHost);
|
|
658
|
+
if (!surface || !(surface.skillCount >= 1)) {
|
|
659
|
+
missing.push({ host: expectedHost, skillCount: surface?.skillCount ?? 0 });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { passed: missing.length === 0, missing };
|
|
663
|
+
}
|
|
@@ -179,6 +179,9 @@ const CLAUDE = Object.freeze({
|
|
|
179
179
|
schemaRequiredFields: Object.freeze(['plugin', 'marketplace', 'entrySkill']),
|
|
180
180
|
skillRendering: Object.freeze({ mode: 'verbatim', preamble: null, placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
|
|
181
181
|
buildAdapter: Object.freeze({
|
|
182
|
+
// D6: explicit adapter directory name (adapters/claude/); assertRegistry
|
|
183
|
+
// requires this on every platform.
|
|
184
|
+
name: 'claude',
|
|
182
185
|
pluginDirName: '.claude-plugin',
|
|
183
186
|
templateFileName: 'plugin.json',
|
|
184
187
|
marketplaceFileName: 'marketplace.json',
|
|
@@ -237,6 +240,8 @@ const CODEX = Object.freeze({
|
|
|
237
240
|
schemaRequiredFields: Object.freeze(['plugin', 'marketplace', 'entrySkill']),
|
|
238
241
|
skillRendering: Object.freeze({ mode: 'substitute', preamble: 'codex', placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
|
|
239
242
|
buildAdapter: Object.freeze({
|
|
243
|
+
// D6: explicit adapter directory name (adapters/codex/).
|
|
244
|
+
name: 'codex',
|
|
240
245
|
pluginDirName: '.codex-plugin',
|
|
241
246
|
templateFileName: 'plugin.json',
|
|
242
247
|
marketplaceFileName: null,
|
|
@@ -294,6 +299,8 @@ const KIMI = Object.freeze({
|
|
|
294
299
|
schemaRequiredFields: Object.freeze(['plugin', 'entrySkill']),
|
|
295
300
|
skillRendering: Object.freeze({ mode: 'substitute', preamble: 'kimi', placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
|
|
296
301
|
buildAdapter: Object.freeze({
|
|
302
|
+
// D6: explicit adapter directory name (adapters/kimi/).
|
|
303
|
+
name: 'kimi',
|
|
297
304
|
pluginDirName: '.kimi-plugin',
|
|
298
305
|
templateFileName: 'plugin.json',
|
|
299
306
|
marketplaceFileName: null,
|
|
@@ -478,6 +485,11 @@ export function assertRegistry(registry = PLATFORMS) {
|
|
|
478
485
|
if (!platform.buildAdapter || typeof platform.buildAdapter !== 'object') {
|
|
479
486
|
throw new Error(`platform registry: ${label} buildAdapter must be an object`);
|
|
480
487
|
}
|
|
488
|
+
// D6: the adapter directory name is always explicit — prepare's G4
|
|
489
|
+
// declared-host reconciliation and the build producer both key on it.
|
|
490
|
+
if (typeof platform.buildAdapter.name !== 'string' || platform.buildAdapter.name.length === 0) {
|
|
491
|
+
throw new Error(`platform registry: ${label} buildAdapter needs a non-empty name (its adapter directory name)`);
|
|
492
|
+
}
|
|
481
493
|
if (!VALID_LIST_OUTPUTS.has(platform.jsonProtocol?.listOutput)) {
|
|
482
494
|
throw new Error(`platform registry: ${label} has illegal jsonProtocol.listOutput "${platform.jsonProtocol?.listOutput}"`);
|
|
483
495
|
}
|