Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/curation_apply.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ai/curation_apply.go |
| Original SHA-256 | d1a87819bac65c0f5774a1120f20bd29d6041b21447bb5e563f104091cbac339 |
| Git blob SHA | 41befd9abc93e6a8ece16c580b4396c074e20c04 |
| Reading-copy SHA-256 | d1a87819bac65c0f5774a1120f20bd29d6041b21447bb5e563f104091cbac339 |
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_apply.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_apply.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓320 lines · 13.9 KB
package ai
// curation_apply.go is the WS8 Phase 4 TWO STEP human in the loop apply path (LIVE). It is the only place a
// curation proposal turns into a curated ontology edge, and it does so in two distinct, separately authorized
// human actions, never one click and never automatically:
//
// 1. APPROVE (the first human step). A compliance or operator user approves a pending proposal. The proposal
// row is moved to approved with the approver and approved_at recorded, and for a substitutes_for_edge a
// skill_relationships row is INSERTED with verified_at NULL. That edge is STAGED and INERT: the resolver
// loads only verified_at IS NOT NULL edges (aliasresolver.go), so a staged edge changes NO score. The
// proposal approved event is audited through System.
//
// 2. VERIFY (a SECOND, distinct human step). A compliance or operator user verifies the staged edge, setting
// verified_at and the verifier. Only this single write makes the edge live: from now on the resolver
// reads it and it changes FUTURE scores (never an already served and audited score, and never the engine
// math, which is byte identical, only the resolver reference set grew). The edge added event is audited
// through System carrying the from, the to, the relation, the strength, and the verifier.
//
// Every write here goes through the MIGRATOR pool, never the app role, because curation_proposals and
// skill_relationships are global migrator owned tables the app role can only SELECT. The actor on each step is
// the validated session principal, never a client supplied id, and the two events are global (NULL org)
// because a proposal and an ontology edge are global objects, not per tenant ones.
import (
"context"
"encoding/json"
"errors"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
"github.com/missionworkforce/missionmatch/internal/platform/database/dbgen"
)
// Curation apply errors. They map to typed HTTP statuses at the handler so a double submit or a missing
// proposal is a clear client error, not a 500.
var (
// ErrProposalNotFound is returned when no proposal exists for the id.
ErrProposalNotFound = errors.New("curation: proposal not found")
// ErrProposalNotPending is returned when an approve targets a proposal that is not pending (already
// approved, already rejected, or concurrently moved), so an approval never re fires.
ErrProposalNotPending = errors.New("curation: proposal is not pending")
// ErrProposalNotApproved is returned when a verify targets a proposal that is not approved yet, enforcing
// that verify is genuinely the second step after approve.
ErrProposalNotApproved = errors.New("curation: proposal is not approved")
// ErrNotASubstitutionEdge is returned when an approve or verify targets a proposal whose kind is not a
// substitutes_for_edge, because only that kind stages and verifies an ontology edge. The evidence only
// kinds (reciprocity_review, recalibration_evidence) are not applied to the resolver.
ErrNotASubstitutionEdge = errors.New("curation: proposal is not a substitutes_for_edge")
// ErrNoStagedEdge is returned when a verify finds no staged edge to activate (the approve never ran, or
// the edge is already verified).
ErrNoStagedEdge = errors.New("curation: no staged edge to verify")
// ErrNoActor is returned when the request has no validated session principal to record as the actor.
ErrNoActor = errors.New("curation: no validated actor")
// ErrNoMigratorPool is returned when the apply service has no migrator pool configured, so the governance
// write surface reports unavailable rather than silently using the app role.
ErrNoMigratorPool = errors.New("curation: migrator pool not configured")
// ErrVerifierMustDifferFromApprover is returned when separation of duties is enforced and the same user
// who approved a proposal tries to verify its staged edge. It makes the two person control real rather
// than procedural. A single operator pilot may turn enforcement off in config to self verify.
ErrVerifierMustDifferFromApprover = errors.New("curation: the verifier must differ from the approver")
)
// CurationApplyService runs the two step apply on the migrator pool. It holds the migrator pool directly (the
// global write seam) and the audit writer (System events). It is constructed only when a migrator DSN is
// present; a nil pool makes every action return ErrNoMigratorPool so the surface fails closed.
type CurationApplyService struct {
migrator *pgxpool.Pool
audit *audit.Writer
enforceSeparation bool // when true, the verifier must differ from the approver (the two person control)
}
// NewCurationApplyService wires the apply service. migrator is the BYPASSRLS migrator pool (the only pool that
// may write the global curation tables); auditW records the global proposal approved and edge added events.
// enforceSeparation requires the verifier to differ from the approver (config MM_CURATION_SEPARATION_OF_DUTIES,
// default true); a single operator pilot may pass false to self verify, an explicit audited choice.
func NewCurationApplyService(migrator *pgxpool.Pool, auditW *audit.Writer, enforceSeparation bool) *CurationApplyService {
return &CurationApplyService{migrator: migrator, audit: auditW, enforceSeparation: enforceSeparation}
}
// ProposalView is the read shape the /compliance surface lists: a pending proposal with its kind, the skill
// pair (ids only, never PII), the proposed strength, and the evidence counts. The frontend renders the
// approve and verify actions over this.
type ProposalView struct {
ID string `json:"id"`
Kind string `json:"kind"`
FromSkillID string `json:"from_skill_id"`
ToSkillID string `json:"to_skill_id"`
Relation string `json:"relation"`
ProposedStrength string `json:"proposed_strength"`
EvidenceObservations int `json:"evidence_observations"`
EvidenceSuccesses int `json:"evidence_successes"`
ModelVersion string `json:"model_version"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
// ListPending returns the proposals awaiting the first human step, read through the migrator pool.
func (s *CurationApplyService) ListPending(ctx context.Context) ([]ProposalView, error) {
if s == nil || s.migrator == nil {
return nil, ErrNoMigratorPool
}
rows, err := dbgen.New(s.migrator).ListPendingProposals(ctx)
if err != nil {
return nil, err
}
out := make([]ProposalView, 0, len(rows))
for _, r := range rows {
out = append(out, toProposalView(r))
}
return out, nil
}
// Approve is the FIRST human step. It moves a pending substitutes_for_edge proposal to approved (recording the
// approver and approved_at) and stages the INERT skill_relationships edge (verified_at NULL) in one migrator
// transaction, so the proposal transition and the staged edge commit together. The staged edge is read by NO
// score (the resolver ignores unverified edges), so this step changes nothing live yet. It audits
// teaching.proposal.approved through System. actor is the validated session principal.
func (s *CurationApplyService) Approve(ctx context.Context, proposalID, actor uuid.UUID) (ProposalView, error) {
if s == nil || s.migrator == nil {
return ProposalView{}, ErrNoMigratorPool
}
var view ProposalView
err := pgx.BeginFunc(ctx, s.migrator, func(tx pgx.Tx) error {
q := dbgen.New(tx)
prop, gerr := q.GetProposal(ctx, proposalID)
if errors.Is(gerr, pgx.ErrNoRows) {
return ErrProposalNotFound
}
if gerr != nil {
return gerr
}
if prop.Kind != kindSubstitutesForEdge {
return ErrNotASubstitutionEdge
}
if prop.Status != "pending" {
return ErrProposalNotPending
}
if prop.FromSkillID == nil || prop.ToSkillID == nil || prop.ProposedStrength == nil {
return ErrNotASubstitutionEdge
}
approved, aerr := q.ApproveProposalTx(ctx, dbgen.ApproveProposalTxParams{ID: proposalID, Approver: &actor})
if errors.Is(aerr, pgx.ErrNoRows) {
return ErrProposalNotPending // a concurrent approval won the race
}
if aerr != nil {
return aerr
}
// Stage the INERT edge. verified_at stays NULL, so the resolver does not read it; a SECOND human action
// verifies it later to make it live. ON CONFLICT keeps approve idempotent against an existing edge.
if _, serr := q.StageSubstitutesForEdge(ctx, dbgen.StageSubstitutesForEdgeParams{
FromSkillID: *approved.FromSkillID,
ToSkillID: *approved.ToSkillID,
Strength: *approved.ProposedStrength,
}); serr != nil {
return serr
}
payload, merr := json.Marshal(map[string]any{
"from_skill_id": approved.FromSkillID.String(),
"to_skill_id": approved.ToSkillID.String(),
"relation": substitutionRelation,
"staged": true, // staged and inert, not yet verified
})
if merr != nil {
return merr
}
if auderr := s.audit.System(ctx, audit.Event{
Action: "teaching.proposal.approved",
ActorUserID: &actor,
EntityType: "curation_proposal",
EntityID: proposalID.String(),
NewValues: payload,
}); auderr != nil {
return auderr
}
view = toProposalView(approved)
return nil
})
if err != nil {
return ProposalView{}, err
}
return view, nil
}
// Verify is the SECOND, distinct human step. It activates the staged edge that an approval created by setting
// verified_at and the verifier, ONLY when the proposal is approved and the edge is still unverified. Only this
// write makes the edge live: the resolver now reads it and it changes FUTURE scores. The engine ModelVersion
// stays v1.4.0, because the math is unchanged and only the resolver reference set grew (recorded in the model
// card). It audits ontology.edge.added through System carrying the from, the to, the relation, the strength,
// and the verifier. actor is the validated session principal, distinct from the approver in practice.
func (s *CurationApplyService) Verify(ctx context.Context, proposalID, actor uuid.UUID) (ProposalView, error) {
if s == nil || s.migrator == nil {
return ProposalView{}, ErrNoMigratorPool
}
var view ProposalView
err := pgx.BeginFunc(ctx, s.migrator, func(tx pgx.Tx) error {
q := dbgen.New(tx)
prop, gerr := q.GetProposal(ctx, proposalID)
if errors.Is(gerr, pgx.ErrNoRows) {
return ErrProposalNotFound
}
if gerr != nil {
return gerr
}
if prop.Kind != kindSubstitutesForEdge {
return ErrNotASubstitutionEdge
}
if prop.Status != "approved" {
return ErrProposalNotApproved
}
// Separation of duties: the human who verifies an edge live must not be the human who approved it, so
// the two person control is enforced, not merely procedural. A single operator pilot may turn this off
// in config. The approver is recorded on the proposal, so no extra read is needed.
if s.enforceSeparation && prop.Approver != nil && *prop.Approver == actor {
return ErrVerifierMustDifferFromApprover
}
if prop.FromSkillID == nil || prop.ToSkillID == nil {
return ErrNotASubstitutionEdge
}
// Find the staged edge for this pair and activate it. A double submit finds the edge already verified
// (VerifyStagedEdge only matches verified_at IS NULL), so it returns no row and we report ErrNoStagedEdge,
// keeping verify a single transition.
edgeID, found, ferr := stagedEdgeID(ctx, tx, *prop.FromSkillID, *prop.ToSkillID)
if ferr != nil {
return ferr
}
if !found {
return ErrNoStagedEdge
}
edge, verr := q.VerifyStagedEdge(ctx, dbgen.VerifyStagedEdgeParams{ID: edgeID, Verifier: &actor})
if errors.Is(verr, pgx.ErrNoRows) {
return ErrNoStagedEdge // already verified by a concurrent action
}
if verr != nil {
return verr
}
payload, merr := json.Marshal(map[string]any{
"from_skill_id": edge.FromSkillID.String(),
"to_skill_id": edge.ToSkillID.String(),
"relation": edge.Relation,
"strength": edge.Strength.String(),
"verifier": actor.String(),
})
if merr != nil {
return merr
}
if auderr := s.audit.System(ctx, audit.Event{
Action: "ontology.edge.added",
ActorUserID: &actor,
EntityType: "skill_relationship",
EntityID: edge.ID.String(),
NewValues: payload,
}); auderr != nil {
return auderr
}
view = toProposalView(prop)
view.Status = "verified" // surface the live state to the caller; the proposal row itself stays approved
return nil
})
if err != nil {
return ProposalView{}, err
}
return view, nil
}
// stagedEdgeID finds the unverified staged skill_relationships edge for a substitutes_for pair, so verify can
// activate exactly that row. It reads through the same migrator tx. An edge that is already verified is not
// returned (found false), so verify is a single transition.
func stagedEdgeID(ctx context.Context, tx pgx.Tx, fromSkillID, toSkillID uuid.UUID) (uuid.UUID, bool, error) {
var id uuid.UUID
err := tx.QueryRow(ctx,
`SELECT id FROM skill_relationships
WHERE from_skill_id = $1 AND to_skill_id = $2 AND relation = 'substitutes_for'
AND verified_at IS NULL`,
fromSkillID, toSkillID).Scan(&id)
if errors.Is(err, pgx.ErrNoRows) {
return uuid.Nil, false, nil
}
if err != nil {
return uuid.Nil, false, err
}
return id, true, nil
}
// toProposalView maps a stored proposal to the read shape, with empty strings for the nullable id and strength
// fields rather than a nil pointer, so the frontend renders a stable row.
func toProposalView(p dbgen.CurationProposal) ProposalView {
v := ProposalView{
ID: p.ID.String(),
Kind: p.Kind,
EvidenceObservations: int(p.EvidenceObservations),
EvidenceSuccesses: int(p.EvidenceSuccesses),
ModelVersion: p.ModelVersion,
Status: p.Status,
CreatedAt: p.CreatedAt.Format("2006-01-02"),
}
if p.FromSkillID != nil {
v.FromSkillID = p.FromSkillID.String()
}
if p.ToSkillID != nil {
v.ToSkillID = p.ToSkillID.String()
}
if p.Relation != nil {
v.Relation = *p.Relation
}
if p.ProposedStrength != nil {
v.ProposedStrength = p.ProposedStrength.String()
}
return v
}