Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/teaching.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ai/teaching.go |
| Original SHA-256 | 89f2b3af1f9f381e7a94b5d9edf1405a88cda581767293bbf0413652b02adffc |
| Git blob SHA | 049865eaa04676b3ba514265418b84e05933ddfc |
| Reading-copy SHA-256 | 89f2b3af1f9f381e7a94b5d9edf1405a88cda581767293bbf0413652b02adffc |
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/teaching.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / internal/modules/ai/teaching.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓447 lines · 21.3 KB
package ai
// teaching.go is the WS8 in tenant teaching producer (Phase 1, LIVE). It reads one tenant's reviewer craft
// (the slate decisions and the frozen match evidence) and folds it into the closed scrub.TeachingSignal
// vocabulary of contents free counts, so MEGAMIND can learn the CRAFT of the work (abstract patterns) and
// never a client fact. It is LIVE in the sense that it runs in production and persists the per tenant
// aggregates, but transmission to the inference plane stays gated off by TeachingEnabled until multiple
// live tenants exist, so nothing leaves the box yet.
//
// The producer never touches the engine math. It reads what the frozen engine already wrote (the decision,
// the sub scores, the gap summary, the matched skills) and counts it. It never changes a weight, a band
// cut, the semantic threshold, or any served score.
//
// Three correctness boundaries hold by construction:
//
// 1. The READ is RLS scoped. It runs inside RunInTenantTx on the NOBYPASSRLS app pool, so Postgres Row
// Level Security restricts the join to the caller's own org. One tenant's producer can never read
// another tenant's decisions.
// 2. The aggregate WRITE goes through the MIGRATOR pool seam, never the app role. teaching_signal_aggregates
// is a global, migrator owned table the app role can only SELECT, so the live in tenant rows are written
// through an injected Writer backed by the migrator pool.
// 3. The audit event runs through audit.Writer.OnTx INSIDE the same tenant tx that read the decisions, so
// it commits with the read and carries the correct org. It carries counts only, no PII, no candidate id,
// and no raw score.
//
// Everything score affecting is deferred to the two human step curation pipeline (Phase 4); this producer
// only aggregates evidence.
import (
"context"
"encoding/json"
"sort"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"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/inference"
"github.com/missionworkforce/missionmatch/internal/platform/scrub"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// init registers the live engine model version with the scrub boundary so a signal stamped with
// engine.ModelVersion is accepted, and a drifted version is refused the same way an unknown Pattern is. The
// version is pinned from the engine package (never a literal), the only module here allowed to import it.
func init() {
scrub.RegisterModelVersion(engine.ModelVersion)
}
// teachingThresholds keeps the few derived classification rules in one place so the folding logic is a pure
// function of the stored evidence and the rules are visible in one spot for review.
const (
// clearanceRequiredNone and clearanceRequiredUnknown are the two required_level values that mean no
// clearance is required (matching engine.scoreClearance, which passes everyone below Confidential). Any
// other required_level means a clearance was demanded, so a zero clearance_score on an approved member is
// a reviewer override of a clearance gap.
clearanceRequiredNone = "none"
clearanceRequiredUnknown = "unknown"
)
// DecisionRow is the contents free craft evidence for one slate member: the reviewer decision, the stored
// sub scores that reveal a clearance gap or an experience shortfall, the frozen gap summary, the required
// clearance level, and the matched skills evidence from the candidate's persisted evaluation. It is the
// shape ListSlateDecisionEvidenceForOrgRow already returns; the producer reads only these closed fields and
// never a name, an email, or a resume.
type DecisionRow = dbgen.ListSlateDecisionEvidenceForOrgRow
// DecisionReader is the RLS scoped read seam. The live implementation runs inside RunInTenantTx on the app
// pool; a test injects a fake returning fixture rows so the folding logic unit tests with no database.
type DecisionReader interface {
ListDecisionEvidence(ctx context.Context) ([]DecisionRow, error)
}
// AggregateWriter is the migrator pool write seam for the global teaching_signal_aggregates table. The live
// implementation is backed by the migrator pool (the app role has SELECT only on this global table); a test
// injects a fake that records the upserts so the producer can be exercised with no database.
type AggregateWriter interface {
UpsertTeachingSignalAggregate(ctx context.Context, arg dbgen.UpsertTeachingSignalAggregateParams) error
}
// AppReader is the app pool RunInTenantTx seam. *tenant.DB satisfies it. It is an interface so the audit on
// the same tenant tx can be exercised, while the folding logic itself unit tests through the DecisionReader.
type AppReader interface {
RunInTenantTx(ctx context.Context, fn func(ctx context.Context, tx pgx.Tx) error) error
}
// *tenant.DB is the live AppReader, a compile time assertion that the app pool satisfies the read seam.
var _ AppReader = (*tenant.DB)(nil)
// readerFromTx builds a DecisionReader bound to a tenant transaction, so the RLS scoped read runs on the
// same connection whose app.current_org_id GUC RunInTenantTx set.
type readerFromTx struct{ tx pgx.Tx }
// readerFromTx is the live DecisionReader: a compile time assertion that the tenant tx backed reader honors
// the seam the producer reads through.
var _ DecisionReader = readerFromTx{}
func (r readerFromTx) ListDecisionEvidence(ctx context.Context) ([]DecisionRow, error) {
return dbgen.New(r.tx).ListSlateDecisionEvidenceForOrg(ctx)
}
// migratorAggregateWriter backs AggregateWriter with the migrator pool. It is a thin adapter so the
// producer depends on the interface, not the concrete dbgen DBTX, and a test can substitute a fake.
type migratorAggregateWriter struct{ db dbgen.DBTX }
func (w migratorAggregateWriter) UpsertTeachingSignalAggregate(ctx context.Context, arg dbgen.UpsertTeachingSignalAggregateParams) error {
return dbgen.New(w.db).UpsertTeachingSignalAggregate(ctx, arg)
}
// NewMigratorAggregateWriter wraps a migrator scoped dbgen.DBTX (the migrator pool) as the global aggregate
// writer. The caller passes the migrator pool here, never the app pool, because the app role cannot write
// the global teaching_signal_aggregates table.
func NewMigratorAggregateWriter(migrator dbgen.DBTX) AggregateWriter {
return migratorAggregateWriter{db: migrator}
}
// TeachingProducer folds one tenant's reviewer craft into scrub.TeachingSignal counts and persists the live
// in tenant aggregates. It is LIVE but transmission stays gated off by the inference client's TeachingEnabled
// flag, so calling Teach validates the boundary end to end (scrub.Marshal) without emitting anything off box.
type TeachingProducer struct {
app AppReader // app pool RunInTenantTx, the RLS scoped read boundary
writer AggregateWriter // migrator pool, the global aggregate write seam (never the app role)
audit *audit.Writer // audit.Writer.OnTx on the tenant tx
teacher *inference.Client // Teach, gated off by TeachingEnabled
now func() time.Time // injectable clock so the window dates are deterministic in tests
}
// NewTeachingProducer wires the live producer. app is the app pool (RLS scoped reads), writer is backed by
// the MIGRATOR pool (global aggregate writes), auditW records the per tenant event on the tenant tx, and
// teacher is the inference client (Teach stays gated off by TeachingEnabled).
func NewTeachingProducer(app AppReader, writer AggregateWriter, auditW *audit.Writer, teacher *inference.Client) *TeachingProducer {
return &TeachingProducer{app: app, writer: writer, audit: auditW, teacher: teacher, now: time.Now}
}
// Result reports what one AggregateForOrg run folded: how many slate decisions it read and the signals it
// produced. It carries counts only, never a candidate id or a score, so it is safe to log.
type Result struct {
DecisionsRead int
Signals []scrub.TeachingSignal
}
// AggregateForOrg reads the org's slate decisions and folds them into per tenant craft aggregates. The org
// is taken from ctx (the validated tenant), never an argument.
//
// Order of operations:
// 1. Inside RunInTenantTx on the app pool (RLS scoped), read the decision evidence and emit the
// teaching.signal.aggregated audit on the SAME tenant tx, so the audit commits with the read.
// 2. After the read commits, fold the rows into scrub.TeachingSignal counts (a pure function).
// 3. For each signal, call inference.Client.Teach (boundary checked, transmission gated off).
// 4. Persist each signal into teaching_signal_aggregates through the MIGRATOR pool writer, tenant_id set,
// dp_applied false (the live in tenant rows). The app role cannot write this global table.
func (p *TeachingProducer) AggregateForOrg(ctx context.Context) (Result, error) {
orgID, ok := tenant.OrgFromContext(ctx)
if !ok {
return Result{}, tenant.ErrNoTenant
}
// 1. RLS scoped read plus the audit on the SAME tenant tx (invariant 5: the per tenant event commits
// with the read it records and carries the correct org).
var rows []DecisionRow
err := p.app.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var rerr error
rows, rerr = readerFromTx{tx: tx}.ListDecisionEvidence(ctx)
if rerr != nil {
return rerr
}
// Counts only in the audit payload: how many decisions this run aggregated. No candidate id, no
// score, no name. The org is read from ctx by OnTx, never passed here.
newVals, merr := json.Marshal(map[string]int{"decisions_aggregated": len(rows)})
if merr != nil {
return merr
}
return p.audit.OnTx(ctx, tx, audit.Event{
Action: "teaching.signal.aggregated",
EntityType: "teaching_signal",
NewValues: newVals,
})
})
if err != nil {
return Result{}, err
}
// 2. Fold the evidence into the closed craft vocabulary (a pure function of the rows plus the window).
signals := fold(rows, engine.ModelVersion)
// 3 and 4. Exercise the scrub boundary through Teach (gated off, emits nothing) and persist the live in
// tenant aggregates through the migrator pool writer. Factored out so the teach and persist loop
// unit tests with fakes and no database, while the tenant tx read and audit above are covered by
// the integration test.
if perr := p.persist(ctx, orgID, signals); perr != nil {
return Result{}, perr
}
return Result{DecisionsRead: len(rows), Signals: signals}, nil
}
// persist runs the per signal teach and write loop: for each folded signal it calls inference.Client.Teach
// (which validates the scrub boundary and, with TeachingEnabled false, transmits nothing) and then upserts
// the live in tenant aggregate through the migrator pool writer with tenant_id set and dp_applied false. It
// touches no app pool and no tenant tx, so it is safe to unit test with a fake writer and a gated off teacher.
func (p *TeachingProducer) persist(ctx context.Context, orgID string, signals []scrub.TeachingSignal) error {
tenantUUID, perr := parseOrgUUID(orgID)
if perr != nil {
return perr
}
start, end := p.window()
for _, sig := range signals {
if p.teacher != nil {
if terr := p.teacher.Teach(ctx, sig); terr != nil {
// Teach validates the boundary (scrub.Marshal). A failure means the producer built a signal
// the boundary refuses, which is a bug to surface, not to swallow. Transmission is gated off.
return terr
}
}
if werr := p.writer.UpsertTeachingSignalAggregate(ctx, dbgen.UpsertTeachingSignalAggregateParams{
TenantID: &tenantUUID, // live in tenant row: tenant_id is set
DpApplied: false, // not the cross tenant post DP path
Pattern: sig.Pattern,
Band: string(sig.Band),
Outcome: string(sig.Outcome),
ModelVersion: sig.ModelVersion,
Observations: int32(sig.Observations),
Successes: int32(sig.Successes),
WindowStart: pgDate(start),
WindowEnd: pgDate(end),
}); werr != nil {
return werr
}
}
return nil
}
// foldKey identifies one aggregate bucket: a craft pattern at a similarity band for a decision outcome. The
// producer counts observations and successes per key, then turns each key into one scrub.TeachingSignal.
type foldKey struct {
pattern string
band scrub.SimilarityBand
outcome scrub.Outcome
}
type foldCount struct {
observations int
successes int
}
// fold is the pure folding logic, a function of the stored decision evidence and the model version. It has
// no DB, no clock, and no I/O, so it unit tests with fixture rows.
//
// It folds into the four closed scrub.Patterns. Which patterns are derivable from the stored evidence today:
//
// - adjacent_skill_substitution_outcome: DERIVABLE. candidate_evaluations.matched_skills carries each
// required to candidate SkillMatch with its Similarity and an Exact flag (engine SkillMatch, no json
// tags so the keys are the Go field names). A match with Matched true and Exact false is an adjacent or
// semantic substitution. The Band comes from the cosine bucket of its Similarity, the Outcome from the
// slate decision. A member with no stored evaluation (LEFT join NULL) contributes nothing here, which is
// correct, the producer never fabricates a substitution it cannot see.
//
// - clearance_gap_override_outcome: DERIVABLE. A clearance was required when required_level is neither
// none nor unknown (matching engine.scoreClearance). A zero clearance_score on such a member is the
// engine recording an unmet clearance, so a member with a required clearance and clearance_score zero is
// a clearance gap; the reviewer decision generalizes whether they overrode it. The Band is unknown,
// because clearance carries no similarity.
//
// - experience_shortfall_outcome: DERIVABLE. The engine writes experience_score below 100 when the
// candidate has fewer years than required (engine.scoreExperience). An experience shortfall is therefore
// a member with experience_score below 100; the decision generalizes how reviewers treat it. Note the
// engine does NOT add an experience entry to gap_summary (it appends only clearance, skill, and cert
// gaps), so the score is the authoritative signal, not the gap summary text. The Band is unknown.
//
// - reranker_label_agreement: ZERO TODAY. No reranker label and no reranker agreement is persisted with a
// slate decision or a candidate evaluation. The inference reranker (inference.Client.Rerank) is not
// stored alongside the frozen decision, so there is no stored evidence to fold. The producer derives
// nothing for this pattern and emits no signal for it, rather than fabricate a count. When a reranker
// label is persisted in a later phase, this fold gains a real branch.
func fold(rows []DecisionRow, modelVersion string) []scrub.TeachingSignal {
counts := map[foldKey]*foldCount{}
add := func(pattern string, band scrub.SimilarityBand, outcome scrub.Outcome, success bool) {
k := foldKey{pattern: pattern, band: band, outcome: outcome}
c := counts[k]
if c == nil {
c = &foldCount{}
counts[k] = c
}
c.observations++
if success {
c.successes++
}
}
for _, row := range rows {
outcome, ok := outcomeFromDecision(row.Decision)
if !ok {
continue // an out of vocabulary decision is skipped rather than folded into a wrong bucket
}
// A success is the reviewer approving the candidate despite (or with) the observed evidence; it is
// the positive class a craft signal generalizes over.
approved := outcome == scrub.OutcomeApproved
// adjacent_skill_substitution_outcome: one observation per non exact matched skill, banded by its
// similarity. A member with no stored evaluation contributes none.
for _, sm := range parseMatchedSkills(row.MatchedSkills) {
if sm.Matched && !sm.Exact {
add("adjacent_skill_substitution_outcome", bandFromSimilarity(sm.Similarity), outcome, approved)
}
}
// clearance_gap_override_outcome: a required clearance the engine scored zero is a clearance gap.
if clearanceRequired(row.RequiredLevel) && row.ClearanceScore.IsZero() {
add("clearance_gap_override_outcome", scrub.BandUnknown, outcome, approved)
}
// experience_shortfall_outcome: an experience_score below 100 is a years shortfall.
if row.ExperienceScore.LessThan(decimal.NewFromInt(100)) {
add("experience_shortfall_outcome", scrub.BandUnknown, outcome, approved)
}
// reranker_label_agreement: no stored evidence today, intentionally folds nothing (see the doc above).
}
return signalsFromCounts(counts, modelVersion)
}
// signalsFromCounts turns the folded buckets into scrub.TeachingSignals in a deterministic order (by
// pattern, then band, then outcome), so the producer output and the persisted rows are stable across runs.
func signalsFromCounts(counts map[foldKey]*foldCount, modelVersion string) []scrub.TeachingSignal {
keys := make([]foldKey, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].pattern != keys[j].pattern {
return keys[i].pattern < keys[j].pattern
}
if keys[i].band != keys[j].band {
return keys[i].band < keys[j].band
}
return keys[i].outcome < keys[j].outcome
})
out := make([]scrub.TeachingSignal, 0, len(keys))
for _, k := range keys {
c := counts[k]
out = append(out, scrub.TeachingSignal{
Pattern: k.pattern,
Band: k.band,
Outcome: k.outcome,
Observations: c.observations,
Successes: c.successes,
ModelVersion: modelVersion,
})
}
return out
}
// matchedSkill mirrors the closed subset of engine.SkillMatch the producer needs. The engine type has no
// json tags, so the persisted jsonb keys are the Go field names; these tags pin those names so the producer
// is independent of the engine package's json encoding and reads only the evidence fields it folds.
type matchedSkill struct {
Similarity decimal.Decimal `json:"Similarity"`
Exact bool `json:"Exact"`
Matched bool `json:"Matched"`
}
// parseMatchedSkills decodes the candidate_evaluations.matched_skills jsonb. A NULL column (a slate member
// never screened through screenMATCH) or an empty array yields no skills, so the producer folds nothing for
// such a member rather than guess.
func parseMatchedSkills(raw []byte) []matchedSkill {
if len(raw) == 0 {
return nil
}
var skills []matchedSkill
if err := json.Unmarshal(raw, &skills); err != nil {
return nil // unreadable evidence is treated as absent, never fabricated
}
return skills
}
// outcomeFromDecision maps the slate_members.decision vocabulary to the scrub.Outcome vocabulary. They are
// the same three labels by construction (the scrub Outcome constants were taken verbatim from
// slate_members.decision), so this is a checked passthrough that refuses an unexpected label.
func outcomeFromDecision(decision string) (scrub.Outcome, bool) {
switch decision {
case "approved":
return scrub.OutcomeApproved, true
case "rejected":
return scrub.OutcomeRejected, true
case "pending":
return scrub.OutcomePending, true
default:
return "", false
}
}
// bandFromSimilarity buckets a cosine similarity into the coarse scrub.SimilarityBand, the public
// quantization the teaching boundary speaks in (never the raw score). The cut points match the scrub band
// documentation: exact at 1.00, high 0.90 to 0.99, medium 0.80 to 0.89, low below 0.80. A negative or
// missing similarity is unknown.
func bandFromSimilarity(sim decimal.Decimal) scrub.SimilarityBand {
switch {
case sim.GreaterThanOrEqual(decimal.NewFromInt(1)):
return scrub.BandExact
case sim.GreaterThanOrEqual(decimal.RequireFromString("0.90")):
return scrub.BandHigh
case sim.GreaterThanOrEqual(decimal.RequireFromString("0.80")):
return scrub.BandMedium
case sim.GreaterThanOrEqual(decimal.Zero):
return scrub.BandLow
default:
return scrub.BandUnknown
}
}
// clearanceRequired reports whether the requirement demanded a clearance, matching engine.scoreClearance
// which passes everyone when the required level is below Confidential (the none and unknown labels).
func clearanceRequired(requiredLevel string) bool {
return requiredLevel != clearanceRequiredNone && requiredLevel != clearanceRequiredUnknown
}
// window returns the observation window the producer stamps on each aggregate. v1 aggregates the full
// stored history into one open ended window ending today, so a rerun refreshes the same window row in place
// (the upsert key includes window_start). The window dates carry no client fact, only the run date.
func (p *TeachingProducer) window() (start, end time.Time) {
now := p.now().UTC()
// A fixed epoch start keeps window_start stable across reruns so the upsert key is stable; the end moves
// to the run date. This is the all history window for v1, not a rolling window.
start = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
end = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
return start, end
}
// pgDate wraps a date for the date columns. The producer always sets a valid date, never NULL.
func pgDate(t time.Time) pgtype.Date {
return pgtype.Date{Time: t, Valid: true}
}
// parseOrgUUID parses the validated org id from context into a uuid for the tenant_id column. The org
// originates from RunInTenantTx's validated context, so a parse failure is a programming error, not client
// input.
func parseOrgUUID(orgID string) (uuid.UUID, error) {
return uuid.Parse(orgID)
}