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

116 lines 4.14 kB
/** * Contributor Frontmatter Validation * * Zod schemas for known contributor kinds (`status`, `research`). Schemas are * keyed by kind so future consumers register their schema without forking the * discovery module. * * @architecture @.aiwg/architecture/decisions/ADR-023-contributor-discovery-convention.md * @issue #938 */ import { z } from 'zod'; /** * Detection spec — declarative globs + counts. Common to every kind. */ const DetectionSpecSchema = z.object({ glob: z.array(z.string()).min(1, 'detect.glob must have at least one pattern'), minCount: z.number().int().positive().optional(), conditions: z.record(z.string()).optional(), }); /** * Base frontmatter shared by every kind. Per-kind schemas extend this. */ const ContributorBaseSchema = z.object({ kind: z.string().min(1), domain: z.string().min(1, 'domain is required'), description: z.string().min(1, 'description is required'), detect: DetectionSpecSchema, }); /** * `kind: status` frontmatter. Reports observed state of a framework/domain * (phase, counts, dates). Fields beyond the base schema are descriptive only — * no prescriptive `next:` arrays per ADR-023 §Output voice. */ export const StatusContributorSchema = ContributorBaseSchema.extend({ kind: z.literal('status'), /** * Optional declarative field extractors. Each entry pulls a value out of a * file using regex or count. Keeps contributors data-driven rather than * code-driven; the aggregator reads these to populate the report block. */ fields: z .record(z.object({ type: z.enum(['string', 'number', 'date']), source: z.string(), regex: z.string().optional(), count: z.string().optional(), })) .optional(), }); /** * `kind: research` frontmatter. Configures research fan-out for a framework's * domain — focus areas, source preferences, recency window. */ export const ResearchContributorSchema = ContributorBaseSchema.extend({ kind: z.literal('research'), sources: z .object({ preferred: z.array(z.string()).optional(), exclude: z.array(z.string()).optional(), }) .optional(), focus_areas: z.array(z.string()).min(1, 'research contributors must declare at least one focus area'), recency_default_months: z.number().int().positive().optional(), }); /** * Schema registry. Adding a new kind means adding its schema here — no other * code changes are required for the discovery module to validate it. */ const SCHEMA_REGISTRY = { status: StatusContributorSchema, research: ResearchContributorSchema, }; /** * Look up the zod schema for a kind. Throws if no schema is registered — * unknown kinds must be deliberately added rather than silently accepted. */ export function getSchemaForKind(kind) { const schema = SCHEMA_REGISTRY[kind]; if (!schema) { throw new Error(`Unknown contributor kind '${kind}'. Register a zod schema in src/contributors/validation.ts before using it.`); } return schema; } /** * List all registered kinds. Used by validate-metadata to scan for * orphaned contributor files (kind in frontmatter but no schema registered). */ export function getRegisteredKinds() { return Object.keys(SCHEMA_REGISTRY); } /** * Validate a parsed frontmatter object against its kind's schema. Returns * either the typed data or an error with formatted issue messages. */ export function validateContributor(data) { const kind = data.kind; if (typeof kind !== 'string' || kind.length === 0) { return { ok: false, errors: ['frontmatter is missing required field `kind`'] }; } let schema; try { schema = getSchemaForKind(kind); } catch (err) { return { ok: false, errors: [err.message] }; } const result = schema.safeParse(data); if (!result.success) { return { ok: false, errors: result.error.errors.map(e => `${e.path.join('.') || '<root>'}: ${e.message}`), }; } return { ok: true, data: result.data }; } //# sourceMappingURL=validation.js.map