Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/curation_handler.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ai/curation_handler.go |
| Original SHA-256 | 36bbd1f9366e1b2e486a8d1d0a10b5894e25b8ef28e7e263b24c1833c1a4ed11 |
| Git blob SHA | 400386250b49614b7ceb14c8ae67212f1cfbf300 |
| Reading-copy SHA-256 | 36bbd1f9366e1b2e486a8d1d0a10b5894e25b8ef28e7e263b24c1833c1a4ed11 |
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_handler.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / internal/modules/ai/curation_handler.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓140 lines · 6.9 KB
package ai
// curation_handler.go mounts the WS8 Phase 4 curation surface on the /compliance routes, gated behind the
// curation.curate capability (the compliance and operator tier, owner and admin). It exposes the two step
// human in the loop gate as three endpoints: list the pending proposals, APPROVE one (the first human step,
// which stages an INERT edge), and VERIFY one (the second, distinct human step, which activates the edge). The
// handler is thin: it reads the validated session actor, never a client supplied id, and delegates every write
// to the migrator pool backed apply service.
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// TeachingDPNotice is the short, plain language notice the /compliance page can show next to the curation
// proposals, so a compliance officer sees the privacy and governance posture in one sentence without opening
// the model card. It states the two load bearing facts: the honest score invariant (two human steps before any
// future score moves, and never an already served score or the engine math), and that the cross tenant
// differential privacy path is OFF in tenant LIVE. It carries no PII and no client fact, only the posture. The
// full detail lives in docs/MODEL_CARD_TEACHING.md.
const TeachingDPNotice = "The teaching loop learns abstract reviewer craft from this tenant only. " +
"It never changes a weight, a band cut, the semantic threshold, or any already served score. " +
"A curated skill edge changes only FUTURE scores, and only after two separate humans approve then verify it. " +
"Cross tenant differential privacy aggregation is OFF and stays off until a real cohort of multiple live tenants exists and a privacy budget is signed off. " +
"See the teaching model card for the full privacy and governance detail."
// CurationHandler serves the curation proposal surface. svc may be nil (the apply service is unavailable when
// no migrator pool is configured), in which case every endpoint reports the surface unavailable rather than
// panicking, the graceful posture every module here holds.
type CurationHandler struct{ svc *CurationApplyService }
// NewCurationHandler builds the handler.
func NewCurationHandler(svc *CurationApplyService) *CurationHandler {
return &CurationHandler{svc: svc}
}
// Routes mounts the curation endpoints under /compliance, each gated on the curation.curate capability. The
// approve and verify actions are two SEPARATE POST endpoints so the two human steps are two distinct,
// separately auditable requests, never one combined call.
func (h *CurationHandler) Routes(r chi.Router) {
r.With(security.RequireCapability(security.CapCurationCurate)).Get("/compliance/curation/notice", h.notice)
r.With(security.RequireCapability(security.CapCurationCurate)).Get("/compliance/curation/proposals", h.listPending)
r.With(security.RequireCapability(security.CapCurationCurate)).Post("/compliance/curation/proposals/{id}/approve", h.approve)
r.With(security.RequireCapability(security.CapCurationCurate)).Post("/compliance/curation/proposals/{id}/verify", h.verify)
}
// notice returns the short teaching loop privacy and governance notice the /compliance page shows. It is a
// static, contents free posture statement, so it never touches the apply service or any tenant data.
func (h *CurationHandler) notice(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"notice": TeachingDPNotice})
}
func (h *CurationHandler) listPending(w http.ResponseWriter, r *http.Request) {
if h.svc == nil {
writeErr(w, http.StatusServiceUnavailable, "curation_unavailable", "the curation surface is not configured")
return
}
views, err := h.svc.ListPending(r.Context())
if err != nil {
mapCurationErr(w, err)
return
}
writeJSON(w, http.StatusOK, views)
}
func (h *CurationHandler) approve(w http.ResponseWriter, r *http.Request) {
h.act(w, r, func(id, actor uuid.UUID) (ProposalView, error) {
return h.svc.Approve(r.Context(), id, actor)
})
}
func (h *CurationHandler) verify(w http.ResponseWriter, r *http.Request) {
h.act(w, r, func(id, actor uuid.UUID) (ProposalView, error) {
return h.svc.Verify(r.Context(), id, actor)
})
}
// act is the shared shell for approve and verify: it checks the surface is configured, parses the proposal id
// from the path, resolves the validated session actor, runs the supplied step, and maps the result or the
// typed error. The actor is read from the session principal, never the request body.
func (h *CurationHandler) act(w http.ResponseWriter, r *http.Request, step func(id, actor uuid.UUID) (ProposalView, error)) {
if h.svc == nil {
writeErr(w, http.StatusServiceUnavailable, "curation_unavailable", "the curation surface is not configured")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
writeErr(w, http.StatusBadRequest, "bad_request", "invalid proposal id")
return
}
p, ok := tenant.UserFromContext(r.Context())
if !ok {
writeErr(w, http.StatusForbidden, "no_actor", "no validated actor")
return
}
actor, err := uuid.Parse(p.UserID)
if err != nil {
writeErr(w, http.StatusForbidden, "no_actor", "no validated actor")
return
}
view, err := step(id, actor)
if err != nil {
mapCurationErr(w, err)
return
}
writeJSON(w, http.StatusOK, view)
}
// mapCurationErr maps an apply error to a typed envelope. It never returns a raw SQL or database error string.
func mapCurationErr(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrNoMigratorPool):
writeErr(w, http.StatusServiceUnavailable, "curation_unavailable", "the curation surface is not configured")
case errors.Is(err, ErrProposalNotFound):
writeErr(w, http.StatusNotFound, "not_found", "no such proposal")
case errors.Is(err, ErrProposalNotPending):
writeErr(w, http.StatusConflict, "not_pending", "the proposal is not pending and cannot be approved")
case errors.Is(err, ErrProposalNotApproved):
writeErr(w, http.StatusConflict, "not_approved", "the proposal must be approved before it can be verified")
case errors.Is(err, ErrNotASubstitutionEdge):
writeErr(w, http.StatusUnprocessableEntity, "not_an_edge", "only a substitutes for edge proposal can be approved or verified")
case errors.Is(err, ErrNoStagedEdge):
writeErr(w, http.StatusConflict, "no_staged_edge", "there is no staged edge to verify")
case errors.Is(err, ErrVerifierMustDifferFromApprover):
writeErr(w, http.StatusConflict, "separation_of_duties", "the verifier must differ from the approver")
case errors.Is(err, ErrNoActor):
writeErr(w, http.StatusForbidden, "no_actor", "no validated actor")
case errors.Is(err, tenant.ErrNoTenant):
writeErr(w, http.StatusForbidden, "no_tenant", "no validated tenant")
default:
writeErr(w, http.StatusInternalServerError, "curation_error", "the curation action could not be completed")
}
}