@dzhechkov/harness-core 0.3.2 → 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
@@ -12,4 +12,8 @@ export * from './operations.js';
12
12
  export * from './workflows.js';
13
13
  export { createSkill } from './create-skill.js';
14
14
  export type { CreateSkillOptions, CreateSkillResult } from './create-skill.js';
15
+ export { checkUpstream, loadSourcesManifest } from './sync-upstream.js';
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';
15
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"}
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
@@ -11,4 +11,6 @@ export * from './targets.js';
11
11
  export * from './operations.js';
12
12
  export * from './workflows.js';
13
13
  export { createSkill } from './create-skill.js';
14
+ export { checkUpstream, loadSourcesManifest } from './sync-upstream.js';
15
+ export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
14
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"}
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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Upstream sync — fetches SKILL.md from origin repos defined in sources.json
3
+ * and reports which skills have upstream changes.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ /** A single skill's source mapping. */
8
+ export interface SkillSource {
9
+ readonly path: string;
10
+ readonly version?: string | undefined;
11
+ readonly trust?: string | undefined;
12
+ readonly note?: string | undefined;
13
+ }
14
+ /** The origin repo metadata. */
15
+ export interface OriginMeta {
16
+ readonly repo: string;
17
+ readonly url: string;
18
+ readonly branch: string;
19
+ readonly license: string;
20
+ readonly author: string;
21
+ readonly canonicalized: string;
22
+ }
23
+ /** The sources.json schema. */
24
+ export interface SourcesManifest {
25
+ readonly origin: OriginMeta;
26
+ readonly skills: Record<string, SkillSource>;
27
+ }
28
+ /** Result of checking one skill against upstream. */
29
+ export interface UpstreamCheckResult {
30
+ readonly skillId: string;
31
+ readonly originPath: string;
32
+ readonly status: 'up-to-date' | 'changed' | 'fetch-error' | 'local-missing';
33
+ readonly localLines?: number | undefined;
34
+ readonly upstreamLines?: number | undefined;
35
+ readonly error?: string | undefined;
36
+ }
37
+ /** Result of a full upstream sync check. */
38
+ export interface SyncUpstreamReport {
39
+ readonly origin: OriginMeta;
40
+ readonly checked: number;
41
+ readonly changed: number;
42
+ readonly upToDate: number;
43
+ readonly errors: number;
44
+ readonly skills: readonly UpstreamCheckResult[];
45
+ }
46
+ /** Load sources.json from a skills package directory. */
47
+ export declare function loadSourcesManifest(packageDir: string): SourcesManifest | undefined;
48
+ /**
49
+ * Check all skills in a sources.json manifest against their upstream.
50
+ * Compares line counts as a quick diff indicator (full diff would be too verbose).
51
+ */
52
+ export declare function checkUpstream(packageDir: string): Promise<SyncUpstreamReport | undefined>;
53
+ //# sourceMappingURL=sync-upstream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-upstream.d.ts","sourceRoot":"","sources":["../src/sync-upstream.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,uCAAuC;AACvC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAED,gCAAgC;AAChC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,+BAA+B;AAC/B,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC9C;AAED,qDAAqD;AACrD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,aAAa,GAAG,eAAe,CAAC;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,4CAA4C;AAC5C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACjD;AAED,yDAAyD;AACzD,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAKnF;AA6BD;;;GAGG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAiD/F"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Upstream sync — fetches SKILL.md from origin repos defined in sources.json
3
+ * and reports which skills have upstream changes.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ /** Load sources.json from a skills package directory. */
10
+ export function loadSourcesManifest(packageDir) {
11
+ const sourcesPath = join(packageDir, 'sources.json');
12
+ if (!existsSync(sourcesPath))
13
+ return undefined;
14
+ const raw = readFileSync(sourcesPath, 'utf-8');
15
+ return JSON.parse(raw);
16
+ }
17
+ /** Build the raw GitHub URL for a skill's SKILL.md. */
18
+ function rawUrl(origin, skillPath) {
19
+ return `https://raw.githubusercontent.com/${origin.repo}/${origin.branch}/${skillPath}`;
20
+ }
21
+ /** Fetch text content from a URL with timeout. */
22
+ async function fetchText(url, timeoutMs = 10000) {
23
+ try {
24
+ const controller = new AbortController();
25
+ const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
26
+ const response = await fetch(url, { signal: controller.signal });
27
+ clearTimeout(timer);
28
+ if (!response.ok) {
29
+ return { ok: false, text: '', error: `HTTP ${response.status}` };
30
+ }
31
+ const text = await response.text();
32
+ return { ok: true, text };
33
+ }
34
+ catch (err) {
35
+ return { ok: false, text: '', error: err instanceof Error ? err.message : String(err) };
36
+ }
37
+ }
38
+ /** Count non-empty lines in text. */
39
+ function lineCount(text) {
40
+ return text.split('\n').length;
41
+ }
42
+ /**
43
+ * Check all skills in a sources.json manifest against their upstream.
44
+ * Compares line counts as a quick diff indicator (full diff would be too verbose).
45
+ */
46
+ export async function checkUpstream(packageDir) {
47
+ const manifest = loadSourcesManifest(packageDir);
48
+ if (!manifest)
49
+ return undefined;
50
+ const results = [];
51
+ for (const [skillId, source] of Object.entries(manifest.skills)) {
52
+ const localSkillMd = join(packageDir, skillId, 'SKILL.md');
53
+ if (!existsSync(localSkillMd)) {
54
+ results.push({ skillId, originPath: source.path, status: 'local-missing' });
55
+ continue;
56
+ }
57
+ const localText = readFileSync(localSkillMd, 'utf-8');
58
+ const url = rawUrl(manifest.origin, source.path);
59
+ const upstream = await fetchText(url);
60
+ if (!upstream.ok) {
61
+ results.push({
62
+ skillId,
63
+ originPath: source.path,
64
+ status: 'fetch-error',
65
+ error: upstream.error,
66
+ });
67
+ continue;
68
+ }
69
+ const localLines = lineCount(localText);
70
+ const upstreamLines = lineCount(upstream.text);
71
+ const changed = localLines !== upstreamLines;
72
+ results.push({
73
+ skillId,
74
+ originPath: source.path,
75
+ status: changed ? 'changed' : 'up-to-date',
76
+ localLines,
77
+ upstreamLines,
78
+ });
79
+ }
80
+ return {
81
+ origin: manifest.origin,
82
+ checked: results.length,
83
+ changed: results.filter((r) => r.status === 'changed').length,
84
+ upToDate: results.filter((r) => r.status === 'up-to-date').length,
85
+ errors: results.filter((r) => r.status === 'fetch-error' || r.status === 'local-missing').length,
86
+ skills: results,
87
+ };
88
+ }
89
+ //# sourceMappingURL=sync-upstream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-upstream.js","sourceRoot":"","sources":["../src/sync-upstream.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-core",
3
- "version": "0.3.2",
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,11 +25,11 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "yaml": "^2.0.0",
28
- "@dzhechkov/adapter-claude": "0.2.0",
29
28
  "@dzhechkov/adapter-hermes": "0.2.0",
30
29
  "@dzhechkov/adapter-opencode": "0.2.0",
31
- "@dzhechkov/adapter-codex": "0.2.0",
30
+ "@dzhechkov/adapter-claude": "0.2.0",
32
31
  "@dzhechkov/core": "0.2.0",
32
+ "@dzhechkov/adapter-codex": "0.2.0",
33
33
  "@dzhechkov/adapter-openclaude": "0.1.0"
34
34
  },
35
35
  "devDependencies": {
@@ -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
@@ -14,3 +14,7 @@ export * from './operations.js';
14
14
  export * from './workflows.js';
15
15
  export { createSkill } from './create-skill.js';
16
16
  export type { CreateSkillOptions, CreateSkillResult } from './create-skill.js';
17
+ export { checkUpstream, loadSourcesManifest } from './sync-upstream.js';
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';
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Upstream sync — fetches SKILL.md from origin repos defined in sources.json
3
+ * and reports which skills have upstream changes.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ /** A single skill's source mapping. */
12
+ export interface SkillSource {
13
+ readonly path: string;
14
+ readonly version?: string | undefined;
15
+ readonly trust?: string | undefined;
16
+ readonly note?: string | undefined;
17
+ }
18
+
19
+ /** The origin repo metadata. */
20
+ export interface OriginMeta {
21
+ readonly repo: string;
22
+ readonly url: string;
23
+ readonly branch: string;
24
+ readonly license: string;
25
+ readonly author: string;
26
+ readonly canonicalized: string;
27
+ }
28
+
29
+ /** The sources.json schema. */
30
+ export interface SourcesManifest {
31
+ readonly origin: OriginMeta;
32
+ readonly skills: Record<string, SkillSource>;
33
+ }
34
+
35
+ /** Result of checking one skill against upstream. */
36
+ export interface UpstreamCheckResult {
37
+ readonly skillId: string;
38
+ readonly originPath: string;
39
+ readonly status: 'up-to-date' | 'changed' | 'fetch-error' | 'local-missing';
40
+ readonly localLines?: number | undefined;
41
+ readonly upstreamLines?: number | undefined;
42
+ readonly error?: string | undefined;
43
+ }
44
+
45
+ /** Result of a full upstream sync check. */
46
+ export interface SyncUpstreamReport {
47
+ readonly origin: OriginMeta;
48
+ readonly checked: number;
49
+ readonly changed: number;
50
+ readonly upToDate: number;
51
+ readonly errors: number;
52
+ readonly skills: readonly UpstreamCheckResult[];
53
+ }
54
+
55
+ /** Load sources.json from a skills package directory. */
56
+ export function loadSourcesManifest(packageDir: string): SourcesManifest | undefined {
57
+ const sourcesPath = join(packageDir, 'sources.json');
58
+ if (!existsSync(sourcesPath)) return undefined;
59
+ const raw = readFileSync(sourcesPath, 'utf-8');
60
+ return JSON.parse(raw) as SourcesManifest;
61
+ }
62
+
63
+ /** Build the raw GitHub URL for a skill's SKILL.md. */
64
+ function rawUrl(origin: OriginMeta, skillPath: string): string {
65
+ return `https://raw.githubusercontent.com/${origin.repo}/${origin.branch}/${skillPath}`;
66
+ }
67
+
68
+ /** Fetch text content from a URL with timeout. */
69
+ async function fetchText(url: string, timeoutMs: number = 10000): Promise<{ ok: boolean; text: string; error?: string }> {
70
+ try {
71
+ const controller = new AbortController();
72
+ const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
73
+ const response = await fetch(url, { signal: controller.signal });
74
+ clearTimeout(timer);
75
+ if (!response.ok) {
76
+ return { ok: false, text: '', error: `HTTP ${response.status}` };
77
+ }
78
+ const text = await response.text();
79
+ return { ok: true, text };
80
+ } catch (err) {
81
+ return { ok: false, text: '', error: err instanceof Error ? err.message : String(err) };
82
+ }
83
+ }
84
+
85
+ /** Count non-empty lines in text. */
86
+ function lineCount(text: string): number {
87
+ return text.split('\n').length;
88
+ }
89
+
90
+ /**
91
+ * Check all skills in a sources.json manifest against their upstream.
92
+ * Compares line counts as a quick diff indicator (full diff would be too verbose).
93
+ */
94
+ export async function checkUpstream(packageDir: string): Promise<SyncUpstreamReport | undefined> {
95
+ const manifest = loadSourcesManifest(packageDir);
96
+ if (!manifest) return undefined;
97
+
98
+ const results: UpstreamCheckResult[] = [];
99
+
100
+ for (const [skillId, source] of Object.entries(manifest.skills)) {
101
+ const localSkillMd = join(packageDir, skillId, 'SKILL.md');
102
+
103
+ if (!existsSync(localSkillMd)) {
104
+ results.push({ skillId, originPath: source.path, status: 'local-missing' });
105
+ continue;
106
+ }
107
+
108
+ const localText = readFileSync(localSkillMd, 'utf-8');
109
+ const url = rawUrl(manifest.origin, source.path);
110
+ const upstream = await fetchText(url);
111
+
112
+ if (!upstream.ok) {
113
+ results.push({
114
+ skillId,
115
+ originPath: source.path,
116
+ status: 'fetch-error',
117
+ error: upstream.error,
118
+ });
119
+ continue;
120
+ }
121
+
122
+ const localLines = lineCount(localText);
123
+ const upstreamLines = lineCount(upstream.text);
124
+ const changed = localLines !== upstreamLines;
125
+
126
+ results.push({
127
+ skillId,
128
+ originPath: source.path,
129
+ status: changed ? 'changed' : 'up-to-date',
130
+ localLines,
131
+ upstreamLines,
132
+ });
133
+ }
134
+
135
+ return {
136
+ origin: manifest.origin,
137
+ checked: results.length,
138
+ changed: results.filter((r) => r.status === 'changed').length,
139
+ upToDate: results.filter((r) => r.status === 'up-to-date').length,
140
+ errors: results.filter((r) => r.status === 'fetch-error' || r.status === 'local-missing').length,
141
+ skills: results,
142
+ };
143
+ }