@erclx/aitk 3.15.0 → 3.17.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/bash-script/SKILL.md +1 -1
- package/claude/skills/ci-workflow/SKILL.md +1 -1
- package/claude/skills/cli-script/SKILL.md +1 -1
- package/claude/skills/create-skill/SKILL.md +1 -1
- package/claude/skills/create-snippet/SKILL.md +1 -1
- package/claude/skills/create-standard/SKILL.md +1 -1
- package/claude/skills/decision-escalate/SKILL.md +1 -1
- package/claude/skills/git-branch/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/youtube-transcripts/SKILL.md +1 -1
- package/docs/agents/audits.md +3 -3
- package/docs/agents/commands.md +23 -21
- package/docs/agents/index.md +1 -1
- package/docs/agents/records.md +23 -1
- package/package.json +1 -1
- package/src/audits/catalog.ts +35 -0
- package/src/claude/cases/all.ts +24 -0
- package/src/claude/cases/authoring.ts +53 -0
- package/src/claude/cases/claude-workflow.ts +153 -0
- package/src/claude/cases/git.ts +44 -0
- package/src/claude/cases/misc.ts +27 -0
- package/src/claude/cases/setup.ts +78 -0
- package/src/claude/skills-rank.ts +240 -0
- package/src/commands/claude.ts +148 -4
- package/src/commands/records.ts +270 -0
- package/src/records/migrate.ts +78 -0
- package/src/records/validate.ts +32 -5
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { SkillCase } from '@/claude/skills-rank'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `setup-*`, `migration-*`, `toolkit-*`, and `create-rule`: scaffolding,
|
|
5
|
+
* proposal-only migrations, and the toolkit's own reference and feedback
|
|
6
|
+
* surfaces.
|
|
7
|
+
*/
|
|
8
|
+
export const SETUP_CASES: readonly SkillCase[] = [
|
|
9
|
+
{
|
|
10
|
+
prompt:
|
|
11
|
+
'This project has no rules installed yet, get the right governance in place.',
|
|
12
|
+
expect: 'setup-gov',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
prompt:
|
|
16
|
+
"Get the index.md system bootstrapped across this project's folders.",
|
|
17
|
+
expect: 'setup-indexes',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
prompt:
|
|
21
|
+
'This is a brand-new project, get the toolkit bootstrapped in one shot.',
|
|
22
|
+
expect: 'setup-init',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
prompt: 'Get the usual Claude Code plugins provisioned on this machine.',
|
|
26
|
+
expect: 'setup-plugins',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
prompt:
|
|
30
|
+
"Run through the generated scaffold's scripts and confirm each one passes.",
|
|
31
|
+
expect: 'setup-verify',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
prompt:
|
|
35
|
+
'This CLAUDE.md file has grown huge, break it apart into the tiered context model.',
|
|
36
|
+
expect: 'migration-claude-md',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
prompt:
|
|
40
|
+
'Move the agent-flavored docs out of the docs folder and into context.',
|
|
41
|
+
expect: 'migration-context',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
prompt:
|
|
45
|
+
'The snippets folder needs to move under .claude to match the current layout.',
|
|
46
|
+
expect: 'migration-standards',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
prompt:
|
|
50
|
+
'This file was replaced by a folder, help me split its content into it.',
|
|
51
|
+
expect: 'migration-superseded',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
prompt:
|
|
55
|
+
"Before I run this sync, tell me exactly what it's going to overwrite.",
|
|
56
|
+
expect: 'toolkit-cli',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
prompt:
|
|
60
|
+
'Something about the toolkit itself is broken, write it up and send it back to the maintainers.',
|
|
61
|
+
expect: 'toolkit-feedback',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
prompt:
|
|
65
|
+
"I don't know which specific toolkit skill I need, just handle it for me.",
|
|
66
|
+
expect: 'toolkit-operator',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
prompt:
|
|
70
|
+
'Work through the open feedback issues on the toolkit repo one by one.',
|
|
71
|
+
expect: 'toolkit-triage',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
prompt:
|
|
75
|
+
"This project needs its own coding rule that the toolkit doesn't ship.",
|
|
76
|
+
expect: 'create-rule',
|
|
77
|
+
},
|
|
78
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { listSkills } from '@/claude/skills-list'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether a prompt reaches the right skill, measured by TF-IDF cosine
|
|
7
|
+
* similarity over the shipped catalog's own frontmatter descriptions. This is
|
|
8
|
+
* a necessary condition rather than a report of real routing behavior: it
|
|
9
|
+
* asks whether the descriptions are separable by the words they use, and
|
|
10
|
+
* Claude Code does not route this way.
|
|
11
|
+
*
|
|
12
|
+
* Ported from `.claude/groundwork/42-ai-blueprint/evidence/rank.ts`, which
|
|
13
|
+
* ran once against this catalog and named the collisions this measure now
|
|
14
|
+
* tracks on a cadence.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const SKILLS_DIR = join('claude', 'skills')
|
|
18
|
+
|
|
19
|
+
const STOP = new Set(
|
|
20
|
+
'a about after again all also and any are as at be before but by can do does for from has have help how in into is it its just make not of on or our so that the then there these this to use used uses using want was what when where which who why with you your run'.split(
|
|
21
|
+
' ',
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export interface RankedSkill {
|
|
26
|
+
readonly name: string
|
|
27
|
+
readonly description: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One prompt and the skill it should reach. */
|
|
31
|
+
export interface SkillCase {
|
|
32
|
+
readonly prompt: string
|
|
33
|
+
readonly expect: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A case whose prompt did not rank its expected skill first. */
|
|
37
|
+
export interface Miss {
|
|
38
|
+
readonly prompt: string
|
|
39
|
+
readonly expect: string
|
|
40
|
+
/** The skill the ranker placed first instead. */
|
|
41
|
+
readonly won: string
|
|
42
|
+
/** Where `expect` placed, or 0 when it never appears in the catalog. */
|
|
43
|
+
readonly rank: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Why a measure produced no reading, which is never the same as a clean one. */
|
|
47
|
+
export type RankRefusal = 'no-skills'
|
|
48
|
+
|
|
49
|
+
export type RankReport =
|
|
50
|
+
| {
|
|
51
|
+
readonly kind: 'measured'
|
|
52
|
+
readonly skills: number
|
|
53
|
+
readonly cases: number
|
|
54
|
+
readonly rank1: number
|
|
55
|
+
readonly top3: number
|
|
56
|
+
readonly misses: readonly Miss[]
|
|
57
|
+
/** Cases whose prompt carried no vocabulary to score. */
|
|
58
|
+
readonly unmeasurable: readonly SkillCase[]
|
|
59
|
+
}
|
|
60
|
+
| { readonly kind: 'refused'; readonly reason: RankRefusal }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every shipped skill's frontmatter description, read the way a prompt is
|
|
64
|
+
* matched against it: whole, including the quoted trigger phrases it states.
|
|
65
|
+
* A skill whose frontmatter carries no description contributes no vocabulary
|
|
66
|
+
* and never wins a rank, so it is dropped rather than scored on nothing.
|
|
67
|
+
*/
|
|
68
|
+
export function loadCatalog(root: string): RankedSkill[] {
|
|
69
|
+
return listSkills(root)
|
|
70
|
+
.filter((skill) => skill.description !== '')
|
|
71
|
+
.map((skill) => ({ name: skill.name, description: skill.description }))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tokenize(text: string): string[] {
|
|
75
|
+
return text
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
78
|
+
.split(/\s+/)
|
|
79
|
+
.filter((token) => token.length > 2 && !STOP.has(token))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function termCounts(tokens: readonly string[]): Map<string, number> {
|
|
83
|
+
const counts = new Map<string, number>()
|
|
84
|
+
for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1)
|
|
85
|
+
return counts
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildIdf(
|
|
89
|
+
docs: readonly Map<string, number>[],
|
|
90
|
+
): (term: string) => number {
|
|
91
|
+
const total = docs.length
|
|
92
|
+
const documentFrequency = new Map<string, number>()
|
|
93
|
+
for (const doc of docs) {
|
|
94
|
+
for (const term of doc.keys()) {
|
|
95
|
+
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return (term) =>
|
|
99
|
+
Math.log((total + 1) / ((documentFrequency.get(term) ?? 0) + 1)) + 1
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function tfIdfVector(
|
|
103
|
+
terms: Map<string, number>,
|
|
104
|
+
idf: (term: string) => number,
|
|
105
|
+
): Map<string, number> {
|
|
106
|
+
const vec = new Map<string, number>()
|
|
107
|
+
for (const [term, frequency] of terms) vec.set(term, frequency * idf(term))
|
|
108
|
+
return vec
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cosineSimilarity(
|
|
112
|
+
a: Map<string, number>,
|
|
113
|
+
b: Map<string, number>,
|
|
114
|
+
): number {
|
|
115
|
+
let dot = 0
|
|
116
|
+
for (const [term, weight] of a) dot += weight * (b.get(term) ?? 0)
|
|
117
|
+
if (dot === 0) return 0
|
|
118
|
+
|
|
119
|
+
const norm = (vec: Map<string, number>) =>
|
|
120
|
+
Math.sqrt(
|
|
121
|
+
[...vec.values()].reduce((sum, weight) => sum + weight * weight, 0),
|
|
122
|
+
)
|
|
123
|
+
const denominator = norm(a) * norm(b)
|
|
124
|
+
return denominator === 0 ? 0 : dot / denominator
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface RankedResult {
|
|
128
|
+
readonly name: string
|
|
129
|
+
readonly score: number
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Every skill's TF-IDF vector over the catalog it was loaded with, and the
|
|
134
|
+
* IDF weighting that both the catalog and a scored prompt read from. A model
|
|
135
|
+
* is built once per catalog and reused across every case, since the IDF
|
|
136
|
+
* weights are the same question asked of the same corpus each time.
|
|
137
|
+
*/
|
|
138
|
+
export interface RankModel {
|
|
139
|
+
readonly rank: (prompt: string) => readonly RankedResult[]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildModel(catalog: readonly RankedSkill[]): RankModel {
|
|
143
|
+
const docs = catalog.map((skill) => termCounts(tokenize(skill.description)))
|
|
144
|
+
const idf = buildIdf(docs)
|
|
145
|
+
const vectors = new Map(
|
|
146
|
+
catalog.map((skill, index) => [skill.name, tfIdfVector(docs[index], idf)]),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
rank: (prompt: string): readonly RankedResult[] => {
|
|
151
|
+
const promptVector = tfIdfVector(termCounts(tokenize(prompt)), idf)
|
|
152
|
+
// A prompt built entirely from stopwords and short words tokenizes to
|
|
153
|
+
// nothing, so every skill would score 0 and the sort would fall through
|
|
154
|
+
// to `localeCompare`, handing the alphabetically first skill a win no
|
|
155
|
+
// description earned. Reporting no ranking at all is what keeps that
|
|
156
|
+
// tie-break from reading as a measurement.
|
|
157
|
+
if (promptVector.size === 0) return []
|
|
158
|
+
|
|
159
|
+
return [...vectors]
|
|
160
|
+
.map(([name, vector]) => ({
|
|
161
|
+
name,
|
|
162
|
+
score: cosineSimilarity(promptVector, vector),
|
|
163
|
+
}))
|
|
164
|
+
.sort(
|
|
165
|
+
(left, right) =>
|
|
166
|
+
right.score - left.score || left.name.localeCompare(right.name),
|
|
167
|
+
)
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Scores every case against the catalog's own descriptions, which is the
|
|
174
|
+
* production mechanism: Claude Code matches a prompt against the whole
|
|
175
|
+
* description a skill ships, triggers included.
|
|
176
|
+
*/
|
|
177
|
+
export function measureCases(
|
|
178
|
+
catalog: readonly RankedSkill[],
|
|
179
|
+
cases: readonly SkillCase[],
|
|
180
|
+
): {
|
|
181
|
+
readonly rank1: number
|
|
182
|
+
readonly top3: number
|
|
183
|
+
readonly misses: readonly Miss[]
|
|
184
|
+
readonly unmeasurable: readonly SkillCase[]
|
|
185
|
+
} {
|
|
186
|
+
const model = buildModel(catalog)
|
|
187
|
+
let rank1 = 0
|
|
188
|
+
let top3 = 0
|
|
189
|
+
const misses: Miss[] = []
|
|
190
|
+
const unmeasurable: SkillCase[] = []
|
|
191
|
+
|
|
192
|
+
for (const skillCase of cases) {
|
|
193
|
+
const ranked = model.rank(skillCase.prompt)
|
|
194
|
+
if (ranked.length === 0) {
|
|
195
|
+
unmeasurable.push(skillCase)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const at = ranked.findIndex((entry) => entry.name === skillCase.expect) + 1
|
|
200
|
+
|
|
201
|
+
if (at === 1) rank1 += 1
|
|
202
|
+
if (at > 0 && at <= 3) top3 += 1
|
|
203
|
+
if (at !== 1) {
|
|
204
|
+
misses.push({
|
|
205
|
+
prompt: skillCase.prompt,
|
|
206
|
+
expect: skillCase.expect,
|
|
207
|
+
won: ranked[0]?.name ?? '',
|
|
208
|
+
rank: at,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { rank1, top3, misses, unmeasurable }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Reads the shipped catalog off disk and scores it against the given case
|
|
218
|
+
* corpus. Measures the cwd's catalog rather than the toolkit root, matching
|
|
219
|
+
* the reach and audit verbs, so a linked worktree reads its own branch.
|
|
220
|
+
*/
|
|
221
|
+
export function scanRank(
|
|
222
|
+
root: string,
|
|
223
|
+
cases: readonly SkillCase[],
|
|
224
|
+
): RankReport {
|
|
225
|
+
const skillsRoot = join(root, SKILLS_DIR)
|
|
226
|
+
if (!existsSync(skillsRoot)) return { kind: 'refused', reason: 'no-skills' }
|
|
227
|
+
|
|
228
|
+
const catalog = loadCatalog(root)
|
|
229
|
+
const { rank1, top3, misses, unmeasurable } = measureCases(catalog, cases)
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
kind: 'measured',
|
|
233
|
+
skills: catalog.length,
|
|
234
|
+
cases: cases.length,
|
|
235
|
+
rank1,
|
|
236
|
+
top3,
|
|
237
|
+
misses,
|
|
238
|
+
unmeasurable,
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -34,6 +34,12 @@ import {
|
|
|
34
34
|
type ReachReport,
|
|
35
35
|
scanReach,
|
|
36
36
|
} from '@/claude/skills-reach'
|
|
37
|
+
import { SKILL_CASES } from '@/claude/cases/all'
|
|
38
|
+
import {
|
|
39
|
+
type RankRefusal,
|
|
40
|
+
type RankReport,
|
|
41
|
+
scanRank,
|
|
42
|
+
} from '@/claude/skills-rank'
|
|
37
43
|
import {
|
|
38
44
|
planSettings,
|
|
39
45
|
readSettings,
|
|
@@ -85,6 +91,10 @@ interface SkillsReachOptions {
|
|
|
85
91
|
readonly json?: boolean
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
interface SkillsRankOptions {
|
|
95
|
+
readonly json?: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
interface RoutingOptions {
|
|
89
99
|
readonly json?: boolean
|
|
90
100
|
}
|
|
@@ -215,15 +225,18 @@ export function register(program: Command): void {
|
|
|
215
225
|
|
|
216
226
|
const skills = claude
|
|
217
227
|
.command('skills')
|
|
218
|
-
.description('Plugin skill catalog (list, audit, drift, reach)')
|
|
219
|
-
.argument(
|
|
228
|
+
.description('Plugin skill catalog (list, audit, drift, reach, rank)')
|
|
229
|
+
.argument(
|
|
230
|
+
'[subcommand]',
|
|
231
|
+
"One of 'list', 'audit', 'drift', 'reach', or 'rank'",
|
|
232
|
+
)
|
|
220
233
|
.helpOption('-h, --help', 'Show this help message')
|
|
221
234
|
.action((subcommand: string | undefined) => {
|
|
222
235
|
intro('aitk claude')
|
|
223
236
|
logError(
|
|
224
237
|
subcommand === undefined
|
|
225
|
-
? "Missing subcommand. Use 'list', 'audit', 'drift', or '
|
|
226
|
-
: `Unknown subcommand: ${subcommand}. Use 'list', 'audit', 'drift', or '
|
|
238
|
+
? "Missing subcommand. Use 'list', 'audit', 'drift', 'reach', or 'rank'."
|
|
239
|
+
: `Unknown subcommand: ${subcommand}. Use 'list', 'audit', 'drift', 'reach', or 'rank'.`,
|
|
227
240
|
)
|
|
228
241
|
outro()
|
|
229
242
|
process.exitCode = 1
|
|
@@ -351,6 +364,42 @@ export function register(program: Command): void {
|
|
|
351
364
|
.action((path: string | undefined, opts: SkillsReachOptions) => {
|
|
352
365
|
process.exitCode = runSkillsReach(path, opts)
|
|
353
366
|
})
|
|
367
|
+
|
|
368
|
+
skills
|
|
369
|
+
.command('rank')
|
|
370
|
+
.description('Score the shipped catalog against the routing case corpus')
|
|
371
|
+
.argument('[path]', 'Repository root, defaulting to the current directory')
|
|
372
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
373
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
374
|
+
.addHelpText(
|
|
375
|
+
'after',
|
|
376
|
+
[
|
|
377
|
+
'',
|
|
378
|
+
'Scope:',
|
|
379
|
+
' TF-IDF cosine similarity over every claude/skills/*/SKILL.md',
|
|
380
|
+
' frontmatter description, scored against the hand-authored corpus',
|
|
381
|
+
' at src/claude/cases/. A necessary condition rather than a report of',
|
|
382
|
+
' real routing behavior: it asks whether the descriptions are',
|
|
383
|
+
' separable by the words they use, and Claude Code does not route',
|
|
384
|
+
' this way.',
|
|
385
|
+
'',
|
|
386
|
+
'Exit codes:',
|
|
387
|
+
' 0 the catalog was read, whether or not a case missed rank one',
|
|
388
|
+
' 1 refused, with the reason on stderr',
|
|
389
|
+
'',
|
|
390
|
+
'Reports rather than gates. The corpus is a first run with no',
|
|
391
|
+
'baseline to fail a push against, so `aitk audits run` registers',
|
|
392
|
+
'this with no gating exit and joins the ratchet instead.',
|
|
393
|
+
'',
|
|
394
|
+
'Examples:',
|
|
395
|
+
' aitk claude skills rank',
|
|
396
|
+
' aitk claude skills rank --json',
|
|
397
|
+
'',
|
|
398
|
+
].join('\n'),
|
|
399
|
+
)
|
|
400
|
+
.action((path: string | undefined, opts: SkillsRankOptions) => {
|
|
401
|
+
process.exitCode = runSkillsRank(path, opts)
|
|
402
|
+
})
|
|
354
403
|
}
|
|
355
404
|
|
|
356
405
|
function succeed(message: string): number {
|
|
@@ -816,6 +865,101 @@ function reportReach(report: Extract<ReachReport, { kind: 'measured' }>): void {
|
|
|
816
865
|
)
|
|
817
866
|
}
|
|
818
867
|
|
|
868
|
+
/** What a reader does about the one way the measure fails to build. */
|
|
869
|
+
const RANK_REFUSALS: Record<RankRefusal, string> = {
|
|
870
|
+
'no-skills':
|
|
871
|
+
'No claude/skills/ here, so this tree ships no plugin body to measure.',
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Measures the cwd rather than the toolkit root, matching the reach and audit
|
|
876
|
+
* verbs, so a linked worktree reads its own branch instead of `main`. The
|
|
877
|
+
* case corpus is the toolkit's own, since a target project ships no cases of
|
|
878
|
+
* its own for a catalog it did not author.
|
|
879
|
+
*/
|
|
880
|
+
function runSkillsRank(
|
|
881
|
+
path: string | undefined,
|
|
882
|
+
opts: SkillsRankOptions,
|
|
883
|
+
): number {
|
|
884
|
+
const root = resolve(path ?? process.cwd())
|
|
885
|
+
const report = scanRank(root, SKILL_CASES)
|
|
886
|
+
|
|
887
|
+
if (report.kind === 'refused') {
|
|
888
|
+
frameError(RANK_REFUSALS[report.reason])
|
|
889
|
+
if (opts.json) {
|
|
890
|
+
process.stdout.write(
|
|
891
|
+
`${JSON.stringify({
|
|
892
|
+
root,
|
|
893
|
+
reason: report.reason,
|
|
894
|
+
message: RANK_REFUSALS[report.reason],
|
|
895
|
+
})}\n`,
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
return 1
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
intro('aitk claude skills rank')
|
|
902
|
+
reportRank(report)
|
|
903
|
+
outro()
|
|
904
|
+
|
|
905
|
+
if (opts.json) {
|
|
906
|
+
process.stdout.write(
|
|
907
|
+
`${JSON.stringify({
|
|
908
|
+
root,
|
|
909
|
+
skills: report.skills,
|
|
910
|
+
cases: report.cases,
|
|
911
|
+
rank1: report.rank1,
|
|
912
|
+
top3: report.top3,
|
|
913
|
+
misses: report.misses,
|
|
914
|
+
unmeasurable: report.unmeasurable,
|
|
915
|
+
})}\n`,
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return 0
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* States the corpus and both counts on every run, including a clean one. A
|
|
924
|
+
* miss list alone reads as a verdict on the catalog unless the run also says
|
|
925
|
+
* how many skills and cases it measured against.
|
|
926
|
+
*/
|
|
927
|
+
function reportRank(report: Extract<RankReport, { kind: 'measured' }>): void {
|
|
928
|
+
logStep('Corpus')
|
|
929
|
+
logInfo(
|
|
930
|
+
`${plural(report.skills, 'skill')} scored against ${plural(report.cases, 'case')}`,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
logStep('Score')
|
|
934
|
+
logInfo(
|
|
935
|
+
`rank one: ${report.rank1}/${report.cases}, top three: ${report.top3}/${report.cases}`,
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
if (report.unmeasurable.length > 0) {
|
|
939
|
+
logWarn(plural(report.unmeasurable.length, 'unmeasurable case'))
|
|
940
|
+
pipeOutput(
|
|
941
|
+
report.unmeasurable
|
|
942
|
+
.map((skillCase) => `${skillCase.expect} ${skillCase.prompt}`)
|
|
943
|
+
.join('\n'),
|
|
944
|
+
)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (report.misses.length === 0) {
|
|
948
|
+
logInfo('Every measurable case ranked its expected skill first.')
|
|
949
|
+
return
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
logWarn(plural(report.misses.length, 'collision'))
|
|
953
|
+
pipeOutput(
|
|
954
|
+
report.misses
|
|
955
|
+
.map(
|
|
956
|
+
(miss) =>
|
|
957
|
+
`${miss.expect} lost to ${miss.won} (rank ${miss.rank}) ${miss.prompt}`,
|
|
958
|
+
)
|
|
959
|
+
.join('\n'),
|
|
960
|
+
)
|
|
961
|
+
}
|
|
962
|
+
|
|
819
963
|
/**
|
|
820
964
|
* States the bound on every run, including the run that names nothing. A report
|
|
821
965
|
* listing only what moved reads as a verdict on what a session holds, and the
|