@soleri/core 9.14.4 → 9.15.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/dist/brain/brain.d.ts +9 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +11 -1
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +24 -0
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/chat/chat-session.d.ts +6 -0
- package/dist/chat/chat-session.d.ts.map +1 -1
- package/dist/chat/chat-session.js +68 -17
- package/dist/chat/chat-session.js.map +1 -1
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +138 -0
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/types.d.ts +10 -0
- package/dist/curator/types.d.ts.map +1 -1
- package/dist/engine/bin/soleri-engine.js +0 -0
- package/dist/flows/types.d.ts +16 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts +10 -4
- package/dist/intake/content-classifier.d.ts.map +1 -1
- package/dist/intake/content-classifier.js +19 -5
- package/dist/intake/content-classifier.js.map +1 -1
- package/dist/intake/text-ingester.d.ts +18 -0
- package/dist/intake/text-ingester.d.ts.map +1 -1
- package/dist/intake/text-ingester.js +37 -13
- package/dist/intake/text-ingester.js.map +1 -1
- package/dist/planning/planner.d.ts +3 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +43 -4
- package/dist/planning/planner.js.map +1 -1
- package/dist/plugins/types.d.ts +2 -2
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +59 -20
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +28 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +16 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +19 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/validate-skills.d.ts +32 -0
- package/dist/skills/validate-skills.d.ts.map +1 -0
- package/dist/skills/validate-skills.js +396 -0
- package/dist/skills/validate-skills.js.map +1 -0
- package/dist/vault/default-canonical-tags.d.ts +15 -0
- package/dist/vault/default-canonical-tags.d.ts.map +1 -0
- package/dist/vault/default-canonical-tags.js +65 -0
- package/dist/vault/default-canonical-tags.js.map +1 -0
- package/dist/vault/tag-normalizer.d.ts +42 -0
- package/dist/vault/tag-normalizer.d.ts.map +1 -0
- package/dist/vault/tag-normalizer.js +157 -0
- package/dist/vault/tag-normalizer.js.map +1 -0
- package/package.json +5 -1
- package/src/__tests__/embeddings.test.ts +3 -3
- package/src/brain/brain.ts +25 -1
- package/src/brain/intelligence.ts +25 -0
- package/src/brain/types.ts +1 -0
- package/src/chat/chat-session.ts +75 -17
- package/src/chat/chat-transport.test.ts +31 -1
- package/src/curator/curator.ts +180 -0
- package/src/curator/types.ts +10 -0
- package/src/index.ts +7 -0
- package/src/intake/content-classifier.ts +22 -4
- package/src/intake/text-ingester.ts +61 -12
- package/src/planning/planner.test.ts +86 -90
- package/src/planning/planner.ts +48 -4
- package/src/runtime/admin-setup-ops.test.ts +44 -0
- package/src/runtime/admin-setup-ops.ts +59 -20
- package/src/runtime/facades/orchestrate-facade.ts +27 -1
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/types.ts +19 -0
- package/src/skills/validate-skills.test.ts +205 -0
- package/src/skills/validate-skills.ts +470 -0
- package/src/vault/default-canonical-tags.ts +64 -0
- package/src/vault/tag-normalizer.test.ts +214 -0
- package/src/vault/tag-normalizer.ts +188 -0
- package/dist/embeddings/index.d.ts +0 -5
- package/dist/embeddings/index.d.ts.map +0 -1
- package/dist/embeddings/index.js +0 -3
- package/dist/embeddings/index.js.map +0 -1
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator for user-installed SKILL.md op-call examples.
|
|
3
|
+
*
|
|
4
|
+
* Reads all SKILL.md files from a given skills directory (e.g. ~/.claude/skills/),
|
|
5
|
+
* extracts inline op-call examples, and validates their params against the actual
|
|
6
|
+
* Zod schemas from the facade layer.
|
|
7
|
+
*
|
|
8
|
+
* Returns structured results rather than printing or exiting — the CLI layer owns I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import type { ZodType, ZodError } from 'zod';
|
|
14
|
+
import type { OpDefinition } from '../facades/types.js';
|
|
15
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
16
|
+
|
|
17
|
+
// ── Facade factory imports ──────────────────────────────────────────────
|
|
18
|
+
import { createVaultFacadeOps } from '../runtime/facades/vault-facade.js';
|
|
19
|
+
import { createPlanFacadeOps } from '../runtime/facades/plan-facade.js';
|
|
20
|
+
import { createBrainFacadeOps } from '../runtime/facades/brain-facade.js';
|
|
21
|
+
import { createMemoryFacadeOps } from '../runtime/facades/memory-facade.js';
|
|
22
|
+
import { createAdminFacadeOps } from '../runtime/facades/admin-facade.js';
|
|
23
|
+
import { createCuratorFacadeOps } from '../runtime/facades/curator-facade.js';
|
|
24
|
+
import { createLoopFacadeOps } from '../runtime/facades/loop-facade.js';
|
|
25
|
+
import { createOrchestrateFacadeOps } from '../runtime/facades/orchestrate-facade.js';
|
|
26
|
+
import { createControlFacadeOps } from '../runtime/facades/control-facade.js';
|
|
27
|
+
import { createContextFacadeOps } from '../runtime/facades/context-facade.js';
|
|
28
|
+
import { createAgencyFacadeOps } from '../runtime/facades/agency-facade.js';
|
|
29
|
+
import { createChatFacadeOps } from '../runtime/facades/chat-facade.js';
|
|
30
|
+
import { createOperatorFacadeOps } from '../runtime/facades/operator-facade.js';
|
|
31
|
+
import { createArchiveFacadeOps } from '../runtime/facades/archive-facade.js';
|
|
32
|
+
import { createSyncFacadeOps } from '../runtime/facades/sync-facade.js';
|
|
33
|
+
import { createReviewFacadeOps } from '../runtime/facades/review-facade.js';
|
|
34
|
+
import { createIntakeFacadeOps } from '../runtime/facades/intake-facade.js';
|
|
35
|
+
import { createLinksFacadeOps } from '../runtime/facades/links-facade.js';
|
|
36
|
+
import { createBranchingFacadeOps } from '../runtime/facades/branching-facade.js';
|
|
37
|
+
import { createTierFacadeOps } from '../runtime/facades/tier-facade.js';
|
|
38
|
+
import { createEmbeddingFacadeOps } from '../runtime/facades/embedding-facade.js';
|
|
39
|
+
|
|
40
|
+
// ── Public types ────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface SkillValidationError {
|
|
43
|
+
file: string;
|
|
44
|
+
op: string;
|
|
45
|
+
message: string;
|
|
46
|
+
line?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SkillValidationResult {
|
|
50
|
+
valid: boolean;
|
|
51
|
+
errors: SkillValidationError[];
|
|
52
|
+
totalFiles: number;
|
|
53
|
+
totalExamples: number;
|
|
54
|
+
registrySize: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Internal types ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
interface OpExample {
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
opName: string;
|
|
63
|
+
rawParams: string;
|
|
64
|
+
parsedParams: Record<string, unknown> | null;
|
|
65
|
+
parseError?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Mock runtime ────────────────────────────────────────────────────────
|
|
69
|
+
// Schemas are constructed during factory calls but handlers are never invoked.
|
|
70
|
+
|
|
71
|
+
function createNoopProxy(): AgentRuntime {
|
|
72
|
+
const handler: ProxyHandler<object> = {
|
|
73
|
+
get(_target, prop) {
|
|
74
|
+
if (prop === Symbol.toPrimitive) return () => '';
|
|
75
|
+
if (prop === Symbol.iterator) return undefined;
|
|
76
|
+
if (prop === 'then') return undefined;
|
|
77
|
+
if (prop === 'toString') return () => '[mock]';
|
|
78
|
+
if (prop === 'valueOf') return () => 0;
|
|
79
|
+
if (prop === 'length') return 0;
|
|
80
|
+
return new Proxy(function () {}, handler);
|
|
81
|
+
},
|
|
82
|
+
apply() {
|
|
83
|
+
return new Proxy({}, handler);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
return new Proxy({}, handler) as unknown as AgentRuntime;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Schema registry ─────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function buildSchemaRegistry(): Map<string, ZodType> {
|
|
92
|
+
const runtime = createNoopProxy();
|
|
93
|
+
const registry = new Map<string, ZodType>();
|
|
94
|
+
|
|
95
|
+
const facadeFactories: Array<(rt: AgentRuntime) => OpDefinition[]> = [
|
|
96
|
+
createVaultFacadeOps,
|
|
97
|
+
createPlanFacadeOps,
|
|
98
|
+
createBrainFacadeOps,
|
|
99
|
+
createMemoryFacadeOps,
|
|
100
|
+
createAdminFacadeOps,
|
|
101
|
+
createCuratorFacadeOps,
|
|
102
|
+
createLoopFacadeOps,
|
|
103
|
+
createOrchestrateFacadeOps,
|
|
104
|
+
createControlFacadeOps,
|
|
105
|
+
createContextFacadeOps,
|
|
106
|
+
createAgencyFacadeOps,
|
|
107
|
+
createChatFacadeOps,
|
|
108
|
+
createOperatorFacadeOps,
|
|
109
|
+
createArchiveFacadeOps,
|
|
110
|
+
createSyncFacadeOps,
|
|
111
|
+
createReviewFacadeOps,
|
|
112
|
+
createIntakeFacadeOps,
|
|
113
|
+
createLinksFacadeOps,
|
|
114
|
+
createBranchingFacadeOps,
|
|
115
|
+
createTierFacadeOps,
|
|
116
|
+
createEmbeddingFacadeOps,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const factory of facadeFactories) {
|
|
120
|
+
try {
|
|
121
|
+
const ops = factory(runtime);
|
|
122
|
+
for (const op of ops) {
|
|
123
|
+
if (op.schema) {
|
|
124
|
+
registry.set(op.name, op.schema);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Some facades may fail with the mock runtime — skip them.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return registry;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── SKILL.md discovery ──────────────────────────────────────────────────
|
|
136
|
+
// Discovers all SKILL.md files directly inside skillsDir.
|
|
137
|
+
// Supports both layouts:
|
|
138
|
+
// - skillsDir/{name}/SKILL.md (directory layout)
|
|
139
|
+
// - skillsDir/{name}.md (flat file layout)
|
|
140
|
+
|
|
141
|
+
function discoverSkillFiles(skillsDir: string): string[] {
|
|
142
|
+
const paths: string[] = [];
|
|
143
|
+
|
|
144
|
+
if (!existsSync(skillsDir)) return paths;
|
|
145
|
+
|
|
146
|
+
let entries: string[];
|
|
147
|
+
try {
|
|
148
|
+
entries = readdirSync(skillsDir);
|
|
149
|
+
} catch {
|
|
150
|
+
return paths;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const entryPath = join(skillsDir, entry);
|
|
155
|
+
try {
|
|
156
|
+
const stat = statSync(entryPath);
|
|
157
|
+
if (stat.isDirectory()) {
|
|
158
|
+
const skillMd = join(entryPath, 'SKILL.md');
|
|
159
|
+
if (existsSync(skillMd)) {
|
|
160
|
+
paths.push(skillMd);
|
|
161
|
+
}
|
|
162
|
+
} else if (entry.endsWith('.md')) {
|
|
163
|
+
paths.push(entryPath);
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Skip unreadable entries
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return paths;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── SKILL.md parser ─────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function extractOpExamples(filePath: string): OpExample[] {
|
|
176
|
+
let content: string;
|
|
177
|
+
try {
|
|
178
|
+
content = readFileSync(filePath, 'utf-8');
|
|
179
|
+
} catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const lines = content.split('\n');
|
|
184
|
+
const examples: OpExample[] = [];
|
|
185
|
+
|
|
186
|
+
let inCodeBlock = false;
|
|
187
|
+
let codeBlockStart = -1;
|
|
188
|
+
let codeBlockLines: string[] = [];
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
191
|
+
const line = lines[i];
|
|
192
|
+
if (line.trimStart().startsWith('```')) {
|
|
193
|
+
if (!inCodeBlock) {
|
|
194
|
+
inCodeBlock = true;
|
|
195
|
+
codeBlockStart = i + 1;
|
|
196
|
+
codeBlockLines = [];
|
|
197
|
+
} else {
|
|
198
|
+
extractFromCodeBlock(filePath, codeBlockStart, codeBlockLines, examples);
|
|
199
|
+
inCodeBlock = false;
|
|
200
|
+
codeBlockLines = [];
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (inCodeBlock) {
|
|
205
|
+
codeBlockLines.push(line);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return examples;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractFromCodeBlock(
|
|
213
|
+
filePath: string,
|
|
214
|
+
startLine: number,
|
|
215
|
+
blockLines: string[],
|
|
216
|
+
results: OpExample[],
|
|
217
|
+
): void {
|
|
218
|
+
const opPattern = /(?:YOUR_AGENT_\w+\s+)?op:(\w+)(?:\s+params:\s*(.*))?/;
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < blockLines.length; i++) {
|
|
221
|
+
const line = blockLines[i];
|
|
222
|
+
const match = line.match(opPattern);
|
|
223
|
+
if (!match) continue;
|
|
224
|
+
|
|
225
|
+
const opName = match[1];
|
|
226
|
+
const lineNum = startLine + i + 1;
|
|
227
|
+
|
|
228
|
+
let rawParams = '';
|
|
229
|
+
|
|
230
|
+
if (match[2]) {
|
|
231
|
+
rawParams = match[2].trim();
|
|
232
|
+
} else if (i + 1 < blockLines.length && blockLines[i + 1].trim().startsWith('params:')) {
|
|
233
|
+
const paramsLine = blockLines[i + 1].trim();
|
|
234
|
+
const paramsMatch = paramsLine.match(/^params:\s*(.*)/);
|
|
235
|
+
if (paramsMatch) {
|
|
236
|
+
rawParams = paramsMatch[1].trim();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (rawParams) {
|
|
241
|
+
const fullParams = collectMultiLineParams(blockLines, i, rawParams);
|
|
242
|
+
const { parsed, error } = parseLooseJson(fullParams);
|
|
243
|
+
|
|
244
|
+
results.push({
|
|
245
|
+
file: filePath,
|
|
246
|
+
line: lineNum,
|
|
247
|
+
opName,
|
|
248
|
+
rawParams: fullParams,
|
|
249
|
+
parsedParams: parsed,
|
|
250
|
+
parseError: error,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function collectMultiLineParams(blockLines: string[], opLineIdx: number, initial: string): string {
|
|
257
|
+
if (isBalanced(initial)) return initial;
|
|
258
|
+
|
|
259
|
+
let startIdx = opLineIdx + 1;
|
|
260
|
+
if (!blockLines[opLineIdx].includes('params:') && startIdx < blockLines.length) {
|
|
261
|
+
if (blockLines[startIdx].trim().startsWith('params:')) {
|
|
262
|
+
startIdx++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let result = initial;
|
|
267
|
+
for (let j = startIdx; j < blockLines.length; j++) {
|
|
268
|
+
const nextLine = blockLines[j].trim();
|
|
269
|
+
if (!nextLine) continue;
|
|
270
|
+
if (nextLine.match(/(?:YOUR_AGENT_\w+\s+)?op:\w+/)) break;
|
|
271
|
+
result += '\n' + nextLine;
|
|
272
|
+
if (isBalanced(result)) break;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isBalanced(s: string): boolean {
|
|
279
|
+
let depth = 0;
|
|
280
|
+
for (const ch of s) {
|
|
281
|
+
if (ch === '{' || ch === '[') depth++;
|
|
282
|
+
if (ch === '}' || ch === ']') depth--;
|
|
283
|
+
if (depth < 0) return false;
|
|
284
|
+
}
|
|
285
|
+
return depth === 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseLooseJson(raw: string): {
|
|
289
|
+
parsed: Record<string, unknown> | null;
|
|
290
|
+
error?: string;
|
|
291
|
+
} {
|
|
292
|
+
if (!raw.trim()) return { parsed: null };
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const normalized = raw
|
|
296
|
+
.replace(/"<([^">]+)>"/g, '"$1"')
|
|
297
|
+
.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":')
|
|
298
|
+
.replace(/'/g, '"')
|
|
299
|
+
.replace(/,\s*([}\]])/g, '$1')
|
|
300
|
+
.replace(/\.\.\./g, '')
|
|
301
|
+
.replace(/\["?<[^>]+>"?\]/g, '["placeholder"]');
|
|
302
|
+
|
|
303
|
+
const result = JSON.parse(normalized);
|
|
304
|
+
return { parsed: result };
|
|
305
|
+
} catch (e) {
|
|
306
|
+
try {
|
|
307
|
+
const result = extractFlatParams(raw);
|
|
308
|
+
if (result && Object.keys(result).length > 0) {
|
|
309
|
+
return { parsed: result };
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// fall through
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
parsed: null,
|
|
316
|
+
error: `Cannot parse params: ${(e as Error).message}`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function extractFlatParams(raw: string): Record<string, unknown> | null {
|
|
322
|
+
const result: Record<string, unknown> = {};
|
|
323
|
+
const kvPattern = /(\w+)\s*:\s*(?:"([^"]*)"|\[([^\]]*)\]|(\{[^}]*\})|(\w+))/g;
|
|
324
|
+
let match;
|
|
325
|
+
let found = false;
|
|
326
|
+
|
|
327
|
+
while ((match = kvPattern.exec(raw)) !== null) {
|
|
328
|
+
found = true;
|
|
329
|
+
const key = match[1];
|
|
330
|
+
if (match[2] !== undefined) {
|
|
331
|
+
result[key] = match[2].replace(/<[^>]+>/g, 'placeholder');
|
|
332
|
+
} else if (match[3] !== undefined) {
|
|
333
|
+
result[key] = ['placeholder'];
|
|
334
|
+
} else if (match[4] !== undefined) {
|
|
335
|
+
result[key] = {};
|
|
336
|
+
} else if (match[5] !== undefined) {
|
|
337
|
+
const val = match[5];
|
|
338
|
+
if (val === 'true') result[key] = true;
|
|
339
|
+
else if (val === 'false') result[key] = false;
|
|
340
|
+
else if (/^\d+$/.test(val)) result[key] = parseInt(val, 10);
|
|
341
|
+
else result[key] = val;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return found ? result : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Validation ──────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
function isPlaceholder(value: unknown): boolean {
|
|
351
|
+
if (typeof value !== 'string') return false;
|
|
352
|
+
if (value.includes('|')) return true;
|
|
353
|
+
if (/^<.*>$/.test(value)) return true;
|
|
354
|
+
if (/^(placeholder|example|value|name|id|title|description|type|domain)$/i.test(value))
|
|
355
|
+
return true;
|
|
356
|
+
if (
|
|
357
|
+
/^[\w]+-[\w]+$/.test(value) &&
|
|
358
|
+
/\b(correct|your|my|the|this|some|new|old|entry|item|current)\b/i.test(value)
|
|
359
|
+
)
|
|
360
|
+
return true;
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isPlaceholderIssue(
|
|
365
|
+
issue: { code: string; path: (string | number)[]; received?: unknown; message: string },
|
|
366
|
+
params: Record<string, unknown>,
|
|
367
|
+
): boolean {
|
|
368
|
+
if (issue.code === 'invalid_enum_value') {
|
|
369
|
+
const received = issue.received ?? getNestedValue(params, issue.path);
|
|
370
|
+
if (isPlaceholder(received)) return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getNestedValue(obj: Record<string, unknown>, path: (string | number)[]): unknown {
|
|
376
|
+
let current: unknown = obj;
|
|
377
|
+
for (const key of path) {
|
|
378
|
+
if (current === null || current === undefined || typeof current !== 'object') return undefined;
|
|
379
|
+
current = (current as Record<string | number, unknown>)[key];
|
|
380
|
+
}
|
|
381
|
+
return current;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function validateExamples(
|
|
385
|
+
examples: OpExample[],
|
|
386
|
+
registry: Map<string, ZodType>,
|
|
387
|
+
): SkillValidationError[] {
|
|
388
|
+
const errors: SkillValidationError[] = [];
|
|
389
|
+
|
|
390
|
+
for (const ex of examples) {
|
|
391
|
+
if (ex.parseError) continue;
|
|
392
|
+
if (!ex.parsedParams) continue;
|
|
393
|
+
|
|
394
|
+
const schema = registry.get(ex.opName);
|
|
395
|
+
if (!schema) {
|
|
396
|
+
errors.push({
|
|
397
|
+
file: ex.file,
|
|
398
|
+
op: ex.opName,
|
|
399
|
+
line: ex.line,
|
|
400
|
+
message: `unknown op — not found in any facade schema registry`,
|
|
401
|
+
});
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const result = (
|
|
406
|
+
schema as {
|
|
407
|
+
safeParse: (p: unknown) => { success: boolean; error?: ZodError };
|
|
408
|
+
}
|
|
409
|
+
).safeParse(ex.parsedParams);
|
|
410
|
+
|
|
411
|
+
if (!result.success && result.error) {
|
|
412
|
+
for (const issue of result.error.issues) {
|
|
413
|
+
if (
|
|
414
|
+
isPlaceholderIssue(
|
|
415
|
+
issue as {
|
|
416
|
+
code: string;
|
|
417
|
+
path: (string | number)[];
|
|
418
|
+
received?: unknown;
|
|
419
|
+
message: string;
|
|
420
|
+
},
|
|
421
|
+
ex.parsedParams,
|
|
422
|
+
)
|
|
423
|
+
) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const path = issue.path.join('.');
|
|
427
|
+
errors.push({
|
|
428
|
+
file: ex.file,
|
|
429
|
+
op: ex.opName,
|
|
430
|
+
line: ex.line,
|
|
431
|
+
message: `${path ? path + ': ' : ''}${issue.message}`,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return errors;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Validate all SKILL.md files found in `skillsDir`.
|
|
444
|
+
*
|
|
445
|
+
* `skillsDir` is the directory that contains skill subdirectories, e.g. ~/.claude/skills/.
|
|
446
|
+
* Each subdirectory is expected to have a SKILL.md file.
|
|
447
|
+
*
|
|
448
|
+
* @returns Structured result with errors, counts, and whether all examples are valid.
|
|
449
|
+
*/
|
|
450
|
+
export function validateSkillDocs(skillsDir: string): SkillValidationResult {
|
|
451
|
+
const registry = buildSchemaRegistry();
|
|
452
|
+
const skillFiles = discoverSkillFiles(skillsDir);
|
|
453
|
+
let totalExamples = 0;
|
|
454
|
+
const allErrors: SkillValidationError[] = [];
|
|
455
|
+
|
|
456
|
+
for (const file of skillFiles) {
|
|
457
|
+
const examples = extractOpExamples(file);
|
|
458
|
+
totalExamples += examples.length;
|
|
459
|
+
const errors = validateExamples(examples, registry);
|
|
460
|
+
allErrors.push(...errors);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
valid: allErrors.length === 0,
|
|
465
|
+
errors: allErrors,
|
|
466
|
+
totalFiles: skillFiles.length,
|
|
467
|
+
totalExamples,
|
|
468
|
+
registrySize: registry.size,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default canonical tag taxonomy for Soleri agents.
|
|
3
|
+
*
|
|
4
|
+
* These tags represent the most common knowledge domains. When a vault is
|
|
5
|
+
* configured with tagConstraintMode 'suggest' or 'enforce', incoming tags
|
|
6
|
+
* are mapped to the nearest entry in this list via edit-distance matching.
|
|
7
|
+
*
|
|
8
|
+
* To use these defaults in your agent runtime config:
|
|
9
|
+
* import { DEFAULT_CANONICAL_TAGS } from '@soleri/core';
|
|
10
|
+
* // ...
|
|
11
|
+
* canonicalTags: DEFAULT_CANONICAL_TAGS,
|
|
12
|
+
* tagConstraintMode: 'suggest',
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_CANONICAL_TAGS: string[] = [
|
|
15
|
+
'architecture',
|
|
16
|
+
'typescript',
|
|
17
|
+
'react',
|
|
18
|
+
'testing',
|
|
19
|
+
'workflow',
|
|
20
|
+
'design-tokens',
|
|
21
|
+
'accessibility',
|
|
22
|
+
'performance',
|
|
23
|
+
'security',
|
|
24
|
+
'planning',
|
|
25
|
+
'soleri',
|
|
26
|
+
'vault',
|
|
27
|
+
'mcp',
|
|
28
|
+
'claude-code',
|
|
29
|
+
'ai',
|
|
30
|
+
'learning',
|
|
31
|
+
'gamification',
|
|
32
|
+
'education',
|
|
33
|
+
'adhd',
|
|
34
|
+
'routing',
|
|
35
|
+
'orchestration',
|
|
36
|
+
'skills',
|
|
37
|
+
'automation',
|
|
38
|
+
'git',
|
|
39
|
+
'database',
|
|
40
|
+
'api',
|
|
41
|
+
'authentication',
|
|
42
|
+
'subagent',
|
|
43
|
+
'design-system',
|
|
44
|
+
'component',
|
|
45
|
+
'frontend',
|
|
46
|
+
'backend',
|
|
47
|
+
'tooling',
|
|
48
|
+
'monorepo',
|
|
49
|
+
'refactoring',
|
|
50
|
+
'debugging',
|
|
51
|
+
'deployment',
|
|
52
|
+
'configuration',
|
|
53
|
+
'documentation',
|
|
54
|
+
'pattern',
|
|
55
|
+
'anti-pattern',
|
|
56
|
+
'principle',
|
|
57
|
+
'decision',
|
|
58
|
+
'migration',
|
|
59
|
+
'plugin',
|
|
60
|
+
'hook',
|
|
61
|
+
'schema',
|
|
62
|
+
'pipeline',
|
|
63
|
+
'ingestion',
|
|
64
|
+
];
|