@aeondave/pi-skill-optimizer 3.0.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/BENCHMARK.md +354 -0
- package/CHANGELOG.md +117 -0
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/assets/banner.png +0 -0
- package/docs/ARCHITECTURE.md +66 -0
- package/docs/CONFIGURATION.md +138 -0
- package/docs/RANKING.md +155 -0
- package/package.json +85 -0
- package/src/activation.ts +88 -0
- package/src/aliases.ts +111 -0
- package/src/config.ts +446 -0
- package/src/corpus.ts +416 -0
- package/src/evaluation.ts +692 -0
- package/src/generate.ts +365 -0
- package/src/history.ts +147 -0
- package/src/index.ts +1362 -0
- package/src/init-profile.ts +96 -0
- package/src/init-prompt.ts +10 -0
- package/src/init-readiness.ts +123 -0
- package/src/optimize.ts +424 -0
- package/src/output-recovery.ts +19 -0
- package/src/output.ts +629 -0
- package/src/persistence.ts +535 -0
- package/src/profile.ts +253 -0
- package/src/prompts/init-profile.md +40 -0
- package/src/request.ts +304 -0
- package/src/skill-loader.ts +138 -0
- package/src/skills.ts +1230 -0
- package/src/stats.ts +178 -0
- package/src/tools.ts +548 -0
- package/src/usage.ts +256 -0
package/src/corpus.ts
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for building a private, shareable optimization corpus.
|
|
3
|
+
*
|
|
4
|
+
* Raw values are never retained by these APIs. Stable identifiers are one-way
|
|
5
|
+
* HMACs, redaction uses non-reversible category markers, and skill roots are
|
|
6
|
+
* replaced with deterministic placeholders scoped to one catalog.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHmac } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
export const CORPUS_SCHEMA_VERSION = 1 as const;
|
|
12
|
+
|
|
13
|
+
export type CorpusSchemaVersion = typeof CORPUS_SCHEMA_VERSION;
|
|
14
|
+
export type CorpusExampleKind = "catalog" | "skill" | "output";
|
|
15
|
+
|
|
16
|
+
export interface CorpusSkillV1 {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
location: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CatalogExampleV1 {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
skills: CorpusSkillV1[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SkillExampleV1 {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
catalogId: string;
|
|
32
|
+
query: string;
|
|
33
|
+
relevantSkillNames: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OutputExampleV1 {
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
tool: string;
|
|
40
|
+
text: string;
|
|
41
|
+
evidence: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PrivateCorpusV1 {
|
|
45
|
+
schemaVersion: CorpusSchemaVersion;
|
|
46
|
+
catalogs: CatalogExampleV1[];
|
|
47
|
+
skillExamples: SkillExampleV1[];
|
|
48
|
+
outputExamples: OutputExampleV1[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RawCorpusSkill {
|
|
52
|
+
name: string;
|
|
53
|
+
description: string;
|
|
54
|
+
location: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RawCatalogExampleV1 {
|
|
58
|
+
label: string;
|
|
59
|
+
skills: readonly RawCorpusSkill[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RawSkillExampleV1 {
|
|
63
|
+
label: string;
|
|
64
|
+
catalogId: string;
|
|
65
|
+
query: string;
|
|
66
|
+
relevantSkillNames: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RawOutputExampleV1 {
|
|
70
|
+
label: string;
|
|
71
|
+
tool: string;
|
|
72
|
+
text: string;
|
|
73
|
+
evidence: readonly string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type CorpusValidationCode =
|
|
77
|
+
| "invalid-shape"
|
|
78
|
+
| "invalid-schema-version"
|
|
79
|
+
| "missing-id"
|
|
80
|
+
| "duplicate-id"
|
|
81
|
+
| "missing-label"
|
|
82
|
+
| "unsafe-content"
|
|
83
|
+
| "invalid-location"
|
|
84
|
+
| "duplicate-skill-name"
|
|
85
|
+
| "missing-catalog-reference"
|
|
86
|
+
| "unknown-relevant-skill"
|
|
87
|
+
| "missing-evidence";
|
|
88
|
+
|
|
89
|
+
export interface CorpusValidationIssue {
|
|
90
|
+
code: CorpusValidationCode;
|
|
91
|
+
path: string;
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CorpusValidationResult {
|
|
96
|
+
valid: boolean;
|
|
97
|
+
issues: CorpusValidationIssue[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface EvidenceRecallResult {
|
|
101
|
+
expected: number;
|
|
102
|
+
matched: number;
|
|
103
|
+
recall: number;
|
|
104
|
+
missing: string[];
|
|
105
|
+
complete: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const PEM_BLOCK_RE = /-----BEGIN ([A-Z0-9 ]+)-----[\s\S]*?-----END \1-----/g;
|
|
109
|
+
const JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
110
|
+
const BEARER_RE = /\b(authorization\s*[:=]\s*bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
111
|
+
const NAMED_TOKEN_RE = /\b(api[_-]?key|access[_-]?token|auth[_-]?token|secret|token)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}["']?/gi;
|
|
112
|
+
const PREFIXED_TOKEN_RE = /\b(?:sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{16,}|xox[baprs]-[A-Za-z0-9-]{16,}|AKIA[0-9A-Z]{16})\b/g;
|
|
113
|
+
const URL_AUTHORITY_RE = /\b(https?:\/\/)(?:[^@\s/]+@)?(\[[^\]]+\]|[^:/\s?#<>]+)(:\d+)?/gi;
|
|
114
|
+
const EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
115
|
+
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi;
|
|
116
|
+
const IPV4_RE = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
117
|
+
const HOME_RE = /(?:[A-Za-z]:[\\/]Users[\\/][^\\/\s<>"']+|\/(?:home|Users)\/[^/\s<>"']+)/g;
|
|
118
|
+
const TILDE_HOME_RE = /(^|[^A-Za-z0-9_])~(?=[\\/])/g;
|
|
119
|
+
const UNC_PATH_RE = /\\\\[^\\\s<>"']+\\[^\\\s<>"']+(?:\\[^\\\s<>"']+)*/g;
|
|
120
|
+
const WINDOWS_ABSOLUTE_PATH_RE = /\b[A-Za-z]:[\\/](?:[^\\/\s<>"']+[\\/]?)+/g;
|
|
121
|
+
const POSIX_ABSOLUTE_PATH_RE = /(?<![A-Za-z0-9_>])(?:\/[^/\s<>"']+){2,}/g;
|
|
122
|
+
|
|
123
|
+
function stableSerialize(value: unknown, seen = new WeakSet<object>()): string {
|
|
124
|
+
if (value === null) return "null";
|
|
125
|
+
if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
126
|
+
if (typeof value === "number") {
|
|
127
|
+
if (!Number.isFinite(value)) throw new TypeError("stable ID input must contain only finite numbers");
|
|
128
|
+
return JSON.stringify(value);
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableSerialize(item, seen)).join(",")}]`;
|
|
131
|
+
if (typeof value === "object") {
|
|
132
|
+
if (seen.has(value)) throw new TypeError("stable ID input must not be cyclic");
|
|
133
|
+
seen.add(value);
|
|
134
|
+
const record = value as Record<string, unknown>;
|
|
135
|
+
const fields = Object.keys(record)
|
|
136
|
+
.filter((key) => record[key] !== undefined)
|
|
137
|
+
.sort()
|
|
138
|
+
.map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key], seen)}`);
|
|
139
|
+
seen.delete(value);
|
|
140
|
+
return `{${fields.join(",")}}`;
|
|
141
|
+
}
|
|
142
|
+
throw new TypeError(`stable ID input contains unsupported ${typeof value}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Build a deterministic, non-reversible 128-bit identifier with an explicit salt. */
|
|
146
|
+
export function stableCorpusId(salt: string, kind: CorpusExampleKind, value: unknown): string {
|
|
147
|
+
if (typeof salt !== "string" || salt.length < 16) throw new TypeError("corpus HMAC salt must contain at least 16 characters");
|
|
148
|
+
const digest = createHmac("sha256", salt)
|
|
149
|
+
.update(`${CORPUS_SCHEMA_VERSION}\0${kind}\0${stableSerialize(value)}`, "utf8")
|
|
150
|
+
.digest("hex")
|
|
151
|
+
.slice(0, 32);
|
|
152
|
+
return `${kind}_${digest}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validIpv4(value: string): boolean {
|
|
156
|
+
return value.split(".").every((part) => {
|
|
157
|
+
const octet = Number(part);
|
|
158
|
+
return Number.isInteger(octet) && octet >= 0 && octet <= 255;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Redact common secrets and identifiers without returning or retaining a map.
|
|
164
|
+
* Repeated values intentionally collapse to the same category marker.
|
|
165
|
+
*/
|
|
166
|
+
export function redactSensitiveText(text: string): string {
|
|
167
|
+
return text
|
|
168
|
+
.replace(PEM_BLOCK_RE, "<PEM_SECRET>")
|
|
169
|
+
.replace(JWT_RE, "<JWT>")
|
|
170
|
+
.replace(BEARER_RE, "$1<API_TOKEN>")
|
|
171
|
+
.replace(NAMED_TOKEN_RE, (_match, key: string) => `${key}=<API_TOKEN>`)
|
|
172
|
+
.replace(PREFIXED_TOKEN_RE, "<API_TOKEN>")
|
|
173
|
+
.replace(URL_AUTHORITY_RE, (_match, scheme: string, _host: string, port: string | undefined) => `${scheme}<URL_HOST>${port ?? ""}`)
|
|
174
|
+
.replace(EMAIL_RE, "<EMAIL>")
|
|
175
|
+
.replace(UUID_RE, "<UUID>")
|
|
176
|
+
.replace(IPV4_RE, (value) => validIpv4(value) ? "<IPV4>" : value)
|
|
177
|
+
.replace(HOME_RE, "<HOME>")
|
|
178
|
+
.replace(TILDE_HOME_RE, "$1<HOME>")
|
|
179
|
+
.replace(UNC_PATH_RE, "<ABS_PATH>")
|
|
180
|
+
.replace(WINDOWS_ABSOLUTE_PATH_RE, "<ABS_PATH>")
|
|
181
|
+
.replace(POSIX_ABSOLUTE_PATH_RE, "<ABS_PATH>");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeLocation(location: string): string {
|
|
185
|
+
return location.replace(/\\/g, "/").replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function locationRoot(skill: RawCorpusSkill): string {
|
|
189
|
+
const location = normalizeLocation(skill.location.trim());
|
|
190
|
+
const conventionalSuffix = `/${skill.name}/SKILL.md`;
|
|
191
|
+
if (location.endsWith(conventionalSuffix)) return location.slice(0, -conventionalSuffix.length) || "<RELATIVE_ROOT>";
|
|
192
|
+
const slash = location.lastIndexOf("/");
|
|
193
|
+
return slash > 0 ? location.slice(0, slash) : "<RELATIVE_ROOT>";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Sanitize a catalog while retaining skill names and deterministic root relationships. */
|
|
197
|
+
export function sanitizeCatalogSkills(skills: readonly RawCorpusSkill[]): CorpusSkillV1[] {
|
|
198
|
+
const roots = [...new Set(skills.map(locationRoot))].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
199
|
+
const rootNames = new Map(roots.map((root, index) => [root, `<ROOT_${index + 1}>`] as const));
|
|
200
|
+
return skills.map((skill) => {
|
|
201
|
+
const name = redactSensitiveText(skill.name);
|
|
202
|
+
return {
|
|
203
|
+
name,
|
|
204
|
+
description: redactSensitiveText(skill.description),
|
|
205
|
+
location: `${rootNames.get(locationRoot(skill))}/${name}/SKILL.md`,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function createCatalogExampleV1(input: RawCatalogExampleV1, salt: string): CatalogExampleV1 {
|
|
211
|
+
return {
|
|
212
|
+
id: stableCorpusId(salt, "catalog", input),
|
|
213
|
+
label: redactSensitiveText(input.label),
|
|
214
|
+
skills: sanitizeCatalogSkills(input.skills),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function createSkillExampleV1(input: RawSkillExampleV1, salt: string): SkillExampleV1 {
|
|
219
|
+
return {
|
|
220
|
+
id: stableCorpusId(salt, "skill", input),
|
|
221
|
+
label: redactSensitiveText(input.label),
|
|
222
|
+
catalogId: input.catalogId,
|
|
223
|
+
query: redactSensitiveText(input.query),
|
|
224
|
+
relevantSkillNames: input.relevantSkillNames.map(redactSensitiveText),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function createOutputExampleV1(input: RawOutputExampleV1, salt: string): OutputExampleV1 {
|
|
229
|
+
return {
|
|
230
|
+
id: stableCorpusId(salt, "output", input),
|
|
231
|
+
label: redactSensitiveText(input.label),
|
|
232
|
+
tool: redactSensitiveText(input.tool),
|
|
233
|
+
text: redactSensitiveText(input.text),
|
|
234
|
+
evidence: input.evidence.map(redactSensitiveText),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function createPrivateCorpusV1(
|
|
239
|
+
catalogs: readonly CatalogExampleV1[],
|
|
240
|
+
skillExamples: readonly SkillExampleV1[],
|
|
241
|
+
outputExamples: readonly OutputExampleV1[],
|
|
242
|
+
): PrivateCorpusV1 {
|
|
243
|
+
return {
|
|
244
|
+
schemaVersion: CORPUS_SCHEMA_VERSION,
|
|
245
|
+
catalogs: [...catalogs],
|
|
246
|
+
skillExamples: [...skillExamples],
|
|
247
|
+
outputExamples: [...outputExamples],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function escapeXml(value: string): string {
|
|
252
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function renderAvailableSkills(skills: readonly CorpusSkillV1[]): string {
|
|
256
|
+
const lines = ["<available_skills>"];
|
|
257
|
+
for (const skill of skills) {
|
|
258
|
+
lines.push(
|
|
259
|
+
" <skill>",
|
|
260
|
+
` <name>${escapeXml(skill.name)}</name>`,
|
|
261
|
+
` <description>${escapeXml(skill.description)}</description>`,
|
|
262
|
+
` <location>${escapeXml(skill.location)}</location>`,
|
|
263
|
+
" </skill>",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
lines.push("</available_skills>");
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function lineCounts(lines: readonly string[]): Map<string, number> {
|
|
271
|
+
const counts = new Map<string, number>();
|
|
272
|
+
for (const line of lines) counts.set(line, (counts.get(line) ?? 0) + 1);
|
|
273
|
+
return counts;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Exact evidence recall. Duplicate expected lines require duplicate occurrences. */
|
|
277
|
+
export function evidenceRecall(expected: readonly string[], actual: string | readonly string[]): EvidenceRecallResult {
|
|
278
|
+
const lines = typeof actual === "string" ? actual.split(/\r?\n/) : actual;
|
|
279
|
+
const available = lineCounts(lines);
|
|
280
|
+
const missing: string[] = [];
|
|
281
|
+
let matched = 0;
|
|
282
|
+
for (const line of expected) {
|
|
283
|
+
const count = available.get(line) ?? 0;
|
|
284
|
+
if (count === 0) {
|
|
285
|
+
missing.push(line);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
available.set(line, count - 1);
|
|
289
|
+
matched++;
|
|
290
|
+
}
|
|
291
|
+
const total = expected.length;
|
|
292
|
+
return {
|
|
293
|
+
expected: total,
|
|
294
|
+
matched,
|
|
295
|
+
recall: total === 0 ? 1 : matched / total,
|
|
296
|
+
missing,
|
|
297
|
+
complete: missing.length === 0,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
302
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isSafe(value: string): boolean {
|
|
306
|
+
return redactSensitiveText(value) === value;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Validate schema, referential integrity, privacy, labels and evidence recall. */
|
|
310
|
+
export function validateCorpusV1(value: unknown): CorpusValidationResult {
|
|
311
|
+
const issues: CorpusValidationIssue[] = [];
|
|
312
|
+
const issue = (code: CorpusValidationCode, path: string, message: string) => issues.push({ code, path, message });
|
|
313
|
+
if (!isRecord(value)) return { valid: false, issues: [{ code: "invalid-shape", path: "$", message: "corpus must be an object" }] };
|
|
314
|
+
if (value.schemaVersion !== CORPUS_SCHEMA_VERSION) issue("invalid-schema-version", "$.schemaVersion", "schemaVersion must be 1");
|
|
315
|
+
|
|
316
|
+
const catalogs = Array.isArray(value.catalogs) ? value.catalogs : [];
|
|
317
|
+
const skillExamples = Array.isArray(value.skillExamples) ? value.skillExamples : [];
|
|
318
|
+
const outputExamples = Array.isArray(value.outputExamples) ? value.outputExamples : [];
|
|
319
|
+
if (!Array.isArray(value.catalogs)) issue("invalid-shape", "$.catalogs", "catalogs must be an array");
|
|
320
|
+
if (!Array.isArray(value.skillExamples)) issue("invalid-shape", "$.skillExamples", "skillExamples must be an array");
|
|
321
|
+
if (!Array.isArray(value.outputExamples)) issue("invalid-shape", "$.outputExamples", "outputExamples must be an array");
|
|
322
|
+
|
|
323
|
+
const seenIds = new Set<string>();
|
|
324
|
+
const catalogSkills = new Map<string, Set<string>>();
|
|
325
|
+
const checkExample = (example: Record<string, unknown>, path: string): string | undefined => {
|
|
326
|
+
const id = typeof example.id === "string" ? example.id.trim() : "";
|
|
327
|
+
if (!id) issue("missing-id", `${path}.id`, "example id is required");
|
|
328
|
+
else if (seenIds.has(id)) issue("duplicate-id", `${path}.id`, `duplicate example id: ${id}`);
|
|
329
|
+
else seenIds.add(id);
|
|
330
|
+
const label = typeof example.label === "string" ? example.label : "";
|
|
331
|
+
if (!label.trim()) issue("missing-label", `${path}.label`, "non-empty label is required");
|
|
332
|
+
else if (!isSafe(label)) issue("unsafe-content", `${path}.label`, "label contains raw PII or secret material");
|
|
333
|
+
return id || undefined;
|
|
334
|
+
};
|
|
335
|
+
const checkSafeString = (field: unknown, path: string): field is string => {
|
|
336
|
+
if (typeof field !== "string") {
|
|
337
|
+
issue("invalid-shape", path, "value must be a string");
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (!isSafe(field)) issue("unsafe-content", path, "value contains raw PII or secret material");
|
|
341
|
+
return true;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
catalogs.forEach((raw, catalogIndex) => {
|
|
345
|
+
const path = `$.catalogs[${catalogIndex}]`;
|
|
346
|
+
if (!isRecord(raw)) {
|
|
347
|
+
issue("invalid-shape", path, "catalog example must be an object");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const id = checkExample(raw, path);
|
|
351
|
+
const skills = Array.isArray(raw.skills) ? raw.skills : [];
|
|
352
|
+
if (!Array.isArray(raw.skills)) issue("invalid-shape", `${path}.skills`, "skills must be an array");
|
|
353
|
+
const names = new Set<string>();
|
|
354
|
+
skills.forEach((rawSkill, skillIndex) => {
|
|
355
|
+
const skillPath = `${path}.skills[${skillIndex}]`;
|
|
356
|
+
if (!isRecord(rawSkill)) {
|
|
357
|
+
issue("invalid-shape", skillPath, "skill must be an object");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const name = checkSafeString(rawSkill.name, `${skillPath}.name`) ? rawSkill.name : "";
|
|
361
|
+
checkSafeString(rawSkill.description, `${skillPath}.description`);
|
|
362
|
+
if (name && names.has(name)) issue("duplicate-skill-name", `${skillPath}.name`, `duplicate skill name: ${name}`);
|
|
363
|
+
if (name) names.add(name);
|
|
364
|
+
if (checkSafeString(rawSkill.location, `${skillPath}.location`)) {
|
|
365
|
+
const expected = /^<ROOT_[1-9]\d*>\//.test(rawSkill.location) && rawSkill.location.endsWith(`/${name}/SKILL.md`);
|
|
366
|
+
if (!expected) issue("invalid-location", `${skillPath}.location`, "location must be <ROOT_n>/<name>/SKILL.md");
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
if (id) catalogSkills.set(id, names);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
skillExamples.forEach((raw, index) => {
|
|
373
|
+
const path = `$.skillExamples[${index}]`;
|
|
374
|
+
if (!isRecord(raw)) {
|
|
375
|
+
issue("invalid-shape", path, "skill example must be an object");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
checkExample(raw, path);
|
|
379
|
+
checkSafeString(raw.query, `${path}.query`);
|
|
380
|
+
const catalogId = typeof raw.catalogId === "string" ? raw.catalogId : "";
|
|
381
|
+
const knownNames = catalogSkills.get(catalogId);
|
|
382
|
+
if (!knownNames) issue("missing-catalog-reference", `${path}.catalogId`, `unknown catalog id: ${catalogId}`);
|
|
383
|
+
const relevant = Array.isArray(raw.relevantSkillNames) ? raw.relevantSkillNames : [];
|
|
384
|
+
if (!Array.isArray(raw.relevantSkillNames)) issue("invalid-shape", `${path}.relevantSkillNames`, "relevantSkillNames must be an array");
|
|
385
|
+
relevant.forEach((name, relevantIndex) => {
|
|
386
|
+
const namePath = `${path}.relevantSkillNames[${relevantIndex}]`;
|
|
387
|
+
if (!checkSafeString(name, namePath)) return;
|
|
388
|
+
if (knownNames && !knownNames.has(name)) issue("unknown-relevant-skill", namePath, `skill is not in catalog ${catalogId}: ${name}`);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
outputExamples.forEach((raw, index) => {
|
|
393
|
+
const path = `$.outputExamples[${index}]`;
|
|
394
|
+
if (!isRecord(raw)) {
|
|
395
|
+
issue("invalid-shape", path, "output example must be an object");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
checkExample(raw, path);
|
|
399
|
+
checkSafeString(raw.tool, `${path}.tool`);
|
|
400
|
+
const text = checkSafeString(raw.text, `${path}.text`) ? raw.text : "";
|
|
401
|
+
const evidence = Array.isArray(raw.evidence) ? raw.evidence : [];
|
|
402
|
+
if (!Array.isArray(raw.evidence)) issue("invalid-shape", `${path}.evidence`, "evidence must be an array");
|
|
403
|
+
const safeEvidence = evidence.filter((entry, evidenceIndex): entry is string => checkSafeString(entry, `${path}.evidence[${evidenceIndex}]`));
|
|
404
|
+
const recall = evidenceRecall(safeEvidence, text);
|
|
405
|
+
if (!recall.complete) issue("missing-evidence", `${path}.evidence`, `${recall.missing.length} evidence occurrence(s) are absent from text`);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return { valid: issues.length === 0, issues };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function assertValidCorpusV1(value: unknown): asserts value is PrivateCorpusV1 {
|
|
412
|
+
const result = validateCorpusV1(value);
|
|
413
|
+
if (!result.valid) {
|
|
414
|
+
throw new Error(`invalid private corpus:\n${result.issues.map((entry) => `${entry.path} [${entry.code}] ${entry.message}`).join("\n")}`);
|
|
415
|
+
}
|
|
416
|
+
}
|