← back to Homesonspec
packages/validation/src/types.ts
66 lines
import type { ExtractedEntityType, ExtractedRecord } from "@homesonspec/schemas";
/**
* Deterministic validation layer. Rules — not the LLM — decide whether a
* staged record can publish.
*
* Severity semantics:
* error → blocks publish (verdict: rejected)
* review → publishable only after human approval (verdict: needs_review)
* warn → publishes, logged as a ValidationEvent
*/
export type Severity = "error" | "review" | "warn";
export type Verdict = "publishable" | "needs_review" | "rejected";
export interface RuleContext {
/** Read-only DB lookups (dup check, prior price, FK existence). */
db: RuleDb;
sourceKey: string;
now: Date;
}
/** The narrow read-only surface rules may touch — keeps rules testable with a stub. */
export interface RuleDb {
activeHomeExistsAtAddress(normalizedAddress: string, zip: string | null, excludeCanonicalKey: string): Promise<boolean>;
publishedPriceForCanonicalKey(canonicalKey: string): Promise<number | null>;
communityExists(communitySlugOrName: string, builderSlug: string): Promise<boolean>;
builderExists(builderSlug: string): Promise<boolean>;
}
export type RuleResult =
| { passed: true }
| { passed: false; message: string; details?: Record<string, unknown> };
export interface ValidationRule {
id: string;
severity: Severity;
entityTypes: ExtractedEntityType[];
check(record: StagedCandidate, ctx: RuleContext): RuleResult | Promise<RuleResult>;
}
/** What the pipeline hands to validation: extracted record + canonical key. */
export interface StagedCandidate extends ExtractedRecord {
canonicalKey: string;
}
export interface ValidationEventInput {
ruleId: string;
severity: Severity;
passed: boolean;
message?: string;
details?: Record<string, unknown>;
}
export interface ValidationOutcome {
events: ValidationEventInput[];
verdict: Verdict;
}
/** Helper: pull a typed field value out of the FieldValue envelope. */
export function fieldValue<T>(record: StagedCandidate, field: string): T | null {
const entry = record.fields[field];
if (!entry) return null;
return (entry.value as T) ?? null;
}