Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/curation.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ai/curation.go |
| Original SHA-256 | 4c883ecede481564c7876f0c835feebb064bae90cdf6260e2f244e96df059455 |
| Git blob SHA | 0f15371113504c2de614becd13b297eba7124b61 |
| Reading-copy SHA-256 | 4c883ecede481564c7876f0c835feebb064bae90cdf6260e2f244e96df059455 |
Every recorded occurrence
These 14 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/ai/curation.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓656 lines · 28.7 KB
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
}
}