Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/opportunities/normalize.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/opportunities/normalize.go |
| Original SHA-256 | db904080ac850490beec7df0716e00dff7a5d82f0c66e3af938a1333112ff6fe |
| Git blob SHA | 8512f69e60644bd294204b3be7ab4e70b1fb0671 |
| Reading-copy SHA-256 | db904080ac850490beec7df0716e00dff7a5d82f0c66e3af938a1333112ff6fe |
Recovered source transcript
Download complete source text ↓163 lines · 6.4 KB
package opportunities
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"unicode"
)
// Model normalization, directive Section 5.2. The model proposes scope items; this file decides which of
// them are allowed to exist. It is a PURE function of the model payload and the source text, with no
// database and no I/O, so the grounding rules can be exhaustively tested without a model or a tenant.
//
// The rule that matters most is the third one below. A model that omits a citation is merely unhelpful; a
// model that INVENTS a citation is dangerous, because fabricated support reads exactly like real support to
// every reader downstream. So a quote is checked for actual presence in the source text, and an item whose
// quote cannot be found is rejected as fabricated rather than accepted with a warning.
// scopeTypes is the closed vocabulary from migration 0076. A model that invents a category is refused
// rather than being folded into "other", because silently recategorizing model output loses the fact that
// the model did not understand the ask.
var scopeTypes = map[string]bool{
"objective": true, "task": true, "deliverable": true, "labor_category": true,
"staffing_quantity": true, "skill": true, "certification": true, "clearance": true,
"customer_experience": true, "location": true, "travel": true, "schedule": true,
"security_control": true, "reporting": true, "transition": true, "key_personnel": true,
"pricing_instruction": true, "evaluation_factor": true, "compliance_clause": true,
"assumption": true, "dependency": true, "risk": true, "other": true,
}
const (
maxScopeTitleLen = 300
maxScopeDescLen = 4000
minQuoteLen = 12 // a quote shorter than this cannot ground anything; it matches by accident
)
// ScopeCandidate is one item as the MODEL proposed it, before any validation.
type ScopeCandidate struct {
ScopeType string `json:"scope_type"`
Title string `json:"title"`
Description string `json:"description"`
Quote string `json:"quote"`
SectionLabel string `json:"section_label"`
Confidence float64 `json:"confidence"`
}
// ValidatedScope is a candidate that survived every grounding rule, carrying the hash of the exact quote
// that grounds it. That hash is what the scope item stores as its citation.
type ValidatedScope struct {
ScopeType string
Title string
Description string
Quote string
QuoteHash string
SectionLabel string
Confidence float64
}
// RejectedScope records what was refused and why, so a normalization run can report its own precision
// instead of silently dropping most of what the model said.
type RejectedScope struct {
Title string
Reason string
}
// ValidateScopeCandidates parses a model response and returns only the items that are grounded in the
// source text. It fails closed: an unparseable payload is an error and yields nothing, and every individual
// rule rejects the item rather than repairing it.
func ValidateScopeCandidates(raw []byte, sourceText string) ([]ValidatedScope, []RejectedScope, error) {
var payload struct {
Items []ScopeCandidate `json:"items"`
}
trimmed := extractJSONObject(raw)
if trimmed == nil {
return nil, nil, fmt.Errorf("%w: model response is not a JSON object", ErrInvalidInput)
}
if err := json.Unmarshal(trimmed, &payload); err != nil {
return nil, nil, fmt.Errorf("%w: model response did not match the declared schema: %v", ErrInvalidInput, err)
}
// Normalizing whitespace on both sides makes containment robust to the line wrapping a model reproduces
// differently from the source, without loosening the check into a fuzzy match.
haystack := normalizeWS(sourceText)
accepted := make([]ValidatedScope, 0, len(payload.Items))
rejected := make([]RejectedScope, 0)
for _, c := range payload.Items {
title := strings.TrimSpace(c.Title)
quote := strings.TrimSpace(c.Quote)
switch {
case !scopeTypes[c.ScopeType]:
rejected = append(rejected, RejectedScope{title, fmt.Sprintf("scope_type %q is not in the closed vocabulary", clipStr(c.ScopeType, 40))})
case title == "":
rejected = append(rejected, RejectedScope{title, "title is empty"})
case len(title) > maxScopeTitleLen:
rejected = append(rejected, RejectedScope{title, "title exceeds the length cap"})
case len(c.Description) > maxScopeDescLen:
rejected = append(rejected, RejectedScope{title, "description exceeds the length cap"})
case len(quote) < minQuoteLen:
rejected = append(rejected, RejectedScope{title, "uncited, the item carries no usable source quote"})
case haystack == "" || !strings.Contains(haystack, normalizeWS(quote)):
// A fabricated citation. This is the load bearing check.
rejected = append(rejected, RejectedScope{title, "the cited quote was not found in the source text, treating it as a fabricated citation"})
case c.Confidence < 0 || c.Confidence > 1:
rejected = append(rejected, RejectedScope{title, "confidence is outside 0 to 1"})
default:
sum := sha256.Sum256([]byte(normalizeWS(quote)))
accepted = append(accepted, ValidatedScope{
ScopeType: c.ScopeType,
Title: title,
Description: strings.TrimSpace(c.Description),
Quote: quote,
QuoteHash: hex.EncodeToString(sum[:]),
SectionLabel: clipStr(strings.TrimSpace(c.SectionLabel), 120),
Confidence: c.Confidence,
})
}
}
return accepted, rejected, nil
}
// extractJSONObject finds the outermost JSON object in a model response. Instruct models routinely wrap
// JSON in prose or a fenced code block, and refusing those outright would reject correct output for a
// formatting habit; anything that is not a locatable object still fails closed.
func extractJSONObject(raw []byte) []byte {
s := strings.TrimSpace(string(raw))
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start < 0 || end <= start {
return nil
}
return []byte(s[start : end+1])
}
// normalizeWS collapses every run of whitespace to one space and lowercases, so a quote matches the source
// despite reflowed lines or a changed case, while still requiring the actual words in the actual order.
func normalizeWS(s string) string {
var b strings.Builder
b.Grow(len(s))
space := false
for _, r := range strings.ToLower(s) {
if unicode.IsSpace(r) {
space = true
continue
}
if space && b.Len() > 0 {
b.WriteByte(' ')
}
space = false
b.WriteRune(r)
}
return b.String()
}
func clipStr(s string, n int) string {
if len(s) > n {
return s[:n]
}
return s
}