package ai // curation.go is the WS8 Phase 4 deterministic curation proposal generator (LIVE). It turns one tenant's // recurring, human reviewed reviewer craft into curation_proposals that a human approves, then a SECOND human // verifies, before anything affects a future score. It never changes the engine math, a weight, a band cut, // the 0.62 semantic threshold, or any already served score. Its ONLY downstream effect is, after TWO human // steps, an additive verified skill_relationships edge the resolver reads, which changes FUTURE scores only. // // The generator answers a question the contents free teaching_signal_aggregates rows cannot, because those // rows are by design contents free (a pattern, a band, an outcome, and counts, NEVER a skill id). They tell // us the CROSS CUTTING GOVERNANCE CONFIDENCE: is the adjacent_skill_substitution_outcome pattern reliably // approved at a high band. They do not name a from_skill or a to_skill. So the generator gets the SPECIFIC // recurring approved pairs from a WITHIN TENANT, RLS scoped scan of the tenant's own candidate_evaluations // (its own human reviewed evaluation data, which is allowed in tenant and is reviewed by a human before it // ever becomes a global edge), and proposes a pair ONLY when both hold: the tenant's own evaluations show the // pair was approved repeatedly above a curated threshold, AND the contents free aggregate confirms the // pattern is reliably approved at that band. Under the one tenant posture this is correct and safe: the scan // is RLS scoped, the proposal is a SUGGESTION, and two humans must approve then verify before a score moves. // // The candidate_evaluations.matched_skills jsonb carries the full engine SkillMatch, which has BOTH ids and // names (RequiredSkillID, RequiredSkillName, CandidateSkillID, CandidateName, Similarity, Exact, Matched). So // a recurring approved non exact pair is identified by id directly when the matcher populated ids, and by // resolving the skill name to a global skills.id when only names are present. The proposal's from_skill_id is // the CANDIDATE skill (the substitute the reviewer accepted) and to_skill_id is the REQUIRED skill, so a // later verified substitutes_for edge reads exactly the way the resolver expects (a candidate skill // substitutes for the required skill the candidate did not hold exactly). // // Three correctness boundaries hold by construction, the same three the producer holds: // // 1. The evaluation SCAN is RLS scoped. It runs inside RunInTenantTx on the NOBYPASSRLS app pool, so one // tenant's generator can only ever read its own evaluations. // 2. The proposal WRITE, the idempotency check, and the governance confidence read go through the MIGRATOR // pool seam, never the app role. curation_proposals and teaching_signal_aggregates are global, migrator // owned tables the app role can only SELECT. // 3. The proposal created event runs through audit.Writer.System with a NULL org, because a proposal is a // global object, never a per tenant one. It carries skill ids and counts only, never PII. import ( "context" "encoding/json" "errors" "sort" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/shopspring/decimal" "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" "github.com/missionworkforce/missionmatch/internal/platform/audit" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" "github.com/missionworkforce/missionmatch/internal/platform/scrub" "github.com/missionworkforce/missionmatch/internal/platform/tenant" ) // Curation thresholds keep the few classification rules in one place so the proposal selection is a pure // function of the evidence and the rules are visible for review. const ( // minSubstitutionApprovals is the curated floor on how many times a specific candidate to required pair // must have been APPROVED by a reviewer in this tenant's own evaluations before the generator will // propose a substitutes_for edge. A one off approval is not craft, it is an anecdote, so it never // proposes. Two or more recurring approvals is the floor. minSubstitutionApprovals = 2 // substitutionRelation is the relation a substitutes_for_edge proposal asks for, matching the resolver // vocabulary the engine consults (substitutes_for and broader_than). substitutionRelation = "substitutes_for" // minOverrideApprovals is the curated floor on how many times a clearance gap override was approved in // this tenant before the generator raises a reciprocity_review for human governance. A clearance override // is never turned into a scoring edge; it is surfaced for a human to review the policy, evidence only. minOverrideApprovals = 2 // kindSubstitutesForEdge, kindReciprocityReview, and kindRecalibrationEvidence are the three proposal // kinds, matching the curation_proposals kind CHECK. kindSubstitutesForEdge = "substitutes_for_edge" kindReciprocityReview = "reciprocity_review" kindRecalibrationEvidence = "recalibration_evidence" ) // strengthByBand maps a similarity band to the curated proposed strength of the substitutes_for edge. The // strength is a partial credit weight in the 0.62 to 1.0 range the resolver honors; it is NOT the engine // math, only the strength the resolver multiplies into a partial skill match once a human verifies the edge. // A high band pair earns a stronger curated strength than a medium band one; a low or unknown band never // proposes a substitution at all (see selectSubstitutionProposals), so they are absent here. var strengthByBand = map[scrub.SimilarityBand]string{ scrub.BandHigh: "0.90", scrub.BandMedium: "0.80", } // pairKey identifies one specific recurring substitution: a candidate skill that a reviewer accepted in place // of a required skill the candidate did not hold exactly. Both sides are resolved skill ids. type pairKey struct { fromSkillID string // the CANDIDATE skill (the accepted substitute) toSkillID string // the REQUIRED skill it stood in for } // pairEvidence accumulates the approval and total counts for one pair and the strongest band the reviewers // accepted it at. The band drives the proposed strength; the counts drive the threshold and the evidence // recorded on the proposal. type pairEvidence struct { approvals int total int band scrub.SimilarityBand } // evalRow is the contents free craft evidence the generator scans: the reviewer decision and the persisted // matched skills (which carry the skill ids and names plus the similarity and the exact flag). It is the same // ListSlateDecisionEvidenceForOrgRow shape the producer reads, so both run off one RLS scoped query. type evalRow = dbgen.ListSlateDecisionEvidenceForOrgRow // EvaluationScanner is the RLS scoped read seam for the tenant's own evaluation evidence and the skill name // resolution. The live implementation runs inside RunInTenantTx on the app pool; a test injects a fake so the // derivation and selection logic unit tests with no database. type EvaluationScanner interface { // ScanEvaluations returns the org's slate decisions joined to the persisted match evidence, RLS scoped. ScanEvaluations(ctx context.Context) ([]evalRow, error) // ResolveSkillID resolves a skill name to its global skills.id. It returns ok false when no skill matches, // so a matched_skills entry that carries a name the ontology does not know yields no pair rather than a // fabricated id. The resolution runs on the same app tx (skills is global, app SELECT). ResolveSkillID(ctx context.Context, name string) (uuid.UUID, bool, error) } // GovernanceReader reads the cross cutting governance confidence (the tenant's teaching_signal_aggregates // rows). The live implementation is backed by the MIGRATOR pool (the global table the app role can only // SELECT); a test injects a fake so selection unit tests with no database. type GovernanceReader interface { GovernanceConfidence(ctx context.Context, tenantID uuid.UUID, modelVersion string) ([]dbgen.TeachingSignalAggregate, error) } // systemAuditor is the global (NULL org) audit seam the generator records the proposal created event through. // *audit.Writer satisfies it. It is an interface so the write loop unit tests with a no op auditor and no // database, while the live path records through audit.Writer.System exactly as the invariant requires. type systemAuditor interface { System(ctx context.Context, e audit.Event) error } // *audit.Writer is the live systemAuditor. var _ systemAuditor = (*audit.Writer)(nil) // ProposalWriter is the migrator pool write seam for curation_proposals. The live implementation is backed by // the MIGRATOR pool; a test injects a fake that records the inserts and answers the idempotency check. type ProposalWriter interface { // ActiveProposalExists reports whether a pending or approved proposal already exists for the triple, so the // generator never creates a duplicate suggestion for a pair a human is already deciding or has approved. ActiveProposalExists(ctx context.Context, fromSkillID, toSkillID uuid.UUID, relation string) (bool, error) // InsertProposal creates one pending proposal and returns its id. InsertProposal(ctx context.Context, arg dbgen.InsertCurationProposalParams) (uuid.UUID, error) } // scannerFromTx binds an EvaluationScanner to a tenant transaction, so the RLS scoped read runs on the same // connection whose app.current_org_id GUC RunInTenantTx set. type scannerFromTx struct{ tx pgx.Tx } var _ EvaluationScanner = scannerFromTx{} func (s scannerFromTx) ScanEvaluations(ctx context.Context) ([]evalRow, error) { return dbgen.New(s.tx).ListSlateDecisionEvidenceForOrg(ctx) } func (s scannerFromTx) ResolveSkillID(ctx context.Context, name string) (uuid.UUID, bool, error) { id, err := dbgen.New(s.tx).ResolveSkillIDByName(ctx, name) if errors.Is(err, pgx.ErrNoRows) { return uuid.Nil, false, nil } if err != nil { return uuid.Nil, false, err } return id, true, nil } // migratorGovernanceReader backs GovernanceReader with the migrator pool. type migratorGovernanceReader struct{ db dbgen.DBTX } func (g migratorGovernanceReader) GovernanceConfidence(ctx context.Context, tenantID uuid.UUID, modelVersion string) ([]dbgen.TeachingSignalAggregate, error) { return dbgen.New(g.db).ListTeachingAggregatesForTenant(ctx, dbgen.ListTeachingAggregatesForTenantParams{ TenantID: &tenantID, ModelVersion: modelVersion, }) } // NewMigratorGovernanceReader wraps a migrator scoped dbgen.DBTX as the governance confidence reader. The // caller passes the migrator pool here, never the app pool, because teaching_signal_aggregates is a global // table the app role can only SELECT. func NewMigratorGovernanceReader(migrator dbgen.DBTX) GovernanceReader { return migratorGovernanceReader{db: migrator} } // migratorProposalWriter backs ProposalWriter with the migrator pool. type migratorProposalWriter struct{ db dbgen.DBTX } func (w migratorProposalWriter) ActiveProposalExists(ctx context.Context, fromSkillID, toSkillID uuid.UUID, relation string) (bool, error) { _, err := dbgen.New(w.db).GetActiveProposalForPair(ctx, dbgen.GetActiveProposalForPairParams{ FromSkillID: &fromSkillID, ToSkillID: &toSkillID, Relation: &relation, }) if errors.Is(err, pgx.ErrNoRows) { return false, nil } if err != nil { return false, err } return true, nil } func (w migratorProposalWriter) InsertProposal(ctx context.Context, arg dbgen.InsertCurationProposalParams) (uuid.UUID, error) { row, err := dbgen.New(w.db).InsertCurationProposal(ctx, arg) if err != nil { return uuid.Nil, err } return row.ID, nil } // NewMigratorProposalWriter wraps a migrator scoped dbgen.DBTX as the proposal writer. The caller passes the // migrator pool here, never the app pool, because curation_proposals is a global table the app role can only // SELECT. func NewMigratorProposalWriter(migrator dbgen.DBTX) ProposalWriter { return migratorProposalWriter{db: migrator} } // ProposalGenerator turns one tenant's recurring approved reviewer craft into pending curation_proposals. The // evaluation scan is RLS scoped on the app pool; the governance read and the proposal write go through the // migrator pool; the proposal created event is a global System audit. type ProposalGenerator struct { app AppReader // app pool RunInTenantTx, the RLS scoped evaluation scan boundary governance GovernanceReader // migrator pool, the contents free governance confidence read writer ProposalWriter // migrator pool, the proposal write and idempotency seam audit systemAuditor // audit.Writer.System for the global proposal created event } // NewProposalGenerator wires the live generator. app is the app pool (RLS scoped evaluation scans), governance // and writer are backed by the MIGRATOR pool (the global tables), and auditW records the global proposal // created event through System. func NewProposalGenerator(app AppReader, governance GovernanceReader, writer ProposalWriter, auditW systemAuditor) *ProposalGenerator { return &ProposalGenerator{app: app, governance: governance, writer: writer, audit: auditW} } // ProposalResult reports what one GenerateProposalsForOrg run produced: how many evaluations it scanned and // the proposals it created (by kind). It carries counts and skill ids only, never PII, so it is safe to log. type ProposalResult struct { EvaluationsScanned int Created int SubstitutionEdges int ReciprocityReviews int RecalibrationRows int } // GenerateProposalsForOrg scans the org's own evaluations for recurring approved craft and emits pending // curation_proposals. The org is taken from ctx (the validated tenant), never an argument. // // Order of operations: // 1. Inside RunInTenantTx on the app pool (RLS scoped), scan the org's slate decisions joined to the // persisted match evidence and resolve each pair's skill ids (by id when the matcher populated them, by // name through the global skills table otherwise). This is the ONLY app pool step. // 2. Read the contents free governance confidence (the tenant's teaching_signal_aggregates) through the // migrator pool. // 3. Select the pairs that clear the curated threshold AND are supported by the governance confidence, then // insert each as a pending substitutes_for_edge proposal through the migrator pool, idempotently (never a // duplicate for a pair already pending or approved). Audit teaching.proposal.created through System. // 4. Emit reciprocity_review proposals for recurring approved clearance gap overrides, and one // recalibration_evidence proposal carrying the aggregate counts for WS4 (evidence only, never a threshold // change), the same idempotent and audited way. func (p *ProposalGenerator) GenerateProposalsForOrg(ctx context.Context) (ProposalResult, error) { orgID, ok := tenant.OrgFromContext(ctx) if !ok { return ProposalResult{}, tenant.ErrNoTenant } tenantUUID, err := parseOrgUUID(orgID) if err != nil { return ProposalResult{}, err } // 1. RLS scoped evaluation scan plus the skill id resolution, all on the app tx. var rows []evalRow var pairs map[pairKey]*pairEvidence var overrideApprovals int if rerr := p.app.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error { s := scannerFromTx{tx: tx} var serr error rows, serr = s.ScanEvaluations(ctx) if serr != nil { return serr } pairs, serr = derivePairs(ctx, rows, s) if serr != nil { return serr } overrideApprovals = countClearanceOverrideApprovals(rows) return nil }); rerr != nil { return ProposalResult{}, rerr } // 2. The contents free governance confidence (migrator pool, the global table). aggregates, gerr := p.governance.GovernanceConfidence(ctx, tenantUUID, engine.ModelVersion) if gerr != nil { return ProposalResult{}, gerr } // 3 and 4. Pure selection over the derived pairs and the governance confidence, then the idempotent, // audited write of each proposal through the migrator pool. plans := selectProposals(pairs, overrideApprovals, aggregates) res := ProposalResult{EvaluationsScanned: len(rows)} for _, plan := range plans { created, werr := p.writeProposal(ctx, plan) if werr != nil { return res, werr } if !created { continue } res.Created++ switch plan.kind { case kindSubstitutesForEdge: res.SubstitutionEdges++ case kindReciprocityReview: res.ReciprocityReviews++ case kindRecalibrationEvidence: res.RecalibrationRows++ } } return res, nil } // proposalPlan is one candidate proposal the pure selection produced, before the idempotency check and the // write. A substitutes_for_edge plan carries the resolved from and to skill ids and the band derived strength; // the reciprocity_review and recalibration_evidence plans carry counts only, no skill ids. type proposalPlan struct { kind string fromSkillID *uuid.UUID toSkillID *uuid.UUID relation string strength *decimal.Decimal observations int successes int } // writeProposal performs the idempotency check and the insert for one plan through the migrator pool, then // emits the global teaching.proposal.created audit. It returns created false (with no error) when an active // proposal already exists for the pair, so a rerun never duplicates a suggestion. The audit carries skill ids // and counts only, NEVER PII, and runs through System with a NULL org because a proposal is a global object. func (p *ProposalGenerator) writeProposal(ctx context.Context, plan proposalPlan) (bool, error) { // Idempotency only applies to a skill pair (a substitutes_for_edge). The evidence only kinds // (reciprocity_review, recalibration_evidence) carry no pair, so there is nothing to dedupe on a pair key; // the per run window scoping in the caller keeps them from multiplying. if plan.kind == kindSubstitutesForEdge && plan.fromSkillID != nil && plan.toSkillID != nil { exists, err := p.writer.ActiveProposalExists(ctx, *plan.fromSkillID, *plan.toSkillID, plan.relation) if err != nil { return false, err } if exists { return false, nil } } var relation *string if plan.relation != "" { r := plan.relation relation = &r } id, err := p.writer.InsertProposal(ctx, dbgen.InsertCurationProposalParams{ Kind: plan.kind, FromSkillID: plan.fromSkillID, ToSkillID: plan.toSkillID, Relation: relation, ProposedStrength: plan.strength, EvidenceObservations: int32(plan.observations), EvidenceSuccesses: int32(plan.successes), ModelVersion: engine.ModelVersion, }) if err != nil { return false, err } payload, merr := json.Marshal(map[string]any{ "kind": plan.kind, "relation": plan.relation, "evidence_observations": plan.observations, "evidence_successes": plan.successes, }) if merr != nil { return false, merr } if aerr := p.audit.System(ctx, audit.Event{ Action: "teaching.proposal.created", EntityType: "curation_proposal", EntityID: id.String(), NewValues: payload, }); aerr != nil { return false, aerr } return true, nil } // matchedSkillFull mirrors the full closed subset of engine.SkillMatch the generator needs to identify a // specific pair. The engine type has no json tags, so the persisted jsonb keys are the Go field names; these // tags pin those names. The generator reads the ids first and falls back to the names, never any other field. type matchedSkillFull struct { RequiredSkillID string `json:"RequiredSkillID"` RequiredSkillName string `json:"RequiredSkillName"` CandidateSkillID string `json:"CandidateSkillID"` CandidateName string `json:"CandidateName"` Similarity decimal.Decimal `json:"Similarity"` Exact bool `json:"Exact"` Matched bool `json:"Matched"` } // derivePairs scans the evaluation rows for recurring APPROVED non exact (required skill, candidate skill) // pairs and counts, per pair, how many times it was approved and how many times it was seen at all, tracking // the strongest band the reviewers accepted it at. It resolves each side to a global skills.id: by id when // the matcher populated the SkillMatch ids, and by resolving the name through the skills table otherwise. A // pair that cannot be resolved to two ids is skipped (never a fabricated id). An exact match is never a // substitution, so it is skipped here. derivePairs runs on the app tx because the name resolution reads the // global skills table on the same connection; the counting itself is pure. func derivePairs(ctx context.Context, rows []evalRow, scanner EvaluationScanner) (map[pairKey]*pairEvidence, error) { out := map[pairKey]*pairEvidence{} // resolveCache memoizes name to id resolution across rows so one skill name resolves once per run. resolveCache := map[string]*uuid.UUID{} resolve := func(id, name string) (string, error) { if id != "" { return id, nil } if name == "" { return "", nil } if cached, ok := resolveCache[name]; ok { if cached == nil { return "", nil } return cached.String(), nil } rid, found, err := scanner.ResolveSkillID(ctx, name) if err != nil { return "", err } if !found { resolveCache[name] = nil return "", nil } resolveCache[name] = &rid return rid.String(), nil } for _, row := range rows { outcome, ok := outcomeFromDecision(row.Decision) if !ok { continue } approved := outcome == scrub.OutcomeApproved for _, sm := range parseMatchedSkillsFull(row.MatchedSkills) { if !sm.Matched || sm.Exact { continue // only a non exact accepted substitution is a candidate pair } fromID, err := resolve(sm.CandidateSkillID, sm.CandidateName) if err != nil { return nil, err } toID, err := resolve(sm.RequiredSkillID, sm.RequiredSkillName) if err != nil { return nil, err } if fromID == "" || toID == "" || fromID == toID { continue // unresolved or a self pair is never a substitution } k := pairKey{fromSkillID: fromID, toSkillID: toID} ev := out[k] if ev == nil { ev = &pairEvidence{band: scrub.BandUnknown} out[k] = ev } ev.total++ if approved { ev.approvals++ if band := bandFromSimilarity(sm.Similarity); bandRank(band) > bandRank(ev.band) { ev.band = band // track the strongest band an approval was accepted at } } } } return out, nil } // countClearanceOverrideApprovals counts how many slate decisions in this tenant approved a candidate whose // engine clearance_score was zero against a required clearance, which is a reviewer overriding a clearance // gap. It is contents free (a count, no candidate id), and it feeds the reciprocity_review proposal, which is // evidence for a human governance review, never a scoring edge. func countClearanceOverrideApprovals(rows []evalRow) int { n := 0 for _, row := range rows { outcome, ok := outcomeFromDecision(row.Decision) if !ok || outcome != scrub.OutcomeApproved { continue } if clearanceRequired(row.RequiredLevel) && row.ClearanceScore.IsZero() { n++ } } return n } // selectProposals is the pure selection over the derived pairs, the clearance override count, and the // governance confidence. It is a function of the evidence and the curated thresholds, so it unit tests with // fixtures and no DB. // // For a substitutes_for_edge it requires THREE things to all hold: // - the pair was approved at least minSubstitutionApprovals times in this tenant's own evaluations, // - the strongest accepted band is high or medium (a low or unknown band similarity is too weak to curate), // - the contents free governance confidence confirms the adjacent_skill_substitution_outcome pattern is // reliably approved at that band (the aggregate has more approved than not at the band). // // That double gate is the design correction: the specific pair comes from the tenant's own reviewed // evaluations, and the cross cutting reliability comes from the contents free aggregate. Neither alone // proposes an edge. func selectProposals(pairs map[pairKey]*pairEvidence, overrideApprovals int, aggregates []dbgen.TeachingSignalAggregate) []proposalPlan { var plans []proposalPlan // Index the governance confidence by band for the substitution pattern, so the lookup is O(1) per pair. subConfidence := substitutionConfidenceByBand(aggregates) // Deterministic order: sort the pair keys so the proposals are stable across runs. keys := make([]pairKey, 0, len(pairs)) for k := range pairs { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { if keys[i].fromSkillID != keys[j].fromSkillID { return keys[i].fromSkillID < keys[j].fromSkillID } return keys[i].toSkillID < keys[j].toSkillID }) for _, k := range keys { ev := pairs[k] if ev.approvals < minSubstitutionApprovals { continue } strengthStr, bandable := strengthByBand[ev.band] if !bandable { continue // a low or unknown band is too weak to curate as a substitution } if !subConfidence[ev.band] { continue // the contents free aggregate does not support the pattern at this band } from, err1 := uuid.Parse(k.fromSkillID) to, err2 := uuid.Parse(k.toSkillID) if err1 != nil || err2 != nil { continue // a malformed id never proposes } strength := decimal.RequireFromString(strengthStr) plans = append(plans, proposalPlan{ kind: kindSubstitutesForEdge, fromSkillID: &from, toSkillID: &to, relation: substitutionRelation, strength: &strength, observations: ev.total, successes: ev.approvals, }) } // A reciprocity_review for recurring approved clearance gap overrides. Evidence only, never a scoring // edge: it asks a human to review whether the tenant's clearance policy and the reviewers' overrides are // reciprocal. It carries no skill ids, only the counts. if overrideApprovals >= minOverrideApprovals { plans = append(plans, proposalPlan{ kind: kindReciprocityReview, relation: "", observations: overrideApprovals, successes: overrideApprovals, }) } // One recalibration_evidence proposal carrying the reranker agreement aggregate when present, for WS4. // Evidence only: it never changes a threshold automatically, it gives a human the counts to recalibrate. if obs, succ, ok := recalibrationEvidence(aggregates); ok { plans = append(plans, proposalPlan{ kind: kindRecalibrationEvidence, relation: "", observations: obs, successes: succ, }) } return plans } // substitutionConfidenceByBand reduces the governance aggregates to a per band yes or no: is the // adjacent_skill_substitution_outcome pattern reliably approved at the band (more approved observations than // not). It is the contents free reliability check that gates a specific pair from the tenant's own // evaluations. func substitutionConfidenceByBand(aggregates []dbgen.TeachingSignalAggregate) map[scrub.SimilarityBand]bool { approved := map[scrub.SimilarityBand]int{} other := map[scrub.SimilarityBand]int{} for _, a := range aggregates { if a.Pattern != "adjacent_skill_substitution_outcome" { continue } band := scrub.SimilarityBand(a.Band) if a.Outcome == string(scrub.OutcomeApproved) { approved[band] += int(a.Successes) } else { other[band] += int(a.Observations) } } out := map[scrub.SimilarityBand]bool{} for band, n := range approved { out[band] = n > 0 && n >= other[band] } return out } // recalibrationEvidence sums the reranker_label_agreement aggregate counts across bands, when any exist, for a // single recalibration_evidence proposal. It returns ok false when there is no reranker agreement aggregate, // so no empty evidence proposal is created. The reranker pattern folds nothing today (no stored reranker // label), so this is dormant until that evidence exists, exactly like the producer's fold. func recalibrationEvidence(aggregates []dbgen.TeachingSignalAggregate) (observations, successes int, ok bool) { for _, a := range aggregates { if a.Pattern != "reranker_label_agreement" { continue } observations += int(a.Observations) successes += int(a.Successes) ok = true } return observations, successes, ok } // parseMatchedSkillsFull decodes the candidate_evaluations.matched_skills jsonb into the full closed subset // the generator needs (the ids and names plus the similarity and the flags). A NULL or empty column yields no // skills, and unreadable bytes are treated as absent, never fabricated, the same posture as the producer. func parseMatchedSkillsFull(raw []byte) []matchedSkillFull { if len(raw) == 0 { return nil } var skills []matchedSkillFull if err := json.Unmarshal(raw, &skills); err != nil { return nil } return skills } // bandRank orders the similarity bands so derivePairs can track the strongest band an approval was accepted // at. Higher is stronger. Unknown and low rank lowest; exact ranks highest but never reaches here for a non // exact substitution. func bandRank(b scrub.SimilarityBand) int { switch b { case scrub.BandExact: return 4 case scrub.BandHigh: return 3 case scrub.BandMedium: return 2 case scrub.BandLow: return 1 default: return 0 } }