All files / src/skill-builder outline-contract.ts

84.61% Statements 11/13
50% Branches 4/8
100% Functions 2/2
84.61% Lines 11/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106        4x 4x   4x   4x           4x         4x             29x             29x                 4x                               4x                                                                           17x                
import { createHash } from 'node:crypto';
import { z } from 'zod';
import type { UsageStats } from '../types/index.js';
 
export const SKILL_BUILD_VERSION = '1';
export const SKILL_BUILD_OUTLINE_SCHEMA_VERSION = 1;
 
const OutlinePhaseStatusSchema = z.enum(['cached', 'generated', 'validated']);
 
export const SkillBuildExternalSourceSchema = z.object({
  title: z.string().min(1),
  url: z.string().min(1),
  reason: z.string().min(1),
}).strict();
 
export const SkillBuildPhaseSchema = z.object({
  id: z.string().min(1),
  status: OutlinePhaseStatusSchema,
}).strict();
 
export const SkillBuildScopeProfileSchema = z.object({
  kind: z.enum(['domain', 'ecosystem', 'repository', 'product']),
  subject: z.string().min(1),
  localContextUsed: z.boolean(),
  observedContext: z.array(z.string().min(1)).min(1),
  unresolvedContext: z.array(z.string().min(1)).default([]),
}).strict().superRefine((value, ctx) => {
  Iif (value.kind === 'repository' && !value.localContextUsed) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'repository scope requires localContextUsed to be true',
      path: ['localContextUsed'],
    });
  }
  Iif (value.localContextUsed && value.observedContext.length === 0) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'observedContext is required when localContextUsed is true',
      path: ['observedContext'],
    });
  }
});
 
export const SkillBuildTrackSchema = z.object({
  id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/),
  title: z.string().min(1),
  goal: z.string().min(1),
  rationale: z.string().min(1),
  sourceSignals: z.array(z.string().min(1)).min(1),
  owns: z.array(z.string().min(1)).min(1),
  excludes: z.array(z.string().min(1)).default([]),
  relevanceSignals: z.array(z.string().min(1)).min(1),
  evidenceFocus: z.array(z.string().min(1)).min(1),
  checks: z.array(z.string().min(1)).min(1),
  safeCounterpatterns: z.array(z.string().min(1)).min(1),
  falsePositiveTraps: z.array(z.string().min(1)).min(1),
  researchHints: z.array(z.string().min(1)).default([]),
}).strict();
 
export const SkillBuildOutlineSchema = z.object({
  version: z.literal(SKILL_BUILD_OUTLINE_SCHEMA_VERSION),
  skill: z.string().min(1),
  sourceHash: z.string().min(1),
  buildVersion: z.string().min(1),
  scopeProfile: SkillBuildScopeProfileSchema,
  build: z.object({
    phases: z.array(SkillBuildPhaseSchema).min(1),
    externalSources: z.array(SkillBuildExternalSourceSchema).optional(),
  }).strict(),
  tracks: z.array(SkillBuildTrackSchema).min(1),
}).strict();
 
export type SkillBuildOutline = z.infer<typeof SkillBuildOutlineSchema>;
 
export interface SkillBuildSourceFile {
  path: string;
  content: string;
}
 
export interface SkillBuildSource {
  hash: string;
  files: SkillBuildSourceFile[];
}
 
export type SkillBuildOutlineSource = 'cache' | 'generated';
 
export interface SkillBuildOutlineResult {
  outline: SkillBuildOutline;
  source: SkillBuildOutlineSource;
  statePath: string;
  usage?: UsageStats;
  durationMs?: number;
  responseModel?: string;
  numTurns?: number;
}
 
export function outlineHash(outline: SkillBuildOutline): string {
  return createHash('sha256').update(JSON.stringify({
    skill: outline.skill,
    sourceHash: outline.sourceHash,
    buildVersion: outline.buildVersion,
    tracks: outline.tracks,
    scopeProfile: outline.scopeProfile,
  })).digest('hex');
}