export type Severity = "hint" | "info" | "warning"; export interface Finding { readonly ruleId: string; readonly severity: Severity; readonly message: string; readonly note?: string; readonly path: string; readonly range: Readonly<{ start: Readonly<{ line: number; column: number }>; end: Readonly<{ line: number; column: number }> }>; readonly matchedTextHash: string; } export interface AnalyzerFailure { readonly kind: "missing" | "incompatible" | "timeout" | "nonzero" | "malformed" | "killed" | "internal"; readonly message: string } export interface AnalysisSnapshot { readonly analyzerKey: string; readonly workspaceRevision: number; readonly paths: readonly string[]; readonly findings: readonly Finding[]; readonly durationMs: number; readonly failure?: AnalyzerFailure } export interface ProjectContext { readonly root: string; readonly packageName?: string; readonly identity: string; readonly git: boolean } export interface PolicyContext { readonly project: ProjectContext; readonly changedPaths: readonly string[]; readonly workspaceRevision: number } export interface Diagnostic { readonly policyId: string; readonly ruleId: string; readonly path: string; readonly line: number; readonly column: number; readonly message: string; readonly guidance: string; readonly exception?: string; readonly matchedTextHash: string } export interface DiagnosticPolicy { readonly kind: "diagnostic"; readonly id: string; readonly analyzerKeys: readonly string[]; appliesTo(project: ProjectContext, paths: readonly string[]): boolean; evaluate(analysis: ReadonlyMap, context: PolicyContext): readonly Diagnostic[] } export interface ProjectMatcher { matches(project: ProjectContext): boolean } export interface VerificationRequirement { readonly id: string; readonly command: string; readonly cwd: "project-root"; affects(path: string): boolean } export interface VerificationPolicy { readonly kind: "verification"; readonly id: string; readonly project: ProjectMatcher; readonly requirements: readonly VerificationRequirement[] } export type PersonalPolicy = DiagnosticPolicy | VerificationPolicy; const isFn = (value: unknown): value is (...args: never[]) => unknown => typeof value === "function"; export function validatePolicy(value: unknown): { ok: true; policy: PersonalPolicy } | { ok: false; error: string } { if (!value || typeof value !== "object") return { ok: false, error: "default export must be an object" }; const candidate = value as Record; if (typeof candidate.id !== "string" || !candidate.id.trim()) return { ok: false, error: "policy id must be non-empty" }; if (candidate.kind === "diagnostic") { if (!Array.isArray(candidate.analyzerKeys) || candidate.analyzerKeys.some((key) => typeof key !== "string" || !key)) return { ok: false, error: "diagnostic analyzerKeys must be non-empty strings" }; if (!isFn(candidate.appliesTo) || !isFn(candidate.evaluate)) return { ok: false, error: "diagnostic policy requires appliesTo and evaluate functions" }; return { ok: true, policy: value as DiagnosticPolicy }; } if (candidate.kind === "verification") { const project = candidate.project as Record | undefined; if (!project || !isFn(project.matches) || !Array.isArray(candidate.requirements)) return { ok: false, error: "verification policy requires project.matches and requirements" }; for (const requirement of candidate.requirements as unknown[]) { const req = requirement as Record | undefined; if (!req || typeof req.id !== "string" || !req.id || typeof req.command !== "string" || !req.command.trim() || req.cwd !== "project-root" || !isFn(req.affects)) return { ok: false, error: "invalid verification requirement" }; } return { ok: true, policy: value as VerificationPolicy }; } return { ok: false, error: "policy kind must be diagnostic or verification" }; }