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
JavaScript
/**
* 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