@dzhechkov/harness-core 0.3.3 → 0.3.4

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.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Skill benchmarking — Layer 0 deterministic checks + scoring.
3
+ *
4
+ * Runs structural validation checks against a skill directory,
5
+ * producing a scored report. Checks are based on BTO Layer 0
6
+ * (universal U1-U5 + skill-specific S1-S10).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ /** A single benchmark check result. */
11
+ export interface BenchmarkCheck {
12
+ readonly id: string;
13
+ readonly name: string;
14
+ readonly passed: boolean;
15
+ readonly detail?: string | undefined;
16
+ }
17
+ /** Score for a single skill benchmark. */
18
+ export interface BenchmarkScore {
19
+ readonly skillId: string;
20
+ readonly skillDir: string;
21
+ readonly checks: readonly BenchmarkCheck[];
22
+ readonly passed: number;
23
+ readonly total: number;
24
+ readonly passRate: number;
25
+ readonly grade: 'A' | 'B' | 'C' | 'D' | 'F';
26
+ }
27
+ /** Result of benchmarking one or more skills. */
28
+ export interface BenchmarkReport {
29
+ readonly skills: readonly BenchmarkScore[];
30
+ readonly totalPassed: number;
31
+ readonly totalChecks: number;
32
+ readonly overallPassRate: number;
33
+ }
34
+ /** Result of A/B comparison between two skills. */
35
+ export interface CompareResult {
36
+ readonly skillA: BenchmarkScore;
37
+ readonly skillB: BenchmarkScore;
38
+ readonly winner: string;
39
+ readonly deltaChecks: readonly {
40
+ readonly id: string;
41
+ readonly aPass: boolean;
42
+ readonly bPass: boolean;
43
+ }[];
44
+ }
45
+ /** Run Layer 0 benchmark checks on a single skill directory. */
46
+ export declare function benchmarkSkill(skillDir: string, skillId: string): BenchmarkScore;
47
+ /** Benchmark multiple skills. */
48
+ export declare function benchmarkSkills(skillDirs: {
49
+ id: string;
50
+ dir: string;
51
+ }[]): BenchmarkReport;
52
+ /** Compare two skills head-to-head. */
53
+ export declare function compareSkills(skillADir: string, skillAId: string, skillBDir: string, skillBId: string): CompareResult;
54
+ //# sourceMappingURL=benchmark.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"benchmark.d.ts","sourceRoot":"","sources":["../src/benchmark.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;CAC7C;AAED,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,SAAS;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CAC5G;AAUD,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,CAsGhF;AAED,iCAAiC;AACjC,wBAAgB,eAAe,CAAC,SAAS,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,eAAe,CAUzF;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EACnC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAClC,aAAa,CAiBf"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Skill benchmarking — Layer 0 deterministic checks + scoring.
3
+ *
4
+ * Runs structural validation checks against a skill directory,
5
+ * producing a scored report. Checks are based on BTO Layer 0
6
+ * (universal U1-U5 + skill-specific S1-S10).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import { existsSync, readFileSync, statSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ function gradeFromRate(rate) {
13
+ if (rate >= 90)
14
+ return 'A';
15
+ if (rate >= 80)
16
+ return 'B';
17
+ if (rate >= 70)
18
+ return 'C';
19
+ if (rate >= 60)
20
+ return 'D';
21
+ return 'F';
22
+ }
23
+ /** Run Layer 0 benchmark checks on a single skill directory. */
24
+ export function benchmarkSkill(skillDir, skillId) {
25
+ const checks = [];
26
+ const skillMdPath = join(skillDir, 'SKILL.md');
27
+ // U1: SKILL.md exists
28
+ const skillMdExists = existsSync(skillMdPath);
29
+ checks.push({ id: 'U1', name: 'SKILL.md exists', passed: skillMdExists });
30
+ if (!skillMdExists) {
31
+ return {
32
+ skillId, skillDir, checks, passed: 0, total: checks.length,
33
+ passRate: 0, grade: 'F',
34
+ };
35
+ }
36
+ const content = readFileSync(skillMdPath, 'utf-8');
37
+ const stat = statSync(skillMdPath);
38
+ // U2: Valid UTF-8 (if we can read it, it's valid)
39
+ checks.push({ id: 'U2', name: 'valid UTF-8', passed: true });
40
+ // U3: Has headings
41
+ const hasHeadings = /^#{1,6}\s/m.test(content);
42
+ checks.push({ id: 'U3', name: 'has headings', passed: hasHeadings });
43
+ // U4: No excessive blanks (>3 consecutive blank lines)
44
+ const excessiveBlanks = /\n{5,}/.test(content);
45
+ checks.push({ id: 'U4', name: 'no excessive blanks', passed: !excessiveBlanks });
46
+ // U5: Size bounds (100B - 50KB)
47
+ const sizeOk = stat.size >= 100 && stat.size <= 50000;
48
+ checks.push({
49
+ id: 'U5', name: 'size bounds (100B-50KB)', passed: sizeOk,
50
+ detail: `${stat.size} bytes`,
51
+ });
52
+ // S1: Frontmatter present
53
+ const hasFrontmatter = content.startsWith('---');
54
+ checks.push({ id: 'S1', name: 'frontmatter present', passed: hasFrontmatter });
55
+ // S2: name field
56
+ const hasName = /^name:\s/m.test(content);
57
+ checks.push({ id: 'S2', name: 'name field', passed: hasName });
58
+ // S3: description field
59
+ const hasDescription = /^description:\s/m.test(content);
60
+ checks.push({ id: 'S3', name: 'description field', passed: hasDescription });
61
+ // S4: trust_tier field
62
+ const hasTrustTier = /^trust_tier:\s/m.test(content);
63
+ checks.push({ id: 'S4', name: 'trust_tier field', passed: hasTrustTier });
64
+ // S5: Protocol/procedure section
65
+ const hasProtocol = /^##\s+(Protocol|Procedure|Steps|When to use)/mi.test(content);
66
+ checks.push({ id: 'S5', name: 'protocol/procedure section', passed: hasProtocol });
67
+ // S6: Output section or schema reference
68
+ const hasOutput = /^##\s+Output/mi.test(content) || /schema_path/m.test(content);
69
+ checks.push({ id: 'S6', name: 'output section or schema ref', passed: hasOutput });
70
+ // S7: schemas/output.json exists
71
+ const hasSchema = existsSync(join(skillDir, 'schemas', 'output.json'));
72
+ checks.push({ id: 'S7', name: 'schemas/output.json exists', passed: hasSchema });
73
+ // S8: scripts/validate-config.json exists
74
+ const hasValidator = existsSync(join(skillDir, 'scripts', 'validate-config.json'));
75
+ checks.push({ id: 'S8', name: 'scripts/validate-config.json exists', passed: hasValidator });
76
+ // S9: When to use section
77
+ const hasWhenToUse = /^##\s+(When to use|When To Activate|Use this skill when)/mi.test(content);
78
+ checks.push({ id: 'S9', name: 'when-to-use section', passed: hasWhenToUse });
79
+ // S10: No TODO/PLACEHOLDER markers
80
+ const hasTodos = /\[TODO\]|\[PLACEHOLDER\]|\[TBD\]/i.test(content);
81
+ checks.push({ id: 'S10', name: 'no TODO/PLACEHOLDER markers', passed: !hasTodos });
82
+ // S11: Evals exist
83
+ const hasEvals = existsSync(join(skillDir, 'evals'));
84
+ checks.push({ id: 'S11', name: 'evals directory exists', passed: hasEvals });
85
+ // S12: Content depth (>20 lines)
86
+ const lineCount = content.split('\n').length;
87
+ checks.push({
88
+ id: 'S12', name: 'content depth (>20 lines)', passed: lineCount > 20,
89
+ detail: `${lineCount} lines`,
90
+ });
91
+ // S13: Examples section
92
+ const hasExamples = /^##\s+(Examples|Example|Quick Start)/mi.test(content);
93
+ checks.push({ id: 'S13', name: 'examples section', passed: hasExamples });
94
+ // S14: Anti-patterns or self-check section
95
+ const hasAntiPatterns = /^##\s+(Anti.?[Pp]atterns|Self.?check|Self-check)/mi.test(content);
96
+ checks.push({ id: 'S14', name: 'anti-patterns or self-check', passed: hasAntiPatterns });
97
+ const passed = checks.filter((c) => c.passed).length;
98
+ const passRate = Math.round((passed / checks.length) * 100);
99
+ return {
100
+ skillId, skillDir, checks, passed, total: checks.length,
101
+ passRate, grade: gradeFromRate(passRate),
102
+ };
103
+ }
104
+ /** Benchmark multiple skills. */
105
+ export function benchmarkSkills(skillDirs) {
106
+ const skills = skillDirs.map((s) => benchmarkSkill(s.dir, s.id));
107
+ const totalPassed = skills.reduce((sum, s) => sum + s.passed, 0);
108
+ const totalChecks = skills.reduce((sum, s) => sum + s.total, 0);
109
+ return {
110
+ skills,
111
+ totalPassed,
112
+ totalChecks,
113
+ overallPassRate: totalChecks > 0 ? Math.round((totalPassed / totalChecks) * 100) : 0,
114
+ };
115
+ }
116
+ /** Compare two skills head-to-head. */
117
+ export function compareSkills(skillADir, skillAId, skillBDir, skillBId) {
118
+ const skillA = benchmarkSkill(skillADir, skillAId);
119
+ const skillB = benchmarkSkill(skillBDir, skillBId);
120
+ const deltaChecks = [];
121
+ for (const checkA of skillA.checks) {
122
+ const checkB = skillB.checks.find((c) => c.id === checkA.id);
123
+ if (checkB && checkA.passed !== checkB.passed) {
124
+ deltaChecks.push({ id: checkA.id, aPass: checkA.passed, bPass: checkB.passed });
125
+ }
126
+ }
127
+ const winner = skillA.passRate > skillB.passRate ? skillAId
128
+ : skillB.passRate > skillA.passRate ? skillBId
129
+ : 'tie';
130
+ return { skillA, skillB, winner, deltaChecks };
131
+ }
132
+ //# sourceMappingURL=benchmark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"benchmark.js","sourceRoot":"","sources":["../src/benchmark.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -14,4 +14,6 @@ export { createSkill } from './create-skill.js';
14
14
  export type { CreateSkillOptions, CreateSkillResult } from './create-skill.js';
15
15
  export { checkUpstream, loadSourcesManifest } from './sync-upstream.js';
16
16
  export type { SyncUpstreamReport, UpstreamCheckResult, SourcesManifest } from './sync-upstream.js';
17
+ export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
18
+ export type { BenchmarkCheck, BenchmarkScore, BenchmarkReport, CompareResult } from './benchmark.js';
17
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAE5C,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACxE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAE5C,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACxE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnG,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAChF,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -12,4 +12,5 @@ export * from './operations.js';
12
12
  export * from './workflows.js';
13
13
  export { createSkill } from './create-skill.js';
14
14
  export { checkUpstream, loadSourcesManifest } from './sync-upstream.js';
15
+ export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
15
16
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAE5C,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAE5C,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAExE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-core",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Shared harness logic - skill loading, additive apply, and the init/sync/verify/doctor operations.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,12 +25,12 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "yaml": "^2.0.0",
28
+ "@dzhechkov/adapter-hermes": "0.2.0",
29
+ "@dzhechkov/adapter-opencode": "0.2.0",
28
30
  "@dzhechkov/adapter-claude": "0.2.0",
29
- "@dzhechkov/adapter-openclaude": "0.1.0",
31
+ "@dzhechkov/core": "0.2.0",
30
32
  "@dzhechkov/adapter-codex": "0.2.0",
31
- "@dzhechkov/adapter-opencode": "0.2.0",
32
- "@dzhechkov/adapter-hermes": "0.2.0",
33
- "@dzhechkov/core": "0.2.0"
33
+ "@dzhechkov/adapter-openclaude": "0.1.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^25.6.0",
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Skill benchmarking — Layer 0 deterministic checks + scoring.
3
+ *
4
+ * Runs structural validation checks against a skill directory,
5
+ * producing a scored report. Checks are based on BTO Layer 0
6
+ * (universal U1-U5 + skill-specific S1-S10).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import { existsSync, readFileSync, statSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ /** A single benchmark check result. */
15
+ export interface BenchmarkCheck {
16
+ readonly id: string;
17
+ readonly name: string;
18
+ readonly passed: boolean;
19
+ readonly detail?: string | undefined;
20
+ }
21
+
22
+ /** Score for a single skill benchmark. */
23
+ export interface BenchmarkScore {
24
+ readonly skillId: string;
25
+ readonly skillDir: string;
26
+ readonly checks: readonly BenchmarkCheck[];
27
+ readonly passed: number;
28
+ readonly total: number;
29
+ readonly passRate: number;
30
+ readonly grade: 'A' | 'B' | 'C' | 'D' | 'F';
31
+ }
32
+
33
+ /** Result of benchmarking one or more skills. */
34
+ export interface BenchmarkReport {
35
+ readonly skills: readonly BenchmarkScore[];
36
+ readonly totalPassed: number;
37
+ readonly totalChecks: number;
38
+ readonly overallPassRate: number;
39
+ }
40
+
41
+ /** Result of A/B comparison between two skills. */
42
+ export interface CompareResult {
43
+ readonly skillA: BenchmarkScore;
44
+ readonly skillB: BenchmarkScore;
45
+ readonly winner: string;
46
+ readonly deltaChecks: readonly { readonly id: string; readonly aPass: boolean; readonly bPass: boolean }[];
47
+ }
48
+
49
+ function gradeFromRate(rate: number): 'A' | 'B' | 'C' | 'D' | 'F' {
50
+ if (rate >= 90) return 'A';
51
+ if (rate >= 80) return 'B';
52
+ if (rate >= 70) return 'C';
53
+ if (rate >= 60) return 'D';
54
+ return 'F';
55
+ }
56
+
57
+ /** Run Layer 0 benchmark checks on a single skill directory. */
58
+ export function benchmarkSkill(skillDir: string, skillId: string): BenchmarkScore {
59
+ const checks: BenchmarkCheck[] = [];
60
+ const skillMdPath = join(skillDir, 'SKILL.md');
61
+
62
+ // U1: SKILL.md exists
63
+ const skillMdExists = existsSync(skillMdPath);
64
+ checks.push({ id: 'U1', name: 'SKILL.md exists', passed: skillMdExists });
65
+
66
+ if (!skillMdExists) {
67
+ return {
68
+ skillId, skillDir, checks, passed: 0, total: checks.length,
69
+ passRate: 0, grade: 'F',
70
+ };
71
+ }
72
+
73
+ const content = readFileSync(skillMdPath, 'utf-8');
74
+ const stat = statSync(skillMdPath);
75
+
76
+ // U2: Valid UTF-8 (if we can read it, it's valid)
77
+ checks.push({ id: 'U2', name: 'valid UTF-8', passed: true });
78
+
79
+ // U3: Has headings
80
+ const hasHeadings = /^#{1,6}\s/m.test(content);
81
+ checks.push({ id: 'U3', name: 'has headings', passed: hasHeadings });
82
+
83
+ // U4: No excessive blanks (>3 consecutive blank lines)
84
+ const excessiveBlanks = /\n{5,}/.test(content);
85
+ checks.push({ id: 'U4', name: 'no excessive blanks', passed: !excessiveBlanks });
86
+
87
+ // U5: Size bounds (100B - 50KB)
88
+ const sizeOk = stat.size >= 100 && stat.size <= 50000;
89
+ checks.push({
90
+ id: 'U5', name: 'size bounds (100B-50KB)', passed: sizeOk,
91
+ detail: `${stat.size} bytes`,
92
+ });
93
+
94
+ // S1: Frontmatter present
95
+ const hasFrontmatter = content.startsWith('---');
96
+ checks.push({ id: 'S1', name: 'frontmatter present', passed: hasFrontmatter });
97
+
98
+ // S2: name field
99
+ const hasName = /^name:\s/m.test(content);
100
+ checks.push({ id: 'S2', name: 'name field', passed: hasName });
101
+
102
+ // S3: description field
103
+ const hasDescription = /^description:\s/m.test(content);
104
+ checks.push({ id: 'S3', name: 'description field', passed: hasDescription });
105
+
106
+ // S4: trust_tier field
107
+ const hasTrustTier = /^trust_tier:\s/m.test(content);
108
+ checks.push({ id: 'S4', name: 'trust_tier field', passed: hasTrustTier });
109
+
110
+ // S5: Protocol/procedure section
111
+ const hasProtocol = /^##\s+(Protocol|Procedure|Steps|When to use)/mi.test(content);
112
+ checks.push({ id: 'S5', name: 'protocol/procedure section', passed: hasProtocol });
113
+
114
+ // S6: Output section or schema reference
115
+ const hasOutput = /^##\s+Output/mi.test(content) || /schema_path/m.test(content);
116
+ checks.push({ id: 'S6', name: 'output section or schema ref', passed: hasOutput });
117
+
118
+ // S7: schemas/output.json exists
119
+ const hasSchema = existsSync(join(skillDir, 'schemas', 'output.json'));
120
+ checks.push({ id: 'S7', name: 'schemas/output.json exists', passed: hasSchema });
121
+
122
+ // S8: scripts/validate-config.json exists
123
+ const hasValidator = existsSync(join(skillDir, 'scripts', 'validate-config.json'));
124
+ checks.push({ id: 'S8', name: 'scripts/validate-config.json exists', passed: hasValidator });
125
+
126
+ // S9: When to use section
127
+ const hasWhenToUse = /^##\s+(When to use|When To Activate|Use this skill when)/mi.test(content);
128
+ checks.push({ id: 'S9', name: 'when-to-use section', passed: hasWhenToUse });
129
+
130
+ // S10: No TODO/PLACEHOLDER markers
131
+ const hasTodos = /\[TODO\]|\[PLACEHOLDER\]|\[TBD\]/i.test(content);
132
+ checks.push({ id: 'S10', name: 'no TODO/PLACEHOLDER markers', passed: !hasTodos });
133
+
134
+ // S11: Evals exist
135
+ const hasEvals = existsSync(join(skillDir, 'evals'));
136
+ checks.push({ id: 'S11', name: 'evals directory exists', passed: hasEvals });
137
+
138
+ // S12: Content depth (>20 lines)
139
+ const lineCount = content.split('\n').length;
140
+ checks.push({
141
+ id: 'S12', name: 'content depth (>20 lines)', passed: lineCount > 20,
142
+ detail: `${lineCount} lines`,
143
+ });
144
+
145
+ // S13: Examples section
146
+ const hasExamples = /^##\s+(Examples|Example|Quick Start)/mi.test(content);
147
+ checks.push({ id: 'S13', name: 'examples section', passed: hasExamples });
148
+
149
+ // S14: Anti-patterns or self-check section
150
+ const hasAntiPatterns = /^##\s+(Anti.?[Pp]atterns|Self.?check|Self-check)/mi.test(content);
151
+ checks.push({ id: 'S14', name: 'anti-patterns or self-check', passed: hasAntiPatterns });
152
+
153
+ const passed = checks.filter((c) => c.passed).length;
154
+ const passRate = Math.round((passed / checks.length) * 100);
155
+
156
+ return {
157
+ skillId, skillDir, checks, passed, total: checks.length,
158
+ passRate, grade: gradeFromRate(passRate),
159
+ };
160
+ }
161
+
162
+ /** Benchmark multiple skills. */
163
+ export function benchmarkSkills(skillDirs: { id: string; dir: string }[]): BenchmarkReport {
164
+ const skills = skillDirs.map((s) => benchmarkSkill(s.dir, s.id));
165
+ const totalPassed = skills.reduce((sum, s) => sum + s.passed, 0);
166
+ const totalChecks = skills.reduce((sum, s) => sum + s.total, 0);
167
+ return {
168
+ skills,
169
+ totalPassed,
170
+ totalChecks,
171
+ overallPassRate: totalChecks > 0 ? Math.round((totalPassed / totalChecks) * 100) : 0,
172
+ };
173
+ }
174
+
175
+ /** Compare two skills head-to-head. */
176
+ export function compareSkills(
177
+ skillADir: string, skillAId: string,
178
+ skillBDir: string, skillBId: string,
179
+ ): CompareResult {
180
+ const skillA = benchmarkSkill(skillADir, skillAId);
181
+ const skillB = benchmarkSkill(skillBDir, skillBId);
182
+
183
+ const deltaChecks: { id: string; aPass: boolean; bPass: boolean }[] = [];
184
+ for (const checkA of skillA.checks) {
185
+ const checkB = skillB.checks.find((c) => c.id === checkA.id);
186
+ if (checkB && checkA.passed !== checkB.passed) {
187
+ deltaChecks.push({ id: checkA.id, aPass: checkA.passed, bPass: checkB.passed });
188
+ }
189
+ }
190
+
191
+ const winner = skillA.passRate > skillB.passRate ? skillAId
192
+ : skillB.passRate > skillA.passRate ? skillBId
193
+ : 'tie';
194
+
195
+ return { skillA, skillB, winner, deltaChecks };
196
+ }
package/src/index.ts CHANGED
@@ -16,3 +16,5 @@ export { createSkill } from './create-skill.js';
16
16
  export type { CreateSkillOptions, CreateSkillResult } from './create-skill.js';
17
17
  export { checkUpstream, loadSourcesManifest } from './sync-upstream.js';
18
18
  export type { SyncUpstreamReport, UpstreamCheckResult, SourcesManifest } from './sync-upstream.js';
19
+ export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
20
+ export type { BenchmarkCheck, BenchmarkScore, BenchmarkReport, CompareResult } from './benchmark.js';