pi-codex-marketplace 0.1.0
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/LICENSE +21 -0
- package/README.md +134 -0
- package/extensions/pi/git-registration.ts +138 -0
- package/extensions/pi/index.ts +293 -0
- package/extensions/pi/installation.ts +90 -0
- package/extensions/pi/journal.ts +80 -0
- package/extensions/pi/lifecycle.ts +285 -0
- package/extensions/pi/registration.ts +143 -0
- package/extensions/pi/scope-overrides.ts +170 -0
- package/package.json +60 -0
- package/src/barrier/global-barrier.ts +105 -0
- package/src/bridge-state/atomic.ts +237 -0
- package/src/bridge-state/index.ts +5 -0
- package/src/bridge-state/migrate.ts +261 -0
- package/src/bridge-state/paths.ts +75 -0
- package/src/bridge-state/repair.ts +185 -0
- package/src/bridge-state/schema.ts +70 -0
- package/src/bridge-state/store.ts +489 -0
- package/src/bridge-state/types.ts +170 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/paths.ts +42 -0
- package/src/cache/source-cache.ts +365 -0
- package/src/compatibility/index.ts +1 -0
- package/src/compatibility/profile.ts +328 -0
- package/src/installation/flow.ts +443 -0
- package/src/installation/index.ts +1 -0
- package/src/installation/inspection.ts +129 -0
- package/src/journal/active-chains.ts +99 -0
- package/src/journal/index.ts +3 -0
- package/src/journal/journal.ts +215 -0
- package/src/journal/types.ts +49 -0
- package/src/lifecycle/index.ts +5 -0
- package/src/lifecycle/rebind.ts +290 -0
- package/src/lifecycle/refresh.ts +407 -0
- package/src/lifecycle/removal.ts +457 -0
- package/src/lifecycle/update-plan.ts +222 -0
- package/src/lifecycle/update.ts +303 -0
- package/src/projection/collision.ts +120 -0
- package/src/projection/effective-state.ts +182 -0
- package/src/projection/index.ts +4 -0
- package/src/projection/overrides.ts +230 -0
- package/src/projection/project.ts +359 -0
- package/src/reconciliation/startup.ts +144 -0
- package/src/registration/budget.ts +28 -0
- package/src/registration/catalog.ts +224 -0
- package/src/registration/contained.ts +140 -0
- package/src/registration/fence.ts +86 -0
- package/src/registration/findings.ts +188 -0
- package/src/registration/flow.ts +619 -0
- package/src/registration/git-acquisition.ts +481 -0
- package/src/registration/git-flow.ts +654 -0
- package/src/registration/git-locator.ts +380 -0
- package/src/registration/git-selector.ts +279 -0
- package/src/registration/index.ts +16 -0
- package/src/registration/receipt.ts +305 -0
- package/src/registration/registration.ts +102 -0
- package/src/registration/snapshot.ts +382 -0
- package/src/registration/source-key.ts +111 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility Profile v1 — closed, skills-only Codex Plugin classification.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately a pure validation seam: it reads one already-contained Plugin tree and
|
|
5
|
+
* produces an atomic Compatible / Incompatible / Invalid result. Runtime skill collisions are
|
|
6
|
+
* intentionally outside this module; they do not change whole-Plugin classification.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
11
|
+
import { join, relative, sep } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { parseFrontmatter } from '@earendil-works/pi-coding-agent';
|
|
14
|
+
|
|
15
|
+
import type { Scope } from '../bridge-state/types.js';
|
|
16
|
+
import { BUDGET } from '../registration/budget.js';
|
|
17
|
+
import { CODE, RULE, blocking, sortFindings, warning, type ValidationFinding } from '../registration/findings.js';
|
|
18
|
+
|
|
19
|
+
export type PluginClassification = 'compatible' | 'incompatible' | 'invalid';
|
|
20
|
+
export type InvocationPolicy = 'implicit' | 'explicit';
|
|
21
|
+
|
|
22
|
+
export interface CompatibleSkill {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
resources: string[];
|
|
27
|
+
invocationPolicy: InvocationPolicy;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CompatiblePlugin {
|
|
31
|
+
id: string;
|
|
32
|
+
manifestName: string;
|
|
33
|
+
marketplaceEntryId: string;
|
|
34
|
+
skills: CompatibleSkill[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClassificationResult {
|
|
38
|
+
classification: PluginClassification;
|
|
39
|
+
plugin?: CompatiblePlugin;
|
|
40
|
+
/** Valid manifest identity even when the complete Plugin is Invalid/Incompatible. */
|
|
41
|
+
identity?: string;
|
|
42
|
+
/** Hash of the exact manifest, descriptors, and resources used to derive this result. */
|
|
43
|
+
captureFingerprint: string;
|
|
44
|
+
findings: ValidationFinding[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ClassificationOptions {
|
|
48
|
+
scope: Scope;
|
|
49
|
+
marketplaceId: string;
|
|
50
|
+
marketplaceEntryId: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
54
|
+
const UNSUPPORTED_COMPONENTS = new Set(['apps', 'commands', 'hooks', 'mcp', 'mcpServers', 'servers', 'extensions']);
|
|
55
|
+
const INERT_MANIFEST_FIELDS = new Set(['version', 'description', 'author', 'homepage', 'repository', 'license', 'keywords', 'interface']);
|
|
56
|
+
|
|
57
|
+
function finding(
|
|
58
|
+
opts: ClassificationOptions,
|
|
59
|
+
code: string,
|
|
60
|
+
rule: string,
|
|
61
|
+
target: 'plugin' | 'skill',
|
|
62
|
+
pointer: string,
|
|
63
|
+
outcome: string,
|
|
64
|
+
): ValidationFinding {
|
|
65
|
+
return blocking({ code, rule, target, pointer, outcome, scope: opts.scope, phase: 'validation' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseDescriptor(text: string): { frontmatter?: Record<string, unknown>; body?: string } {
|
|
69
|
+
// Compatibility Profile v1 requires an explicit, closed descriptor, but all accepted YAML and
|
|
70
|
+
// whitespace semantics must stay Pi-native. Reuse Pi 0.84.2's parser rather than a lookalike.
|
|
71
|
+
if (!/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.test(text)) return {};
|
|
72
|
+
try {
|
|
73
|
+
const parsed = parseFrontmatter<Record<string, unknown>>(text);
|
|
74
|
+
return { frontmatter: parsed.frontmatter, body: parsed.body };
|
|
75
|
+
} catch {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Read the authoritative manifest identity without projecting a Compatible Plugin. */
|
|
81
|
+
export function pluginIdentity(root: string, marketplaceId: string): string | undefined {
|
|
82
|
+
try {
|
|
83
|
+
const raw: unknown = JSON.parse(readFileSync(join(root, '.codex-plugin', 'plugin.json'), 'utf8'));
|
|
84
|
+
const name = typeof raw === 'object' && raw !== null && !Array.isArray(raw) ? (raw as Record<string, unknown>).name : undefined;
|
|
85
|
+
return typeof name === 'string' && KEBAB.test(name) ? `${marketplaceId}/${name}` : undefined;
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class MaterialCapture {
|
|
92
|
+
private readonly hash = createHash('sha256');
|
|
93
|
+
|
|
94
|
+
add(kind: string, value: string | Buffer): void {
|
|
95
|
+
this.hash.update(kind);
|
|
96
|
+
this.hash.update('\u001f');
|
|
97
|
+
this.hash.update(value);
|
|
98
|
+
this.hash.update('\u001e');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fingerprint(): string {
|
|
102
|
+
return this.hash.digest('hex');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isWithin(root: string, target: string): boolean {
|
|
107
|
+
return target === root || target.startsWith(root.endsWith(sep) ? root : root + sep);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isSnapshotExcluded(root: string, target: string): boolean {
|
|
111
|
+
const rel = relative(root, target);
|
|
112
|
+
return rel.split(sep).some((part) => part === '.git' || part === 'node_modules');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resourcesIn(root: string, skillDirectory: string, capture: MaterialCapture): { resources: string[]; error?: string } {
|
|
116
|
+
const resources: string[] = [];
|
|
117
|
+
const chargedTargets = new Set<string>();
|
|
118
|
+
let files = 0;
|
|
119
|
+
let bytes = 0;
|
|
120
|
+
const walk = (directory: string, relative = '', depth = 1): void => {
|
|
121
|
+
if (depth > BUDGET.maxTreeDepth) throw new Error(`Skill Resource depth exceeds Validation Budget (${BUDGET.maxTreeDepth})`);
|
|
122
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
123
|
+
if (relative === '' && entry.name === 'SKILL.md') continue;
|
|
124
|
+
// These trees are deliberately excluded from the Validation Snapshot. They must never be
|
|
125
|
+
// accepted as projected Skill Resources, otherwise disclosure could outlive its snapshot.
|
|
126
|
+
if (entry.isDirectory() && (entry.name === '.git' || entry.name === 'node_modules')) continue;
|
|
127
|
+
const next = relative === '' ? entry.name : `${relative}/${entry.name}`;
|
|
128
|
+
const path = join(directory, entry.name);
|
|
129
|
+
if (entry.isDirectory()) walk(path, next, depth + 1);
|
|
130
|
+
else {
|
|
131
|
+
const stat = lstatSync(path);
|
|
132
|
+
let chargeSize = stat.size;
|
|
133
|
+
if (stat.isSymbolicLink()) {
|
|
134
|
+
const target = realpathSync.native(path);
|
|
135
|
+
if (!isWithin(skillDirectory, target) || isSnapshotExcluded(root, target)) {
|
|
136
|
+
throw new Error(`Skill Resource symlink '${next}' resolves outside snapshot-covered content`);
|
|
137
|
+
}
|
|
138
|
+
const targetStat = statSync(path);
|
|
139
|
+
if (!targetStat.isFile()) throw new Error(`Skill Resource symlink '${next}' does not resolve to a regular file`);
|
|
140
|
+
chargeSize = chargedTargets.has(target) ? 0 : targetStat.size;
|
|
141
|
+
chargedTargets.add(target);
|
|
142
|
+
} else if (!stat.isFile()) {
|
|
143
|
+
throw new Error(`Skill Resource '${next}' is not a regular file`);
|
|
144
|
+
}
|
|
145
|
+
files += 1;
|
|
146
|
+
bytes += chargeSize;
|
|
147
|
+
if (files > BUDGET.maxFiles || bytes > BUDGET.maxTotalBytes) {
|
|
148
|
+
throw new Error('Skill Resources exceed Validation Budget');
|
|
149
|
+
}
|
|
150
|
+
capture.add(`resource:${next}`, readFileSync(path));
|
|
151
|
+
resources.push(next);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
walk(skillDirectory);
|
|
157
|
+
return { resources };
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { resources: [], error: error instanceof Error ? error.message : String(error) };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Classify the complete Plugin tree as one indivisible unit under Compatibility Profile v1. */
|
|
164
|
+
export function classifyPlugin(root: string, opts: ClassificationOptions): ClassificationResult {
|
|
165
|
+
const findings: ValidationFinding[] = [];
|
|
166
|
+
const capture = new MaterialCapture();
|
|
167
|
+
const manifestPath = join(root, '.codex-plugin', 'plugin.json');
|
|
168
|
+
let manifest: Record<string, unknown> | undefined;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const raw = readFileSync(manifestPath, 'utf8');
|
|
172
|
+
capture.add('manifest', raw);
|
|
173
|
+
const parsed: unknown = JSON.parse(raw);
|
|
174
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) manifest = parsed as Record<string, unknown>;
|
|
175
|
+
} catch {
|
|
176
|
+
capture.add('manifest-error', manifestPath);
|
|
177
|
+
// classified below with one stable, safely-presentable finding
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!manifest || typeof manifest.name !== 'string' || !KEBAB.test(manifest.name)) {
|
|
181
|
+
findings.push(finding(opts, CODE.PLUGIN_MANIFEST_INVALID, RULE.PLUGIN_MANIFEST_INVALID, 'plugin', '.codex-plugin/plugin.json', 'Plugin manifest must be an object with authoritative lowercase kebab-case name'));
|
|
182
|
+
}
|
|
183
|
+
if (!manifest || manifest.skills !== './skills/') {
|
|
184
|
+
findings.push(finding(opts, CODE.PLUGIN_MANIFEST_INVALID, RULE.PLUGIN_MANIFEST_INVALID, 'plugin', '.codex-plugin/plugin.json#/skills', 'Compatibility Profile v1 requires the closed skills declaration "./skills/"'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (manifest) {
|
|
188
|
+
for (const key of Object.keys(manifest).sort((a, b) => a.localeCompare(b))) {
|
|
189
|
+
if (key === 'name' || key === 'skills') continue;
|
|
190
|
+
if (UNSUPPORTED_COMPONENTS.has(key)) {
|
|
191
|
+
findings.push(finding(opts, CODE.UNSUPPORTED_ACTIVE_COMPONENT, RULE.UNSUPPORTED_ACTIVE_COMPONENT, 'plugin', `.codex-plugin/plugin.json#/${key}`, `Compatibility Profile v1 does not support active manifest component '${key}'`));
|
|
192
|
+
} else if (INERT_MANIFEST_FIELDS.has(key)) {
|
|
193
|
+
findings.push(warning({
|
|
194
|
+
code: CODE.INERT_METADATA_IGNORED,
|
|
195
|
+
rule: 'COMP-W01',
|
|
196
|
+
target: 'plugin',
|
|
197
|
+
pointer: `.codex-plugin/plugin.json#/${key}`,
|
|
198
|
+
outcome: `Ignored Inert Metadata '${key}' does not change Plugin classification`,
|
|
199
|
+
scope: opts.scope,
|
|
200
|
+
phase: 'validation',
|
|
201
|
+
}));
|
|
202
|
+
} else {
|
|
203
|
+
findings.push(finding(opts, CODE.UNSUPPORTED_ACTIVE_COMPONENT, RULE.UNSUPPORTED_ACTIVE_COMPONENT, 'plugin', `.codex-plugin/plugin.json#/${key}`, `Unknown manifest field '${key}' may declare active behaviour and is fail-closed`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let rootEntries: string[] = [];
|
|
209
|
+
try {
|
|
210
|
+
if (!lstatSync(root).isDirectory()) throw new Error('Plugin root is not a directory');
|
|
211
|
+
rootEntries = readdirSync(root, { withFileTypes: true }).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
212
|
+
} catch (error) {
|
|
213
|
+
findings.push(finding(opts, CODE.PLUGIN_MANIFEST_INVALID, RULE.PLUGIN_MANIFEST_INVALID, 'plugin', '', `Plugin root cannot be read: ${error instanceof Error ? error.message : String(error)}`));
|
|
214
|
+
}
|
|
215
|
+
for (const name of rootEntries) {
|
|
216
|
+
if (UNSUPPORTED_COMPONENTS.has(name)) {
|
|
217
|
+
findings.push(finding(opts, CODE.UNSUPPORTED_ACTIVE_COMPONENT, RULE.UNSUPPORTED_ACTIVE_COMPONENT, 'plugin', name, `Compatibility Profile v1 does not support Active Component '${name}'`));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const skills: CompatibleSkill[] = [];
|
|
222
|
+
const skillsDirectory = join(root, 'skills');
|
|
223
|
+
let skillEntries: import('node:fs').Dirent[] = [];
|
|
224
|
+
let skillsReadable = true;
|
|
225
|
+
try {
|
|
226
|
+
if (!lstatSync(skillsDirectory).isDirectory()) throw new Error('skills is not a directory');
|
|
227
|
+
skillEntries = readdirSync(skillsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
228
|
+
} catch (error) {
|
|
229
|
+
skillsReadable = false;
|
|
230
|
+
findings.push(finding(opts, CODE.SKILL_DESCRIPTOR_INVALID, RULE.SKILL_DESCRIPTOR_INVALID, 'plugin', 'skills', 'Compatibility Profile v1 requires at least one skills/<directory>/SKILL.md descriptor'));
|
|
231
|
+
}
|
|
232
|
+
if (skillsReadable) {
|
|
233
|
+
for (const entry of skillEntries) {
|
|
234
|
+
if (!entry.isDirectory()) {
|
|
235
|
+
findings.push(finding(opts, CODE.SKILL_DESCRIPTOR_INVALID, RULE.SKILL_DESCRIPTOR_INVALID, 'skill', `skills/${entry.name}`, 'A skill must be a directory containing SKILL.md'));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const skillDirectory = join(skillsDirectory, entry.name);
|
|
239
|
+
const skillPath = join(skillDirectory, 'SKILL.md');
|
|
240
|
+
let descriptor: ReturnType<typeof parseDescriptor> = {};
|
|
241
|
+
try {
|
|
242
|
+
if (!lstatSync(skillPath).isFile()) throw new Error('not a file');
|
|
243
|
+
const raw = readFileSync(skillPath, 'utf8');
|
|
244
|
+
capture.add(`descriptor:${entry.name}`, raw);
|
|
245
|
+
descriptor = parseDescriptor(raw);
|
|
246
|
+
} catch {
|
|
247
|
+
capture.add(`descriptor-error:${entry.name}`, skillPath);
|
|
248
|
+
// reported by shared condition below
|
|
249
|
+
}
|
|
250
|
+
const name = descriptor.frontmatter?.name;
|
|
251
|
+
const description = descriptor.frontmatter?.description;
|
|
252
|
+
if (typeof name !== 'string' || !KEBAB.test(name) || typeof description !== 'string' || !description.trim() || !descriptor.body?.trim()) {
|
|
253
|
+
findings.push(finding(opts, CODE.SKILL_DESCRIPTOR_INVALID, RULE.SKILL_DESCRIPTOR_INVALID, 'skill', `skills/${entry.name}/SKILL.md`, 'Skill Descriptor requires lowercase kebab-case name, non-empty description, closed Pi-native YAML frontmatter, and non-empty Skill Body'));
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
for (const key of Object.keys(descriptor.frontmatter!).sort((a, b) => a.localeCompare(b))) {
|
|
257
|
+
if (!['name', 'description', 'disable-model-invocation'].includes(key)) {
|
|
258
|
+
findings.push(finding(opts, CODE.UNSUPPORTED_ACTIVE_COMPONENT, RULE.UNSUPPORTED_ACTIVE_COMPONENT, 'skill', `skills/${entry.name}/SKILL.md#/${key}`, `Unknown Skill Descriptor field '${key}' is fail-closed under Compatibility Profile v1`));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (descriptor.frontmatter?.['disable-model-invocation'] !== undefined && typeof descriptor.frontmatter['disable-model-invocation'] !== 'boolean') {
|
|
262
|
+
findings.push(finding(opts, CODE.SKILL_DESCRIPTOR_INVALID, RULE.SKILL_DESCRIPTOR_INVALID, 'skill', `skills/${entry.name}/SKILL.md#/disable-model-invocation`, 'disable-model-invocation must be a boolean when declared'));
|
|
263
|
+
}
|
|
264
|
+
// Pi conventionally discovers this companion as an Agent Profile. Its invocation and
|
|
265
|
+
// external-dependency declarations are active behavior, not an opaque resource.
|
|
266
|
+
try {
|
|
267
|
+
if (lstatSync(join(skillDirectory, 'agents', 'openai.yaml')).isFile()) {
|
|
268
|
+
findings.push(finding(opts, CODE.UNSUPPORTED_ACTIVE_COMPONENT, RULE.UNSUPPORTED_ACTIVE_COMPONENT, 'skill', `skills/${entry.name}/agents/openai.yaml`, 'Compatibility Profile v1 does not support Skill Agent Profiles'));
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Optional companion is absent; the descriptor remains the only accepted declaration.
|
|
272
|
+
}
|
|
273
|
+
const disabled = descriptor.frontmatter?.['disable-model-invocation'] === true;
|
|
274
|
+
const pluginId = manifest && typeof manifest.name === 'string' ? `${opts.marketplaceId}/${manifest.name}` : '';
|
|
275
|
+
const resourceResult = resourcesIn(root, skillDirectory, capture);
|
|
276
|
+
if (resourceResult.error) {
|
|
277
|
+
findings.push(finding(opts, CODE.SKILL_DESCRIPTOR_INVALID, RULE.SKILL_DESCRIPTOR_INVALID, 'skill', `skills/${entry.name}`, `Skill Resources cannot be scanned safely: ${resourceResult.error}`));
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
skills.push({
|
|
281
|
+
id: `${pluginId}/${name}`,
|
|
282
|
+
name,
|
|
283
|
+
path: skillDirectory,
|
|
284
|
+
resources: resourceResult.resources,
|
|
285
|
+
invocationPolicy: disabled ? 'explicit' : 'implicit',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const duplicateSkills = new Set<string>();
|
|
291
|
+
const seenSkills = new Set<string>();
|
|
292
|
+
for (const skill of skills) {
|
|
293
|
+
if (seenSkills.has(skill.id)) duplicateSkills.add(skill.id);
|
|
294
|
+
seenSkills.add(skill.id);
|
|
295
|
+
}
|
|
296
|
+
for (const id of [...duplicateSkills].sort((a, b) => a.localeCompare(b))) {
|
|
297
|
+
findings.push(finding(opts, CODE.SKILL_DESCRIPTOR_INVALID, RULE.SKILL_DESCRIPTOR_INVALID, 'skill', id, `Skill ID '${id}' is declared by more than one Skill Descriptor`));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (skills.length === 0 && !findings.some((item) => item.code === CODE.SKILL_DESCRIPTOR_INVALID)) {
|
|
301
|
+
findings.push(finding(opts, CODE.SKILL_DESCRIPTOR_INVALID, RULE.SKILL_DESCRIPTOR_INVALID, 'plugin', 'skills', 'Compatibility Profile v1 requires at least one Skill Descriptor'));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const sorted = sortFindings(findings);
|
|
305
|
+
const identity = manifest && typeof manifest.name === 'string' && KEBAB.test(manifest.name)
|
|
306
|
+
? `${opts.marketplaceId}/${manifest.name}`
|
|
307
|
+
: undefined;
|
|
308
|
+
if (sorted.some((item) => item.code === CODE.PLUGIN_MANIFEST_INVALID || item.code === CODE.SKILL_DESCRIPTOR_INVALID)) {
|
|
309
|
+
return { classification: 'invalid', identity, captureFingerprint: capture.fingerprint(), findings: sorted };
|
|
310
|
+
}
|
|
311
|
+
if (sorted.some((item) => item.code === CODE.UNSUPPORTED_ACTIVE_COMPONENT && item.classification === 'blocking')) {
|
|
312
|
+
return { classification: 'incompatible', identity, captureFingerprint: capture.fingerprint(), findings: sorted };
|
|
313
|
+
}
|
|
314
|
+
if (sorted.some((item) => item.classification === 'blocking')) return { classification: 'invalid', identity, captureFingerprint: capture.fingerprint(), findings: sorted };
|
|
315
|
+
const manifestName = manifest!.name as string;
|
|
316
|
+
return {
|
|
317
|
+
classification: 'compatible',
|
|
318
|
+
plugin: {
|
|
319
|
+
id: `${opts.marketplaceId}/${manifestName}`,
|
|
320
|
+
manifestName,
|
|
321
|
+
marketplaceEntryId: opts.marketplaceEntryId,
|
|
322
|
+
skills,
|
|
323
|
+
},
|
|
324
|
+
identity: `${opts.marketplaceId}/${manifestName}`,
|
|
325
|
+
captureFingerprint: capture.fingerprint(),
|
|
326
|
+
findings: sorted,
|
|
327
|
+
};
|
|
328
|
+
}
|