opencode-agent-skill 9.0.0 → 11.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/CHANGELOG.md +116 -0
- package/README.md +742 -675
- package/bin/ocskill.mjs +354 -5
- package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +75 -0
- package/docs/V11-PERCEPTION-ADAPTIVE.md +220 -0
- package/evals/router-triggers.json +82 -0
- package/evals/routing.json +76 -0
- package/evals/v11/tasks.json +122 -0
- package/global-config/AGENTS.md +78 -163
- package/global-config/agents/merge-arbiter.md +12 -0
- package/global-config/agents/visual-verifier.md +12 -0
- package/global-config/plugins/ues-router/index.js +683 -59
- package/global-config/plugins/ues-router/router.js +62 -3
- package/global-config/plugins/ues-router/runtime-guard.js +265 -0
- package/global-config/skills/browser-qa/SKILL.md +14 -0
- package/global-config/skills/browser-qa/references/workflow.md +11 -0
- package/global-config/skills/browser-security/SKILL.md +12 -0
- package/global-config/skills/component-visual-testing/SKILL.md +10 -0
- package/global-config/skills/design-source/SKILL.md +10 -0
- package/global-config/skills/design-source/references/workflow.md +12 -0
- package/global-config/skills/dynamic-workflow/SKILL.md +18 -0
- package/global-config/skills/dynamic-workflow/references/workflow.md +19 -0
- package/global-config/skills/responsive-verification/SKILL.md +10 -0
- package/global-config/skills/skill-authoring/SKILL.md +12 -0
- package/global-config/skills/skill-evaluation/SKILL.md +17 -0
- package/global-config/skills/visual-fidelity/SKILL.md +14 -0
- package/global-config/skills/visual-fidelity/references/workflow.md +14 -0
- package/lib/benchmark-confidence.mjs +49 -11
- package/lib/browser-adapter.mjs +82 -0
- package/lib/browser-runtime.mjs +193 -0
- package/lib/capability-registry.mjs +109 -0
- package/lib/context-engine-v11.mjs +146 -0
- package/lib/context-manifest.mjs +16 -3
- package/lib/control-center.mjs +12 -2
- package/lib/dynamic-workflow.mjs +179 -0
- package/lib/eval-ablation.mjs +146 -0
- package/lib/eval-report.mjs +83 -0
- package/lib/eval-telemetry.mjs +64 -0
- package/lib/evidence-budget.mjs +84 -0
- package/lib/evidence-store.mjs +178 -0
- package/lib/hermes-bridge.mjs +45 -1
- package/lib/model-config.mjs +9 -1
- package/lib/model-policy.mjs +58 -1
- package/lib/orchestrator-policy.mjs +100 -7
- package/lib/png-diff.mjs +229 -0
- package/lib/prompt-cache.mjs +60 -0
- package/lib/skill-quality.mjs +72 -0
- package/lib/task-engine.mjs +223 -12
- package/lib/ui-inspector.mjs +152 -0
- package/lib/v11-metrics.mjs +64 -0
- package/lib/visual-spec.mjs +159 -0
- package/package.json +11 -5
- package/scripts/eval-ablation.mjs +47 -0
- package/scripts/eval-matrix.mjs +13 -2
- package/scripts/validate-v11-suite.mjs +58 -0
- package/scripts/validate.mjs +16 -4
package/lib/png-diff.mjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { inflateSync, deflateSync } from "node:zlib"
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
const SIGNATURE = Buffer.from([137,80,78,71,13,10,26,10])
|
|
5
|
+
|
|
6
|
+
function paeth(a, b, c) {
|
|
7
|
+
const p = a + b - c
|
|
8
|
+
const pa = Math.abs(p - a)
|
|
9
|
+
const pb = Math.abs(p - b)
|
|
10
|
+
const pc = Math.abs(p - c)
|
|
11
|
+
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function bytesPerPixel(colorType) {
|
|
15
|
+
if (colorType === 0 || colorType === 3) return 1
|
|
16
|
+
if (colorType === 2) return 3
|
|
17
|
+
if (colorType === 4) return 2
|
|
18
|
+
if (colorType === 6) return 4
|
|
19
|
+
throw new Error("Unsupported PNG color type: " + colorType)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseChunks(buffer) {
|
|
23
|
+
if (buffer.length < 8 || !buffer.subarray(0, 8).equals(SIGNATURE)) throw new Error("Invalid PNG signature")
|
|
24
|
+
const chunks = []
|
|
25
|
+
let offset = 8
|
|
26
|
+
while (offset + 12 <= buffer.length) {
|
|
27
|
+
const length = buffer.readUInt32BE(offset)
|
|
28
|
+
const type = buffer.toString("ascii", offset + 4, offset + 8)
|
|
29
|
+
const dataStart = offset + 8
|
|
30
|
+
const dataEnd = dataStart + length
|
|
31
|
+
if (dataEnd + 4 > buffer.length) throw new Error("Truncated PNG chunk")
|
|
32
|
+
chunks.push({ type, data: buffer.subarray(dataStart, dataEnd) })
|
|
33
|
+
offset = dataEnd + 4
|
|
34
|
+
if (type === "IEND") break
|
|
35
|
+
}
|
|
36
|
+
return chunks
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function decodePng(buffer) {
|
|
40
|
+
const chunks = parseChunks(buffer)
|
|
41
|
+
const ihdr = chunks.find((chunk) => chunk.type === "IHDR")?.data
|
|
42
|
+
if (!ihdr || ihdr.length < 13) throw new Error("PNG missing IHDR")
|
|
43
|
+
const width = ihdr.readUInt32BE(0)
|
|
44
|
+
const height = ihdr.readUInt32BE(4)
|
|
45
|
+
const bitDepth = ihdr[8]
|
|
46
|
+
const colorType = ihdr[9]
|
|
47
|
+
const interlace = ihdr[12]
|
|
48
|
+
if (bitDepth !== 8) throw new Error("Only 8-bit PNG is supported")
|
|
49
|
+
if (interlace !== 0) throw new Error("Interlaced PNG is not supported")
|
|
50
|
+
|
|
51
|
+
const paletteChunk = chunks.find((chunk) => chunk.type === "PLTE")?.data || null
|
|
52
|
+
const transparency = chunks.find((chunk) => chunk.type === "tRNS")?.data || null
|
|
53
|
+
const idat = Buffer.concat(chunks.filter((chunk) => chunk.type === "IDAT").map((chunk) => chunk.data))
|
|
54
|
+
const bpp = bytesPerPixel(colorType)
|
|
55
|
+
const pixelsCount = width * height
|
|
56
|
+
if (!Number.isSafeInteger(pixelsCount) || pixelsCount <= 0 || pixelsCount > 50_000_000) {
|
|
57
|
+
throw new Error("PNG dimensions exceed the V11 visual safety budget")
|
|
58
|
+
}
|
|
59
|
+
const stride = width * bpp
|
|
60
|
+
const expectedRawBytes = height * (1 + stride)
|
|
61
|
+
if (!Number.isSafeInteger(expectedRawBytes) || expectedRawBytes > 256 * 1024 * 1024) {
|
|
62
|
+
throw new Error("PNG decompressed payload exceeds the V11 visual safety budget")
|
|
63
|
+
}
|
|
64
|
+
const raw = inflateSync(idat, { maxOutputLength: expectedRawBytes })
|
|
65
|
+
if (raw.length !== expectedRawBytes) throw new Error("PNG decompressed payload length does not match IHDR")
|
|
66
|
+
const pixels = Buffer.alloc(stride * height)
|
|
67
|
+
let input = 0
|
|
68
|
+
|
|
69
|
+
for (let y = 0; y < height; y += 1) {
|
|
70
|
+
const filter = raw[input++]
|
|
71
|
+
const row = y * stride
|
|
72
|
+
for (let x = 0; x < stride; x += 1) {
|
|
73
|
+
const byte = raw[input++]
|
|
74
|
+
const left = x >= bpp ? pixels[row + x - bpp] : 0
|
|
75
|
+
const up = y > 0 ? pixels[row - stride + x] : 0
|
|
76
|
+
const upLeft = y > 0 && x >= bpp ? pixels[row - stride + x - bpp] : 0
|
|
77
|
+
let value
|
|
78
|
+
if (filter === 0) value = byte
|
|
79
|
+
else if (filter === 1) value = (byte + left) & 255
|
|
80
|
+
else if (filter === 2) value = (byte + up) & 255
|
|
81
|
+
else if (filter === 3) value = (byte + Math.floor((left + up) / 2)) & 255
|
|
82
|
+
else if (filter === 4) value = (byte + paeth(left, up, upLeft)) & 255
|
|
83
|
+
else throw new Error("Unsupported PNG filter: " + filter)
|
|
84
|
+
pixels[row + x] = value
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rgba = Buffer.alloc(width * height * 4)
|
|
89
|
+
for (let i = 0, p = 0; i < pixels.length; i += bpp, p += 4) {
|
|
90
|
+
if (colorType === 6) {
|
|
91
|
+
rgba[p] = pixels[i]; rgba[p+1] = pixels[i+1]; rgba[p+2] = pixels[i+2]; rgba[p+3] = pixels[i+3]
|
|
92
|
+
} else if (colorType === 2) {
|
|
93
|
+
rgba[p] = pixels[i]; rgba[p+1] = pixels[i+1]; rgba[p+2] = pixels[i+2]; rgba[p+3] = 255
|
|
94
|
+
} else if (colorType === 0) {
|
|
95
|
+
rgba[p] = pixels[i]; rgba[p+1] = pixels[i]; rgba[p+2] = pixels[i]; rgba[p+3] = 255
|
|
96
|
+
} else if (colorType === 4) {
|
|
97
|
+
rgba[p] = pixels[i]; rgba[p+1] = pixels[i]; rgba[p+2] = pixels[i]; rgba[p+3] = pixels[i+1]
|
|
98
|
+
} else if (colorType === 3) {
|
|
99
|
+
if (!paletteChunk) throw new Error("Indexed PNG missing PLTE")
|
|
100
|
+
const index = pixels[i]
|
|
101
|
+
rgba[p] = paletteChunk[index * 3] ?? 0
|
|
102
|
+
rgba[p+1] = paletteChunk[index * 3 + 1] ?? 0
|
|
103
|
+
rgba[p+2] = paletteChunk[index * 3 + 2] ?? 0
|
|
104
|
+
rgba[p+3] = transparency?.[index] ?? 255
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { width, height, rgba }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let crcTable = null
|
|
111
|
+
function crc32(buffer) {
|
|
112
|
+
if (!crcTable) {
|
|
113
|
+
crcTable = Array.from({ length: 256 }, (_, n) => {
|
|
114
|
+
let c = n
|
|
115
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
|
116
|
+
return c >>> 0
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
let crc = 0xffffffff
|
|
120
|
+
for (const byte of buffer) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8)
|
|
121
|
+
return (crc ^ 0xffffffff) >>> 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function chunk(type, data) {
|
|
125
|
+
const typeBytes = Buffer.from(type, "ascii")
|
|
126
|
+
const length = Buffer.alloc(4)
|
|
127
|
+
length.writeUInt32BE(data.length)
|
|
128
|
+
const crc = Buffer.alloc(4)
|
|
129
|
+
crc.writeUInt32BE(crc32(Buffer.concat([typeBytes, data])))
|
|
130
|
+
return Buffer.concat([length, typeBytes, data, crc])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function encodeRgbaPng({ width, height, rgba }) {
|
|
134
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) throw new Error("Invalid PNG dimensions")
|
|
135
|
+
if (!Buffer.isBuffer(rgba) || rgba.length !== width * height * 4) throw new Error("Invalid RGBA buffer")
|
|
136
|
+
const ihdr = Buffer.alloc(13)
|
|
137
|
+
ihdr.writeUInt32BE(width, 0)
|
|
138
|
+
ihdr.writeUInt32BE(height, 4)
|
|
139
|
+
ihdr[8] = 8
|
|
140
|
+
ihdr[9] = 6
|
|
141
|
+
const scan = Buffer.alloc(height * (1 + width * 4))
|
|
142
|
+
for (let y = 0; y < height; y += 1) {
|
|
143
|
+
const row = y * (1 + width * 4)
|
|
144
|
+
scan[row] = 0
|
|
145
|
+
rgba.copy(scan, row + 1, y * width * 4, (y + 1) * width * 4)
|
|
146
|
+
}
|
|
147
|
+
return Buffer.concat([SIGNATURE, chunk("IHDR", ihdr), chunk("IDAT", deflateSync(scan)), chunk("IEND", Buffer.alloc(0))])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function comparePngBuffers(expectedBuffer, actualBuffer, options = {}) {
|
|
151
|
+
const expected = decodePng(expectedBuffer)
|
|
152
|
+
const actual = decodePng(actualBuffer)
|
|
153
|
+
if (expected.width !== actual.width || expected.height !== actual.height) {
|
|
154
|
+
return {
|
|
155
|
+
schemaVersion: 1,
|
|
156
|
+
verdict: "FAIL",
|
|
157
|
+
reason: "dimension-mismatch",
|
|
158
|
+
expected: { width: expected.width, height: expected.height },
|
|
159
|
+
actual: { width: actual.width, height: actual.height },
|
|
160
|
+
differentPixels: expected.width * expected.height,
|
|
161
|
+
differentRatio: 1,
|
|
162
|
+
bounds: { x: 0, y: 0, width: Math.max(expected.width, actual.width), height: Math.max(expected.height, actual.height) },
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const threshold = Math.max(0, Math.min(255, Number(options.threshold ?? 16)))
|
|
167
|
+
const allowedRatio = Math.max(0, Math.min(1, Number(options.maxDiffRatio ?? 0)))
|
|
168
|
+
let differentPixels = 0
|
|
169
|
+
let minX = expected.width
|
|
170
|
+
let minY = expected.height
|
|
171
|
+
let maxX = -1
|
|
172
|
+
let maxY = -1
|
|
173
|
+
|
|
174
|
+
for (let pixel = 0; pixel < expected.width * expected.height; pixel += 1) {
|
|
175
|
+
const offset = pixel * 4
|
|
176
|
+
let delta = 0
|
|
177
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
178
|
+
delta = Math.max(delta, Math.abs(expected.rgba[offset + channel] - actual.rgba[offset + channel]))
|
|
179
|
+
}
|
|
180
|
+
if (delta <= threshold) continue
|
|
181
|
+
differentPixels += 1
|
|
182
|
+
const x = pixel % expected.width
|
|
183
|
+
const y = Math.floor(pixel / expected.width)
|
|
184
|
+
minX = Math.min(minX, x); maxX = Math.max(maxX, x)
|
|
185
|
+
minY = Math.min(minY, y); maxY = Math.max(maxY, y)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const totalPixels = expected.width * expected.height
|
|
189
|
+
const differentRatio = totalPixels ? differentPixels / totalPixels : 0
|
|
190
|
+
const bounds = differentPixels
|
|
191
|
+
? { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 }
|
|
192
|
+
: null
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
schemaVersion: 1,
|
|
196
|
+
verdict: differentRatio <= allowedRatio ? "PASS" : "FAIL",
|
|
197
|
+
width: expected.width,
|
|
198
|
+
height: expected.height,
|
|
199
|
+
differentPixels,
|
|
200
|
+
differentRatio,
|
|
201
|
+
threshold,
|
|
202
|
+
maxDiffRatio: allowedRatio,
|
|
203
|
+
bounds,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function comparePngFiles(expectedFile, actualFile, options = {}) {
|
|
208
|
+
return comparePngBuffers(await readFile(expectedFile), await readFile(actualFile), options)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function cropDecodedPng(decoded, bounds) {
|
|
212
|
+
const x = Math.max(0, Math.floor(Number(bounds?.x || 0)))
|
|
213
|
+
const y = Math.max(0, Math.floor(Number(bounds?.y || 0)))
|
|
214
|
+
const width = Math.max(1, Math.min(decoded.width - x, Math.floor(Number(bounds?.width || decoded.width))))
|
|
215
|
+
const height = Math.max(1, Math.min(decoded.height - y, Math.floor(Number(bounds?.height || decoded.height))))
|
|
216
|
+
const rgba = Buffer.alloc(width * height * 4)
|
|
217
|
+
for (let row = 0; row < height; row += 1) {
|
|
218
|
+
const sourceStart = ((y + row) * decoded.width + x) * 4
|
|
219
|
+
decoded.rgba.copy(rgba, row * width * 4, sourceStart, sourceStart + width * 4)
|
|
220
|
+
}
|
|
221
|
+
return { width, height, rgba }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function cropPngFile(inputFile, outputFile, bounds) {
|
|
225
|
+
const decoded = decodePng(await readFile(inputFile))
|
|
226
|
+
const cropped = cropDecodedPng(decoded, bounds)
|
|
227
|
+
await writeFile(outputFile, encodeRgbaPng(cropped))
|
|
228
|
+
return { file: outputFile, width: cropped.width, height: cropped.height }
|
|
229
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
function canonical(value) {
|
|
4
|
+
if (Array.isArray(value)) return value.map(canonical)
|
|
5
|
+
if (!value || typeof value !== "object") return value
|
|
6
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function stringify(value) {
|
|
10
|
+
return typeof value === "string" ? value : JSON.stringify(canonical(value))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function digest(value) {
|
|
14
|
+
return createHash("sha256").update(stringify(value)).digest("hex")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function chars(value) {
|
|
18
|
+
return stringify(value ?? "").length
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildPromptEnvelope(input = {}) {
|
|
22
|
+
const stable = {
|
|
23
|
+
invariants: input.invariants || "",
|
|
24
|
+
role: input.role || "",
|
|
25
|
+
skills: input.skills || [],
|
|
26
|
+
projectFacts: input.projectFacts || {},
|
|
27
|
+
toolPolicy: input.toolPolicy || {},
|
|
28
|
+
}
|
|
29
|
+
const dynamic = {
|
|
30
|
+
task: input.task || {},
|
|
31
|
+
evidence: input.evidence || [],
|
|
32
|
+
recentFailure: input.recentFailure || null,
|
|
33
|
+
nextAction: input.nextAction || null,
|
|
34
|
+
recentMessages: input.recentMessages || [],
|
|
35
|
+
}
|
|
36
|
+
const stableChars = chars(stable)
|
|
37
|
+
const dynamicChars = chars(dynamic)
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
stable,
|
|
41
|
+
dynamic,
|
|
42
|
+
stablePrefixHash: digest(stable),
|
|
43
|
+
dynamicHash: digest(dynamic),
|
|
44
|
+
stableChars,
|
|
45
|
+
dynamicChars,
|
|
46
|
+
cacheableRatio: stableChars + dynamicChars ? stableChars / (stableChars + dynamicChars) : 0,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function comparePromptEnvelopes(previous, current) {
|
|
51
|
+
const stableReused = Boolean(previous?.stablePrefixHash && previous.stablePrefixHash === current?.stablePrefixHash)
|
|
52
|
+
const repeatedStableChars = stableReused ? Number(current?.stableChars || 0) : 0
|
|
53
|
+
return {
|
|
54
|
+
schemaVersion: 1,
|
|
55
|
+
stableReused,
|
|
56
|
+
repeatedStableChars,
|
|
57
|
+
avoidableRepeatedChars: stableReused ? 0 : Math.min(Number(previous?.stableChars || 0), Number(current?.stableChars || 0)),
|
|
58
|
+
currentCacheableRatio: Number(current?.cacheableRatio || 0),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
function words(value) {
|
|
6
|
+
return new Set(String(value || "").toLowerCase().match(/[a-z0-9-]{3,}/g) || [])
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function similarity(a, b) {
|
|
10
|
+
const left = words(a)
|
|
11
|
+
const right = words(b)
|
|
12
|
+
const union = new Set([...left, ...right])
|
|
13
|
+
let intersection = 0
|
|
14
|
+
for (const item of left) if (right.has(item)) intersection += 1
|
|
15
|
+
return union.size ? intersection / union.size : 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function frontmatter(source) {
|
|
19
|
+
const name = source.match(/^name:\s*([^\r\n]+)/m)?.[1]?.trim() || null
|
|
20
|
+
const description = source.match(/^description:\s*([^\r\n]+)/m)?.[1]?.trim() || null
|
|
21
|
+
return { name, description }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function lintSkillCatalog(root = process.cwd(), options = {}) {
|
|
25
|
+
const skillsRoot = path.join(path.resolve(root), "global-config", "skills")
|
|
26
|
+
const entries = await readdir(skillsRoot, { withFileTypes: true })
|
|
27
|
+
const skills = []
|
|
28
|
+
const errors = []
|
|
29
|
+
const warnings = []
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (!entry.isDirectory()) continue
|
|
33
|
+
const file = path.join(skillsRoot, entry.name, "SKILL.md")
|
|
34
|
+
if (!existsSync(file)) {
|
|
35
|
+
errors.push({ skill: entry.name, issue: "missing-skill-md" })
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
const source = await readFile(file, "utf8")
|
|
39
|
+
const meta = frontmatter(source)
|
|
40
|
+
const body = source.replace(/^---[\s\S]*?---\s*/m, "")
|
|
41
|
+
const row = {
|
|
42
|
+
id: entry.name,
|
|
43
|
+
description: meta.description,
|
|
44
|
+
chars: source.length,
|
|
45
|
+
bodyChars: body.length,
|
|
46
|
+
estimatedTokens: Math.ceil(source.length / 4),
|
|
47
|
+
}
|
|
48
|
+
skills.push(row)
|
|
49
|
+
if (meta.name !== entry.name) errors.push({ skill: entry.name, issue: "frontmatter-name-mismatch" })
|
|
50
|
+
if (!meta.description) errors.push({ skill: entry.name, issue: "missing-description" })
|
|
51
|
+
if (row.estimatedTokens > Number(options.maxSkillTokens || 1800)) warnings.push({ skill: entry.name, issue: "large-entrypoint", estimatedTokens: row.estimatedTokens })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const collisions = []
|
|
55
|
+
const threshold = Number(options.collisionThreshold || 0.58)
|
|
56
|
+
for (let i = 0; i < skills.length; i += 1) {
|
|
57
|
+
for (let j = i + 1; j < skills.length; j += 1) {
|
|
58
|
+
const score = similarity(skills[i].description, skills[j].description)
|
|
59
|
+
if (score >= threshold) collisions.push({ a: skills[i].id, b: skills[j].id, score: Number(score.toFixed(3)) })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
valid: errors.length === 0,
|
|
66
|
+
skillCount: skills.length,
|
|
67
|
+
errors,
|
|
68
|
+
warnings,
|
|
69
|
+
collisions: collisions.sort((a, b) => b.score - a.score),
|
|
70
|
+
skills: skills.sort((a, b) => a.id.localeCompare(b.id)),
|
|
71
|
+
}
|
|
72
|
+
}
|