import * as yaml from "js-yaml"; import { err, ok, Result } from "neverthrow"; import { appError, AppError, AppErrorVariant } from "@/error"; import { EMPTY_PROJECT_CONFIG, normalizeTag, type ProjectConfig, } from "@/model/projectConfig"; /** * Parse task-folder config.yml content. * Accepts missing/empty tags → []. */ export function parseProjectConfig( content: string, ): Result { if (content.trim() === "") { return ok({ ...EMPTY_PROJECT_CONFIG }); } let data: unknown; try { data = yaml.load(content); } catch (error) { const detail = error instanceof Error ? error.message : "invalid YAML"; return err(appError(AppErrorVariant.INVALID_PROJECT_CONFIG, { detail })); } if (data === null || data === undefined) { return ok({ ...EMPTY_PROJECT_CONFIG }); } if (typeof data !== "object" || Array.isArray(data)) { return err( appError(AppErrorVariant.INVALID_PROJECT_CONFIG, { detail: "root must be a mapping", }), ); } const record = data as Record; if (record.tags === undefined) { return ok({ tags: [] }); } if (!Array.isArray(record.tags)) { return err( appError(AppErrorVariant.INVALID_PROJECT_CONFIG, { detail: "tags must be an array of strings", }), ); } const tags: string[] = []; const seen = new Set(); for (const item of record.tags) { if (typeof item !== "string") { return err( appError(AppErrorVariant.INVALID_PROJECT_CONFIG, { detail: "tags must be an array of strings", }), ); } const tag = normalizeTag(item); if (!tag || seen.has(tag)) continue; seen.add(tag); tags.push(tag); } return ok({ tags }); }