// Package ai is the augmented generation layer that sits ON TOP of the deterministic engine, never // around it. It composes the augmented generation family (RAG over tenant documents, TAG over the tenant // database of record, KAG over clearance and skills, GraphRAG over awards and opportunities, CAG over // the stable public rules), routes each question to the cheapest sufficient strategy, and runs a // faithfulness gate before any answer ships. It enforces, by construction, the platform's hard // boundaries: // // - EEO isolation is absolute. NO AI path may read, index, reference, or name the eeo schema. The // guards in this file refuse any SQL that mentions it, and a test walks every file in this package // and fails if the word ever appears outside a prohibition comment. // - The deterministic engine is the floor. The AI layer reads engine outputs and tenant data and // narrates; it never recomputes a score or routes around engine.ScoreCandidate. // - Reads are read only, parameterized, RLS scoped, and table allowlisted. The TAG layer chooses from // a curated set of safe query intents, never free form SQL, the injection resistant pattern. // - Every AI action emits an append only AEDT audit event. package ai import ( "errors" "regexp" "strings" ) // ErrEEOForbidden is returned when any AI path references the segregated EEO schema. It is a hard, // non recoverable refusal: demographics must never reach a model, an index, a graph, or a generated query. var ErrEEOForbidden = errors.New("ai: the eeo schema is forbidden to every AI path") // ErrNotReadOnly is returned when a TAG query is anything other than a single read. var ErrNotReadOnly = errors.New("ai: only a single read only statement is allowed") // eeoMarkerRe matches any reference to the EEO schema, its table, or its demographic columns, in any // casing. This is the runtime backstop behind the structural separation (sqlc never models eeo). var eeoMarkerRe = regexp.MustCompile(`(?i)\beeo\b|candidate_demographics|race_ethnicity|veteran_status|disability_status`) // mutatingRe matches any statement that is not a pure read. The TAG layer is read only. var mutatingRe = regexp.MustCompile(`(?i)\b(insert|update|delete|drop|alter|create|truncate|grant|revoke|copy|merge|call|do|vacuum|comment|reindex|set|reset|begin|commit|rollback)\b`) // ForbidEEO refuses any text (a generated query, a prompt, a retrieved chunk, a memory record) that // references the EEO schema. Every AI code path that touches SQL or model context calls this; the test // suite proves no path can skip it. func ForbidEEO(text string) error { if eeoMarkerRe.MatchString(text) { return ErrEEOForbidden } return nil } // protectedAttrRe matches a natural language reference to a protected attribute or a close proxy. It is the // NL layer above the schema guard ForbidEEO: ForbidEEO blocks a query or prompt that NAMES the eeo schema, // this catches a question that asks about demographics in plain words ("list candidates by race and // gender") even though it names no schema object. An AEDT must never group, filter, or rank people by a // protected attribute, so the brain refuses such a question rather than answering it. var protectedAttrRe = regexp.MustCompile(`(?i)\b(race|racial|ethnicity|ethnic|gender|disabilit[a-z]+|religion|religious|pregnan[a-z]+|sexual orientation|national origin|marital status|veteran status)\b`) // MentionsProtectedAttribute reports whether text asks about a protected attribute or a close proxy. func MentionsProtectedAttribute(text string) bool { return protectedAttrRe.MatchString(text) } // GuardReadOnlyQuery is the TAG safety gate. A query may execute only if it is a single SELECT (or a // read only CTE leading to SELECT), references no EEO marker, and uses only allowlisted tables. It is the // last line of defense behind the intent registry (which already constrains queries to a curated set); // together they make a write or an EEO read impossible from the natural language path. func GuardReadOnlyQuery(sql string, allowedTables map[string]bool) error { trimmed := strings.TrimSpace(sql) if trimmed == "" { return ErrNotReadOnly } // Exactly one statement: a stray semicolon (stacked queries) is refused. if strings.Count(strings.TrimRight(trimmed, "; \n\t"), ";") > 0 { return errors.New("ai: stacked statements are not allowed") } if err := ForbidEEO(trimmed); err != nil { return err } lower := strings.ToLower(trimmed) if !strings.HasPrefix(lower, "select") && !strings.HasPrefix(lower, "with") { return ErrNotReadOnly } // No mutating keyword may appear anywhere (a WITH ... that hides an INSERT, for example). if mutatingRe.MatchString(lower) { return ErrNotReadOnly } // Every referenced table (FROM/JOIN target) must be in the allowlist OR be a CTE defined in this same // query (a name introduced by WITH ... AS, which is not a real table). ctes := cteNames(lower) for _, tbl := range referencedTables(lower) { if !allowedTables[tbl] && !ctes[tbl] { return errors.New("ai: table " + tbl + " is not in the read allowlist") } } return nil } var tableRefRe = regexp.MustCompile(`(?i)\b(?:from|join)\s+([a-z_][a-z0-9_]*)`) // cteNameRe captures a CTE name introduced by WITH, WITH RECURSIVE, or a subsequent comma separated // CTE. The optional recursive keyword is tolerated so a WITH RECURSIVE name and every following comma // separated CTE name is recognized as a query local CTE, which lets the allowlist check accept the // recursive self reference instead of rejecting it as a missing table. var cteNameRe = regexp.MustCompile(`(?i)(?:with(?:\s+recursive)?|,)\s+([a-z_][a-z0-9_]*)\s+as\s*\(`) // cteNames returns the names introduced by WITH ... AS in a read query, so the allowlist check does not // reject a reference to a query local CTE. func cteNames(lowerSQL string) map[string]bool { out := map[string]bool{} for _, m := range cteNameRe.FindAllStringSubmatch(lowerSQL, -1) { out[m[1]] = true } return out } // referencedTables extracts the FROM/JOIN table identifiers from a read query (lowercased). It is a // conservative helper for the allowlist check; the intent registry is the primary control. func referencedTables(lowerSQL string) []string { var out []string for _, m := range tableRefRe.FindAllStringSubmatch(lowerSQL, -1) { out = append(out, m[1]) } return out } // ReadAllowlist is the set of tables the TAG layer may read. It is deliberately a curated subset of the // public schema and contains NO eeo table, NO auth secret table, and NO demographic column source. // Changing it is a security decision. var ReadAllowlist = map[string]bool{ "candidates": true, "candidate_skills": true, "candidate_certifications": true, "candidate_customer_experience": true, "requirements": true, "required_skills": true, "opportunities": true, "opportunity_records": true, "slates": true, "slate_members": true, "candidate_evaluations": true, "skills": true, "sam_opportunities": true, "usaspending_awards": true, "bls_wages": true, // GLOBAL public reference tables (no PII, safe cross tenant): the WS1 ontology and WS2 skill // relationship and alias layer that the G3 graph traversals walk. "skill_relationships": true, "skill_aliases": true, "ontology_crosswalk": true, "mission_areas": true, // Public market reference tables: the federal mission tags and award incumbent roles that the G1 // market and relationship graph walks. "federal_mission_tags": true, "award_incumbent_roles": true, // Global clearance reference tables (no PII, FK enforced reference data) for the G5 clearance lattice. "clearance_levels": true, "polygraph_types": true, "clearance_compartments": true, "clearance_reciprocity": true, // M7 bid outcomes ledger (a tenant table, RLS scoped) for the advisory bid_win_rate_by_segment base rate // read. A reviewed decision: bid_outcomes records win and loss labels for a bid (opportunity, requirement, // slate links, result, awardee UEI, notes), which is tenant business data, NOT eeo and NOT a person table, // so it carries no protected attribute and needs no small cell suppression. It is allowlisted only for the // single aggregate base rate intent; the win rate it returns is advisory market context, never a candidate // score or an employment decision number. "bid_outcomes": true, // Phase 2 Vendor Intelligence (migration 0042), tenant tables, RLS scoped. staffing_vendors is the org's // vendor registry (no person data); vendor_submissions is the append friendly fact of a vendor submitting a // candidate for a requirement (a candidate id and a requirement id, tenant business data, NOT eeo and NOT a // protected attribute). The vendor_performance_scorecard TAG intent reads them for outcome aggregates; the // vendor_quality_distribution BENCH intent joins them to the frozen candidate_evaluations band and is k=10 // suppressed because it partitions candidate headcounts per vendor per band. Neither ever rescores a person. "staffing_vendors": true, "vendor_submissions": true, // MAG (Model Augmented Generation) coefficient and calibration store (migration 0036), the serving surface // for model backed intents. A reviewed decision: these three tables hold GLOBAL model parameters (the // interpretable numeric coefficients, the champion registry, and the monotone calibration map), NOT eeo, // NOT a person table, and NO protected attribute; they carry no PII, so they are safe cross tenant reference // reads the MAG executor loads the champion coefficients from. They are migrator owned and app SELECT only, // so the app can read them to serve but never write them. "aedt_learned_models": true, "aedt_model_coefficients": true, "aedt_calibration_bins": true, }