planner.ts
1800 bytes
1import type { FileSystem } from "../ports/fs.ts";
2import type { FrontmatterCodec } from "../frontmatter/parser.ts";
3import type { FrontmatterPatcher } from "../frontmatter/patcher.ts";
4import type { SkillChange, SkillDraft, SkillRecord } from "../types.ts";
5import { hasDuplicateDisableModelInvocation } from "../frontmatter/validation.ts";
6import { classifyInvocationMode } from "../inventory/classifier.ts";
7
8export interface SkillTogglePlanner {
9 plan(records: SkillRecord[], drafts: SkillDraft[]): Promise<SkillChange[]>;
10}
11
12export class DefaultSkillTogglePlanner implements SkillTogglePlanner {
13 constructor(
14 private readonly fs: FileSystem,
15 private readonly codec: FrontmatterCodec,
16 private readonly patcher: FrontmatterPatcher,
17 ) {}
18
19 async plan(records: SkillRecord[], drafts: SkillDraft[]): Promise<SkillChange[]> {
20 const recordById = new Map(records.map((record) => [record.id, record]));
21 const changes: SkillChange[] = [];
22
23 for (const draft of drafts) {
24 const record = recordById.get(draft.skill.id);
25 if (!record || !record.editable) continue;
26
27 const raw = await this.fs.readFile(record.filePath);
28 const doc = this.codec.parse(raw);
29 if (!doc.hasFrontmatter) continue;
30
31 const currentMode = classifyInvocationMode(doc);
32 const needsNormalization = hasDuplicateDisableModelInvocation(doc);
33 if (currentMode === draft.desiredMode && !needsNormalization) continue;
34
35 const patch = this.patcher.patchInvocationMode(doc, draft.desiredMode);
36 if (patch.oldText === patch.newText) continue;
37
38 changes.push({
39 skill: { ...record, mode: currentMode },
40 filePath: record.filePath,
41 from: currentMode,
42 to: draft.desiredMode,
43 patch,
44 });
45 }
46
47 return changes;
48 }
49}