package ai // G4 the bench graph. This file adds the FIRST person touching structural plane as FIXED, named, // parameterized QueryIntents in the existing closed Registry. Every G4 intent reads the RLS protected // person tables (candidates, candidate_skills, candidate_evaluations), so each runs inside RunInTenantTx // (the app role sees only the caller's bench through app.current_org_id) and pipes its aggregate partition // through the deterministic small cell suppression layer before any count leaves the process. The model // authors no SQL: every statement is a static, reviewed constant, registered in the closed Registry, // asserted by ValidateRegistry at boot, and re guarded by GuardReadOnlyQuery on every execution. // // The graph NEVER computes a match score, a band, or any employment decision number. The frozen engine in // internal/modules/matching/engine stays the only scorer and is untouched. These intents report STRUCTURAL // counts over the bench: // // - bench_skill_coverage partitions the bench into {holds the skill, does not hold the skill} for one // skill, a count to size who already carries a capability. // - bench_readiness_distribution reports the distribution of bands the frozen engine ALREADY decided and // audited at evaluation time, read from candidate_evaluations. It recomputes nothing; it counts the // decisions the engine already made. // - bench_gap_for_requirement partitions the bench into {meets, does not meet} where meets is a STRUCTURAL // coverage test (holds at least the required clearance level by the rank ladder AND holds at least a // fixed fraction of the required skills). This is a count to SIZE a gap, explicitly not a match score; // the per candidate score and ranking remain the frozen engine. // // Each intent's cells form a PARTITION of the matched set (every matched candidate lands in exactly one // bucket, so the counts sum to the matched total), which is the assumption Suppress's complementary rule // relies on. The executors build the COMPLETE partition in Go (always both buckets, always the three bands) // so an empty bucket is a true zero in the partition and a suppressed cell is indistinguishable from an // empty one. Headcounts are int (a headcount is an integer, suppressed as a unit); no float touches any // surfaced number. // // DEFERRED, by design: a multi skill ontology family rollup for bench_skill_coverage. A candidate can hold // many skills, so grouping by a skill family is NOT a partition (the cells overlap, the counts do not sum // to the bench), which breaks the assumption Suppress's complementary suppression relies on. Phase 2 ships // ONLY the partition safe single skill version; the family rollup waits for a suppression rule that handles // overlapping cells. import ( "context" "errors" "github.com/jackc/pgx/v5" "github.com/missionworkforce/missionmatch/internal/platform/tenant" ) // The registered names of the three G4 bench intents. Each is a FIXED entry in the closed Registry in // tag.go, so ValidateRegistry asserts it at boot and ExecuteIntent runs it through the same guard, tenant // transaction, and audit posture as every other intent. const ( IntentBenchSkillCoverage = "bench_skill_coverage" IntentBenchReadinessDistribution = "bench_readiness_distribution" IntentBenchGapForRequirement = "bench_gap_for_requirement" ) // The fixed buckets and bands. Building the COMPLETE partition in Go (rather than only the buckets SQL // returns) guarantees the partition spans every bucket, so Suppress always sees a closed set whose counts // sum to the matched total and a suppressed cell is indistinguishable from an empty one. const ( bucketHolds = "holds" bucketLacks = "lacks" bucketMeets = "meets" bucketDoesNotMeet = "does_not_meet" ) // readinessBands is the fixed, ordered set of bands the frozen engine assigns. The readiness distribution // is reported over exactly these three, in this order, so the partition is complete and byte stable. var readinessBands = []string{"green", "yellow", "red"} // benchSkillCoverageSQL counts DISTINCT candidates that HOLD a given skill versus those that do not, for the // caller's org. RLS scopes it to the caller's bench. The bucket is computed with an EXISTS subquery over // candidate_skills (a SCALAR predicate, never a FROM clause set returning function), so the read stays over // the allowlisted candidates and candidate_skills tables and the bucket is single valued (a candidate holds // the skill or it does not), a true two bucket PARTITION whose counts sum to the bench size. The soft delete // and anonymize filters mirror count_candidates_by_clearance so an erased candidate is never counted. // Deterministic ORDER BY on the bucket label, a total order over the two buckets. The executor assembles the // complete {holds, lacks} partition in Go, filling a true zero for a bucket SQL omits. const benchSkillCoverageSQL = ` SELECT CASE WHEN EXISTS ( SELECT 1 FROM candidate_skills cs WHERE cs.organization_id = c.organization_id AND cs.candidate_id = c.id AND cs.skill_id = $1 ) THEN 'holds' ELSE 'lacks' END AS bucket, count(DISTINCT c.id) AS headcount FROM candidates c WHERE c.deleted_at IS NULL AND c.anonymized_at IS NULL GROUP BY bucket ORDER BY bucket ASC` // benchReadinessDistributionSQL counts DISTINCT evaluated candidates per band for one requirement, reading // the ALREADY AUDITED candidate_evaluations table. The band column is the frozen engine's decision at // evaluation time; this intent NEVER recomputes a score, it reports the distribution of decisions the engine // already made and audited. RLS scopes it to the caller's org, and the requirement filter ($1) scopes it to // one requirement. It JOINs candidates and filters c.deleted_at and c.anonymized_at IS NULL so an erased or // anonymized in place candidate is never counted: anonymize in place keeps the candidate row and its // evaluations (no cascade fires), so without this join an erased person's band would still feed the count. // The band is single valued (each evaluation carries exactly one band), so the cells form a PARTITION whose // counts sum to the live evaluated count. Deterministic ORDER BY on the band label. The executor assembles // the complete {green, yellow, red} partition in Go, filling a true zero for a band with no evaluations. const benchReadinessDistributionSQL = ` SELECT ce.band AS band, count(DISTINCT ce.candidate_id) AS headcount FROM candidate_evaluations ce JOIN candidates c ON c.organization_id = ce.organization_id AND c.id = ce.candidate_id WHERE ce.requirement_id = $1 AND c.deleted_at IS NULL AND c.anonymized_at IS NULL GROUP BY ce.band ORDER BY ce.band ASC` // benchGapForRequirementSQL counts DISTINCT candidates that STRUCTURALLY meet a requirement versus those // that do not, for the caller's org. A candidate MEETS when it holds at least the required clearance level // (the rank ladder through clearance_levels, the same hierarchy the frozen engine encodes: a held rank at or // above the required rank) AND holds at least a fixed fraction (four fifths, the engine's full skills // threshold) of the requirement's required skills. The skill fraction is compared in integer arithmetic // (held matched times five is at least the required total times four, equivalent to held over total at least // four fifths) so NO float touches the test; a requirement with no required skills is vacuously met on the // skills arm. The required total and the held matched count are SCALAR subqueries over allowlisted tables // (required_skills joined to candidate_skills), never a FROM clause set returning function. This is a // STRUCTURAL coverage COUNT to SIZE a gap, explicitly NOT a match score: the per candidate score and the // ranking remain the frozen engine in internal/modules/matching/engine, which is untouched. RLS scopes the // read to the caller's bench and the requirement filter ($1) plus RLS means a foreign org requirement // returns no rows. The soft delete and anonymize filters exclude an erased candidate. Deterministic ORDER BY // on the bucket label, a total order over the two buckets. The executor assembles the complete {meets, // does_not_meet} partition in Go. const benchGapForRequirementSQL = ` SELECT CASE WHEN cl_c.rank >= cl_r.rank AND ( (SELECT count(*) FROM required_skills rs_total WHERE rs_total.requirement_id = r.id) = 0 OR (SELECT count(*) FROM required_skills rs_held JOIN candidate_skills cs ON cs.organization_id = c.organization_id AND cs.candidate_id = c.id AND cs.skill_id = rs_held.skill_id WHERE rs_held.requirement_id = r.id) * 5 >= (SELECT count(*) FROM required_skills rs_req WHERE rs_req.requirement_id = r.id) * 4 ) THEN 'meets' ELSE 'does_not_meet' END AS bucket, count(DISTINCT c.id) AS headcount FROM requirements r CROSS JOIN candidates c JOIN clearance_levels cl_c ON cl_c.code = c.clearance_level JOIN clearance_levels cl_r ON cl_r.code = r.required_level WHERE r.id = $1 AND r.deleted_at IS NULL AND c.deleted_at IS NULL AND c.anonymized_at IS NULL GROUP BY bucket ORDER BY bucket ASC` // BenchSkillRow is one bucket in the skill coverage partition: the bucket label (holds or lacks) and the // suppressed Cell carrying the headcount. The caller renders RenderCount(Cell), never the raw Count, so a // suppressed sub k headcount renders the sentinel and never leaves the process as a number. It carries no // score and no candidate id, only a structural headcount. type BenchSkillRow struct { Bucket string `json:"bucket"` Cell Cell `json:"cell"` } // BenchBandRow is one band in the readiness distribution: the band label and the suppressed Cell carrying // the headcount. The band is the frozen engine's stored decision, never a recomputed score. type BenchBandRow struct { Band string `json:"band"` Cell Cell `json:"cell"` } // BenchGapRow is one bucket in the structural gap partition: the bucket label (meets or does_not_meet) and // the suppressed Cell carrying the headcount. The meets test is structural coverage, never a match score. type BenchGapRow struct { Bucket string `json:"bucket"` Cell Cell `json:"cell"` } // BenchSkillCoverage runs the registered bench_skill_coverage intent under RLS (the app role sees only the // caller's bench), counting DISTINCT candidates that hold a given skill versus those that do not, then pipes // the complete {holds, lacks} partition through Suppress with the caller supplied threshold k (the caller // passes ConfiguredSuppressionThreshold). The two buckets sum to the bench size, so complementary // suppression is well defined. This implements ONLY the partition safe single skill version; a multi skill // ontology family rollup is deferred by design because a family grouping is not a partition (a candidate can // hold many skills, so the cells overlap and the counts do not sum to the bench), which would break the // assumption Suppress's complementary rule relies on. The caller renders RenderCount, never the raw Count, // for a suppressed cell. func BenchSkillCoverage(ctx context.Context, db *tenant.DB, skillID string, k int) ([]BenchSkillRow, error) { if db == nil { return nil, errors.New("ai: no database") } // Seed the complete two bucket partition with true zeros so an empty bucket is present and a suppressed // cell is indistinguishable from an empty one. The SQL fills whichever buckets carry candidates. counts := map[string]int{bucketHolds: 0, bucketLacks: 0} err := db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error { if err := GuardReadOnlyQuery(benchSkillCoverageSQL, ReadAllowlist); err != nil { return err } rows, err := tx.Query(ctx, benchSkillCoverageSQL, skillID) if err != nil { return err } defer rows.Close() for rows.Next() { var ( bucket string headcount int ) if err := rows.Scan(&bucket, &headcount); err != nil { return err } if _, ok := counts[bucket]; ok { counts[bucket] = headcount } } return rows.Err() }) if err != nil { return nil, err } // Build the partition in the fixed, byte stable bucket order, then suppress over the closed set. order := []string{bucketHolds, bucketLacks} out := make([]BenchSkillRow, len(order)) cells := make([]Cell, len(order)) for i, b := range order { cells[i] = Cell{Key: []string{b}, Count: counts[b]} } cells = Suppress(cells, k) for i, b := range order { out[i] = BenchSkillRow{Bucket: b, Cell: cells[i]} } return out, nil } // BenchReadinessDistribution runs the registered bench_readiness_distribution intent under RLS, reading the // ALREADY AUDITED candidate_evaluations band for one requirement and reporting the distribution of bands the // frozen engine ALREADY decided at evaluation time. It NEVER recomputes a score; it counts the decisions the // engine already made. The complete {green, yellow, red} partition is piped through Suppress with the caller // supplied threshold k. Each evaluation carries exactly one band, so the cells sum to the evaluated count and // complementary suppression is well defined. The caller renders RenderCount, never the raw Count, for a // suppressed cell. func BenchReadinessDistribution(ctx context.Context, db *tenant.DB, requirementID string, k int) ([]BenchBandRow, error) { if db == nil { return nil, errors.New("ai: no database") } counts := map[string]int{} for _, b := range readinessBands { counts[b] = 0 } err := db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error { if err := GuardReadOnlyQuery(benchReadinessDistributionSQL, ReadAllowlist); err != nil { return err } rows, err := tx.Query(ctx, benchReadinessDistributionSQL, requirementID) if err != nil { return err } defer rows.Close() for rows.Next() { var ( band string headcount int ) if err := rows.Scan(&band, &headcount); err != nil { return err } if _, ok := counts[band]; ok { counts[band] = headcount } } return rows.Err() }) if err != nil { return nil, err } out := make([]BenchBandRow, len(readinessBands)) cells := make([]Cell, len(readinessBands)) for i, b := range readinessBands { cells[i] = Cell{Key: []string{b}, Count: counts[b]} } cells = Suppress(cells, k) for i, b := range readinessBands { out[i] = BenchBandRow{Band: b, Cell: cells[i]} } return out, nil } // BenchGapForRequirement runs the registered bench_gap_for_requirement intent under RLS, partitioning the // caller's bench into {meets, does not meet} a requirement by a STRUCTURAL coverage test (holds at least the // required clearance level by the rank ladder AND holds at least four fifths of the required skills), then // pipes the complete partition through Suppress with the caller supplied threshold k. This is a structural // COUNT to SIZE a gap, explicitly NOT a match score: the per candidate score and the ranking remain the // frozen engine in internal/modules/matching/engine, which is untouched. Each candidate lands in exactly one // bucket, so the cells sum to the bench size and complementary suppression is well defined. The caller // renders RenderCount, never the raw Count, for a suppressed cell. func BenchGapForRequirement(ctx context.Context, db *tenant.DB, requirementID string, k int) ([]BenchGapRow, error) { if db == nil { return nil, errors.New("ai: no database") } counts := map[string]int{bucketMeets: 0, bucketDoesNotMeet: 0} err := db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error { if err := GuardReadOnlyQuery(benchGapForRequirementSQL, ReadAllowlist); err != nil { return err } rows, err := tx.Query(ctx, benchGapForRequirementSQL, requirementID) if err != nil { return err } defer rows.Close() for rows.Next() { var ( bucket string headcount int ) if err := rows.Scan(&bucket, &headcount); err != nil { return err } if _, ok := counts[bucket]; ok { counts[bucket] = headcount } } return rows.Err() }) if err != nil { return nil, err } order := []string{bucketDoesNotMeet, bucketMeets} out := make([]BenchGapRow, len(order)) cells := make([]Cell, len(order)) for i, b := range order { cells[i] = Cell{Key: []string{b}, Count: counts[b]} } cells = Suppress(cells, k) for i, b := range order { out[i] = BenchGapRow{Bucket: b, Cell: cells[i]} } return out, nil }