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

59.09% Statements 13/22
18.18% Branches 2/11
100% Functions 3/3
59.09% Lines 13/22

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                  4x 4x   4x                                                                 4x                                       49x       29x         29x 29x         29x 29x                     29x       35x 35x    
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { z } from 'zod';
import { UsageStatsSchema } from '../types/index.js';
import { BUILD_STATE_FILE } from './definition.js';
import {
  SkillBuildOutlineSchema,
} from './outline-contract.js';
 
export const SKILL_BUILD_STATE_SCHEMA_VERSION = 1;
export const SKILL_BUILD_STATE_KIND = 'skill-build-state';
 
export const GeneratedSkillArtifactStateSchema = z.object({
  version: z.literal(5),
  sourceHash: z.string().min(1),
  outlineHash: z.string().min(1),
  buildVersion: z.string().min(1),
  authoringProvider: z.object({
    name: z.string().min(1),
    rootDir: z.string().min(1),
    contentHash: z.string().min(1),
  }).strict(),
  name: z.string().min(1),
  fileManifest: z.array(z.object({
    path: z.string().min(1),
    bytes: z.number().int().nonnegative(),
  }).strict()).min(1),
  deterministicWarnings: z.array(z.string().min(1)).default([]),
  bytes: z.number().int().nonnegative(),
  durationMs: z.number().nonnegative(),
  usage: UsageStatsSchema,
  externalSources: z.array(z.object({
    title: z.string().min(1),
    url: z.string().min(1),
    reason: z.string().min(1),
  }).strict()),
  missingInputs: z.array(z.string().min(1)),
  authoringWarnings: z.array(z.string().min(1)).default([]),
  responseModel: z.string().optional(),
  numTurns: z.number().int().nonnegative().optional(),
  generatedAt: z.string().min(1),
}).strict();
 
export type GeneratedSkillArtifactState = z.infer<typeof GeneratedSkillArtifactStateSchema>;
 
export const SkillBuildStateSchema = z.object({
  version: z.literal(SKILL_BUILD_STATE_SCHEMA_VERSION),
  kind: z.literal(SKILL_BUILD_STATE_KIND),
  identity: z.object({
    requestedModel: z.string().min(1).optional(),
  }).strict().optional(),
  outline: SkillBuildOutlineSchema,
  outlineRun: z.object({
    durationMs: z.number().nonnegative().optional(),
    usage: UsageStatsSchema.optional(),
    responseModel: z.string().optional(),
    numTurns: z.number().int().nonnegative().optional(),
  }).strict().optional(),
  artifact: GeneratedSkillArtifactStateSchema.optional(),
  updatedAt: z.string().optional(),
}).strict();
 
export type SkillBuildState = z.infer<typeof SkillBuildStateSchema>;
 
export function getBuildStatePath(rootDir: string): string {
  return join(rootDir, BUILD_STATE_FILE);
}
 
export function readSkillBuildState(path: string): SkillBuildState | undefined {
  Iif (!existsSync(path)) {
    return undefined;
  }
 
  let parsed: unknown;
  try {
    parsed = JSON.parse(readFileSync(path, 'utf-8'));
  } catch {
    return undefined;
  }
 
  const validation = SkillBuildStateSchema.safeParse(parsed);
  Iif (!validation.success) {
    if (parsed && typeof parsed === 'object' && 'artifact' in parsed) {
      const withoutArtifact = { ...parsed };
      delete (withoutArtifact as { artifact?: unknown }).artifact;
      const outlineOnlyValidation = SkillBuildStateSchema.safeParse(withoutArtifact);
      if (outlineOnlyValidation.success) {
        return outlineOnlyValidation.data;
      }
    }
    return undefined;
  }
  return validation.data;
}
 
export function writeSkillBuildState(path: string, state: SkillBuildState): void {
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
}