UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

170 lines 6.81 kB
/** * View definition parser and validator. * * @implements #1207 * @see .aiwg/architecture/adr-rlm-index-features-impl-plan.md */ import { load as loadYaml, dump as dumpYaml } from 'js-yaml'; const VALID_AGGREGATES = [ 'concat', 'summarize', 'filter-true', 'filter-false', 'json-merge', ]; const VALID_PRODUCERS = ['rlm-batch', 'rlm-query']; const VALID_SCHEDULES = ['never', 'daily', 'weekly', 'monthly']; const NAME_RE = /^[a-z0-9][a-z0-9-_]*$/; export class ViewValidationError extends Error { issues; constructor(issues) { super(`Invalid view definition:\n - ${issues.join('\n - ')}`); this.issues = issues; this.name = 'ViewValidationError'; } } /** Parse a YAML string and validate as a ViewDefinition. Throws on failure. */ export function parseViewYaml(yaml) { const raw = loadYaml(yaml); return validate(raw); } /** Validate an arbitrary value as a ViewDefinition. Throws ViewValidationError. */ export function validate(value) { const issues = []; const v = (value ?? {}); if (typeof v['name'] !== 'string' || !NAME_RE.test(v['name'])) { issues.push("'name' must be a kebab-case string [a-z0-9-_]"); } if (typeof v['prompt'] !== 'string' || v['prompt'].trim().length === 0) { issues.push("'prompt' must be a non-empty string"); } const producer = v['producer']; if (typeof producer !== 'string' || !VALID_PRODUCERS.includes(producer)) { issues.push(`'producer' must be one of: ${VALID_PRODUCERS.join(', ')}`); } const aggregate = v['aggregate']; if (typeof aggregate !== 'string' || !VALID_AGGREGATES.includes(aggregate)) { issues.push(`'aggregate' must be one of: ${VALID_AGGREGATES.join(', ')}`); } const inputs = (v['inputs'] ?? {}); const inputModes = ['glob', 'query', 'neighbors-of', 'neighborsOf'].filter((k) => k in inputs); if (inputModes.length === 0) { issues.push("'inputs' must specify exactly one of: glob, query, neighbors-of"); } else if (inputModes.length > 1) { issues.push(`'inputs' must specify exactly one mode (got: ${inputModes.join(', ')})`); } if (inputs['glob'] !== undefined && typeof inputs['glob'] !== 'string') { issues.push("'inputs.glob' must be a string"); } if (inputs['query'] !== undefined && typeof inputs['query'] !== 'string') { issues.push("'inputs.query' must be a string"); } const neighbors = (inputs['neighbors-of'] ?? inputs['neighborsOf']); if (neighbors !== undefined) { if (typeof neighbors !== 'object' || neighbors === null) { issues.push("'inputs.neighbors-of' must be an object with {id, depth, direction}"); } else { if (typeof neighbors['id'] !== 'string') issues.push("'inputs.neighbors-of.id' is required"); const depth = neighbors['depth']; if (typeof depth !== 'number' || depth < 1) { issues.push("'inputs.neighbors-of.depth' must be a positive integer (default 1)"); } const direction = neighbors['direction']; if (direction !== undefined && direction !== 'in' && direction !== 'out' && direction !== 'both') { issues.push("'inputs.neighbors-of.direction' must be one of: in, out, both"); } } } const refresh = (v['refresh'] ?? {}); const onArtifactChange = refresh['on_artifact_change'] ?? refresh['onArtifactChange']; if (onArtifactChange !== undefined && typeof onArtifactChange !== 'boolean') { issues.push("'refresh.on_artifact_change' must be boolean"); } const schedule = refresh['schedule']; if (schedule !== undefined && !VALID_SCHEDULES.includes(schedule)) { issues.push(`'refresh.schedule' must be one of: ${VALID_SCHEDULES.join(', ')}`); } const manualOnly = refresh['manual_only'] ?? refresh['manualOnly']; if (manualOnly !== undefined && typeof manualOnly !== 'boolean') { issues.push("'refresh.manual_only' must be boolean"); } const outputFormat = v['output_format'] ?? v['outputFormat'] ?? 'json'; if (outputFormat !== 'json' && outputFormat !== 'markdown' && outputFormat !== 'text') { issues.push("'output_format' must be one of: json, markdown, text"); } if (issues.length > 0) { throw new ViewValidationError(issues); } // Normalize the inputs into the canonical TS shape const canonicalInputs = {}; if (typeof inputs['glob'] === 'string') canonicalInputs.glob = inputs['glob']; if (typeof inputs['query'] === 'string') canonicalInputs.query = inputs['query']; if (neighbors) { canonicalInputs.neighborsOf = { id: neighbors['id'], depth: neighbors['depth'] ?? 1, direction: (neighbors['direction'] ?? 'both'), ...(typeof neighbors['graph'] === 'string' ? { graph: neighbors['graph'] } : {}), }; } return { name: v['name'], description: typeof v['description'] === 'string' ? v['description'] : undefined, producer: producer, inputs: canonicalInputs, prompt: v['prompt'], aggregate: aggregate, refresh: { onArtifactChange: typeof onArtifactChange === 'boolean' ? onArtifactChange : true, schedule: schedule ?? 'never', manualOnly: typeof manualOnly === 'boolean' ? manualOnly : false, }, outputFormat: outputFormat ?? 'json', }; } /** Render a ViewDefinition back to canonical YAML. */ export function dumpViewYaml(def) { const out = { name: def.name, ...(def.description ? { description: def.description } : {}), producer: def.producer, inputs: renderInputs(def.inputs), prompt: def.prompt, aggregate: def.aggregate, refresh: { on_artifact_change: def.refresh.onArtifactChange, schedule: def.refresh.schedule, manual_only: def.refresh.manualOnly, }, output_format: def.outputFormat, }; return dumpYaml(out, { lineWidth: 100, noRefs: true }); } function renderInputs(inputs) { if (inputs.glob) return { glob: inputs.glob }; if (inputs.query) return { query: inputs.query }; if (inputs.neighborsOf) { return { 'neighbors-of': { id: inputs.neighborsOf.id, depth: inputs.neighborsOf.depth, direction: inputs.neighborsOf.direction, ...(inputs.neighborsOf.graph ? { graph: inputs.neighborsOf.graph } : {}), }, }; } return {}; } //# sourceMappingURL=definition.js.map