Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/planner.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ai/planner.go |
| Original SHA-256 | d7ce05ee4a63a13db01eaaaec5c8cdd47088444f55d3c353403d661ec6b059f5 |
| Git blob SHA | 78866dd4eb5c20b833de8f0eda846c75fd299f34 |
| Reading-copy SHA-256 | d7ce05ee4a63a13db01eaaaec5c8cdd47088444f55d3c353403d661ec6b059f5 |
Every recorded occurrence
These 2 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/planner.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ai/planner.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓842 lines · 38.8 KB
package ai
// THE PLANNER. This file generalizes the G6 loop (the one hardcoded fan out over four fixed intents in
// graph_loop.go) into a data driven planner that answers a COMPOUND cross plane question by SELECTING and
// running a set of EXISTING closed Registry intents and composing their evidence behind the faithfulness
// gate. It is the biggest unlock on the ANI roadmap: it turns the single shot keyword router (which drops
// every compound capture question to RAG narration) into a true multi step reasoner, while the model
// authors ZERO SQL and ZERO param values.
//
// Why this is safe by construction. The model NEVER authors SQL and NEVER supplies a param value. It
// selects from a CLOSED enum of intent names; the validator asserts every name is in Registry and drops
// anything else. Every param value comes from the same deterministic extractors graphClassify already uses
// (bareNAICS, quotedAgency, quotedUUID, heldClearanceCode, quotedUEI, windowMonths, quotedCompartmentList),
// so the read only guard, the EEO and protected attribute guards, bench suppression, RLS RunInTenantTx, and
// the frozen engine boundary all apply UNCHANGED on every node (each node runs its existing guarded path).
// A bench node runs only through the suppressing executor (runBenchIntent), never the raw ExecuteIntent
// path (which already hard rejects bench intents). The G6 capture composition runs through
// answerCapturePicture. Every other graph or TAG intent runs through ExecuteIntent. The planner adds NO new
// query and NO new SQL. Hard caps (maxPlanNodes, no recursion) bound the work; the surviving node set runs
// in a deterministic order (Registry order, then name) so the composed answer is byte stable. Self
// consistency samples the plan K times and runs only the stable intent set. The plan trace audit carries
// only names, kinds, counts, and verdicts, never a value and never PII.
//
// Iteration 1 (this file) is planned SELECTION with deterministic binding. Data dependency edges (one
// node's scalar output binding a downstream node's arg) are a documented iteration 2, explicitly out of
// scope here so the first planner is maximally safe.
import (
"context"
"encoding/json"
"sort"
"strings"
"github.com/missionworkforce/missionmatch/internal/platform/inference"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// PlanNode is one selected intent in a plan: a name that MUST be in the closed Registry, plus the args the
// validator bound for it from the available params. The model supplies only the Intent name; Args are bound
// deterministically by the orchestrator, never by the model.
type PlanNode struct {
Intent string
Args []any
// EdgeFrom, when non empty, marks that the arg at EdgeArgIdx is supplied at RUN time from the upstream
// EdgeFrom intent's EdgeColumn output (its first result row), not from the question and not from the model.
// The value is a PUBLIC identifier and the catalog is fixed; a placeholder empty string sits in
// Args[EdgeArgIdx] until the runner resolves it, and if the upstream yields no value the node is dropped.
EdgeFrom string
EdgeColumn string
EdgeArgIdx int
}
// Plan is the validated, bounded set of intents to run for one compound question. It carries no SQL and no
// model authored value; every Intent is a Registry name and every Arg came from a deterministic extractor.
type Plan struct {
Nodes []PlanNode
}
// planEdge is one allowed data dependency edge in the CLOSED catalog: an upstream intent's named output column
// supplies a downstream intent's named param. This is what turns the planner from a set of independent lookups
// into a chained reasoner ("recompetes for whoever holds the SIGINT work" becomes incumbent then that
// incumbent's field). It is safe the same way the rest of the planner is: the model authors NO edge (the
// catalog is fixed Go, not model output), every source column is a PUBLIC federal identifier (a prime UEI from
// the public award tables), and a bench, person, or EEO intent is NEVER a source, so no candidate or
// demographic value can ever flow through a chain. An edge wires only when BOTH endpoints are already in the
// selected plan, and the value comes from the upstream RESULT (first row of the named column), never invented.
type planEdge struct {
FromIntent string // upstream intent name, a Registry name
FromColumn string // the upstream result column that carries the value, a PUBLIC identifier only
ToIntent string // downstream intent name
ToParam string // the downstream Registry param the value binds
}
// edgeCatalog is the CLOSED, fixed set of legal dependency edges. Every source column is a public prime UEI
// from the public market reads (incumbent_of_record and segment_competitor_field over usaspending_awards and
// opportunity_records), never a bench or person or EEO output, so the PII firewall holds by construction.
// Adding an edge is a code change reviewed like any other, never a runtime or model decision. A unit test
// asserts every source is in the Registry and is not a bench intent.
var edgeCatalog = []planEdge{
{FromIntent: IntentIncumbentOfRecord, FromColumn: "incumbent_uei", ToIntent: IntentPartnerOrCompetitorRatio, ToParam: "partner_uei"},
{FromIntent: IntentSegmentCompetitorField, FromColumn: "recipient_uei", ToIntent: IntentPartnerOrCompetitorRatio, ToParam: "partner_uei"},
}
// edgeInto returns the catalog edge that can supply param toParam of intent toIntent from an upstream that is
// in the selected set, or nil. The catalog is fixed, so this is a deterministic lookup, never a model choice.
func edgeInto(toIntent, toParam string, selected map[string]bool) *planEdge {
for i := range edgeCatalog {
e := &edgeCatalog[i]
if e.ToIntent == toIntent && e.ToParam == toParam && selected[e.FromIntent] {
return e
}
}
return nil
}
// maxPlanNodes is the hard cap on how many intents one plan may run. It bounds the work the same way a
// recursive CTE's depth cap bounds its walk; there is no recursion in the planner. Raised to 10 so a rich
// cross plane capture question can compose more of the closed Registry in one integrated answer (the ANI
// deepening); the closed enum, per node guards, RLS, and the frozen engine boundary bound every node the
// same regardless of the count, so a deeper plan is exactly as safe as a shallow one, only more complete.
const maxPlanNodes = 10
// planSelfConsistencyK is the number of times the plan is sampled before only the stable (majority) intent
// set runs. It mirrors the low temperature the faithfulness gate already uses, three samples.
const planSelfConsistencyK = 3
// availableParams is the typed bag of what the question unambiguously carries, extracted by the same
// deterministic extractors graphClassify uses. The model supplies none of these; every value here came
// from a regex over the original question. A node binds only from this bag, so the model can never inject a
// param value.
type availableParams struct {
Agency string
AgencyOK bool
NAICS string
NAICSOK bool
UUID string
UUIDOK bool
HeldClearance string
HeldOK bool
UEI string
UEIOK bool
WindowMonths int
Compartments string
CompartmentsOK bool
// M5 wage band params: the SOC occupation code resolved from a quoted role through the curated crosswalk,
// and the OEWS area code (HI or NM) resolved from a named state. Both come from deterministic resolvers over
// the question text, never the model.
OccupationCode string
OccupationOK bool
Area string
AreaOK bool
}
// extractAvailableParams runs the existing deterministic extractors and returns the typed bag of what the
// question unambiguously carries. It is pure, deterministic, and touches no database. Every value comes from
// a regex over the question text, never from the model. WindowMonths defaults to zero so an intent that
// takes a window applies its own clamp when the question states no months.
func extractAvailableParams(question string) availableParams {
lower := strings.ToLower(question)
var a availableParams
if agency, ok := quotedAgency(question); ok {
a.Agency, a.AgencyOK = agency, true
}
if naics, ok := bareNAICS(question); ok {
a.NAICS, a.NAICSOK = naics, true
}
if id, ok := quotedUUID(question); ok {
a.UUID, a.UUIDOK = id, true
}
if code, ok := heldClearanceCode(lower); ok {
a.HeldClearance, a.HeldOK = code, true
}
if uei, ok := quotedUEI(question); ok {
a.UEI, a.UEIOK = uei, true
}
a.WindowMonths = windowMonths(question)
if list, ok := quotedCompartmentList(question); ok {
a.Compartments, a.CompartmentsOK = list, true
}
// M5 wage band: a quoted role resolved to a SOC through the curated crosswalk, and a named state resolved to
// a known OEWS area code. Both are conservative: an unresolvable role or an unknown state leaves the slot
// unset so the wage intent simply drops rather than binding a wrong occupation or state.
if role, ok := quotedRole(question); ok {
if occ, resolved := resolveSOC(role); resolved {
a.OccupationCode, a.OccupationOK = occ.SOC, true
}
}
if area, ok := resolveArea(question); ok {
a.Area, a.AreaOK = area, true
}
return a
}
// paramSource names the available bag field a Registry param binds from. It is the FIXED lookup table keyed
// on the EXACT param names the Registry uses (read from tag.go and graph_*.go), so a param binds by its
// declared name, never by a guess. A param name not in this table never binds, which means its intent is
// simply never selectable by the planner, the safe default.
type paramSource string
const (
srcUUID paramSource = "uuid" // a ParamUUID id binds from avail.UUID
srcAgency paramSource = "agency" // an agency or agency_prefix ParamText binds from avail.Agency
srcNAICS paramSource = "naics" // a naics ParamText binds from avail.NAICS
srcHeldClearance paramSource = "held" // a clearance ParamEnum binds from avail.HeldClearance
srcUEI paramSource = "uei" // a partner_uei ParamText binds from avail.UEI
srcCompartments paramSource = "compartments" // a compartment list ParamText binds from avail.Compartments
srcWindowMonths paramSource = "window_months" // a window ParamInt binds from avail.WindowMonths
srcClampZero paramSource = "clamp_zero" // a bounded ParamInt the question never states binds zero, the intent's own clamp
srcOccupation paramSource = "occupation" // a SOC occupation_code ParamText binds from avail.OccupationCode
srcArea paramSource = "area" // an OEWS area ParamEnum binds from avail.Area
)
// paramBindings maps every Registry param NAME to the available bag field it binds from. The keys are the
// EXACT names declared in the Registry. A required param whose name is absent here cannot bind, so its
// intent drops, which is safe (the planner simply never selects an intent it cannot bind honestly). The
// window and clamp sources differ: a window_months reads the stated window (zero applies the intent clamp),
// while max_depth and lookback_years always bind zero so the intent applies its own clamp and the planner
// never invents a recursion depth or a lookback the user did not state.
var paramBindings = map[string]paramSource{
// UUID id params across the planes.
"requirement_id": srcUUID,
"slate_id": srcUUID,
"start_skill_id": srcUUID,
"required_skill_id": srcUUID,
"opportunity_id": srcUUID,
"skill_id": srcUUID,
// Agency text params (the buyer key the market and TAG reads use).
"agency": srcAgency,
"agency_prefix": srcAgency,
"customer_agency": srcAgency,
// NAICS text param.
"naics": srcNAICS,
// Clearance enum params (both bind the held clearance code; the executor re checks each declared allowed
// set, so a value out of one intent's vocabulary is rejected at execution, not bound blindly).
"held_code": srcHeldClearance,
"clearance_level": srcHeldClearance,
// Partner UEI text param.
"partner_uei": srcUEI,
// Compartment list text param.
"required_compartments": srcCompartments,
// Window and clamp integer params.
"window_months": srcWindowMonths,
"max_depth": srcClampZero,
"lookback_years": srcClampZero,
// M5 wage band params: the SOC occupation code and the OEWS area code.
"occupation_code": srcOccupation,
"area": srcArea,
}
// bindOneParam binds a SINGLE Registry param from the available bag through the fixed paramBindings table,
// returning (value, true) when the question supplies it, else (nil, false). It is the per param core of
// bindIntentArgs, factored so the edge aware binder can ask "can the question supply THIS one param" without
// the all or nothing whole intent bind. The model supplies no value: each value comes from a deterministic
// extractor, or the intent's own clamp (zero) for a bounded integer the question never states.
func bindOneParam(p Param, avail availableParams) (any, bool) {
src, known := paramBindings[p.Name]
if !known {
return nil, false // an unmapped param name can never bind, the safe default
}
switch src {
case srcUUID:
if !avail.UUIDOK {
return nil, false
}
return avail.UUID, true
case srcAgency:
if !avail.AgencyOK {
return nil, false
}
return avail.Agency, true
case srcNAICS:
if !avail.NAICSOK {
return nil, false
}
return avail.NAICS, true
case srcHeldClearance:
if !avail.HeldOK {
return nil, false
}
return avail.HeldClearance, true
case srcUEI:
if !avail.UEIOK {
return nil, false
}
return avail.UEI, true
case srcCompartments:
if !avail.CompartmentsOK {
return nil, false
}
return avail.Compartments, true
case srcWindowMonths:
// A stated window binds; an unstated window binds zero so the intent applies its own clamp.
return avail.WindowMonths, true
case srcClampZero:
// A bounded recursion depth or lookback the question never states binds zero, the intent's own clamp.
return 0, true
case srcOccupation:
if !avail.OccupationOK {
return nil, false
}
return avail.OccupationCode, true
case srcArea:
if !avail.AreaOK {
return nil, false
}
return avail.Area, true
default:
return nil, false
}
}
// bindIntentArgs supplies, for each of the intent's Params in order, the matching available value by the
// param NAME. It returns (args, true) ONLY when every required param is available from the question, else
// (nil, false) so the node is dropped. This is the NON edge bind used by the single shot paths.
func bindIntentArgs(intent QueryIntent, avail availableParams) ([]any, bool) {
if len(intent.Params) == 0 {
return nil, true
}
args := make([]any, 0, len(intent.Params))
for _, p := range intent.Params {
one, ok := bindOneParam(p, avail)
if !ok {
return nil, false
}
args = append(args, one)
}
return args, true
}
// bindIntentArgsWithEdges binds an intent's args from the available bag, allowing AT MOST ONE param to be
// supplied later by a catalog edge from an upstream that is in the selected set. It returns the args (with an
// empty placeholder at the edge bound position resolved at run time), the edge (nil when the node binds fully
// from the question), the edge arg index, and ok. A node needing two edge bound params, or a param that
// neither the question nor a catalog edge supplies, does not bind (ok false), so the planner chains only a
// node it can honestly complete. The model supplies no value and no edge: the edge comes from the fixed
// catalog and the value from the upstream RESULT.
func bindIntentArgsWithEdges(intent QueryIntent, avail availableParams, selected map[string]bool) ([]any, *planEdge, int, bool) {
if len(intent.Params) == 0 {
return nil, nil, -1, true
}
args := make([]any, 0, len(intent.Params))
var edge *planEdge
edgeIdx := -1
for i, p := range intent.Params {
one, ok := bindOneParam(p, avail)
if ok {
args = append(args, one)
continue
}
// The question does not supply this param: at most one param may be edge bound, from a catalog edge
// whose upstream is in the selected set. A second unbindable param drops the node, the safe default.
if edge == nil {
if e := edgeInto(intent.Name, p.Name, selected); e != nil {
edge = e
edgeIdx = i
args = append(args, "") // placeholder resolved at run time from the upstream result
continue
}
}
return nil, nil, -1, false
}
return args, edge, edgeIdx, true
}
// proposePlan asks the inference plane to SELECT the relevant intent names for a compound question. The
// prompt lists every Registry intent NAME with its one line Description and required param names, states
// which params are AVAILABLE for this question, and instructs the model to return ONLY a JSON array of
// intent names from that list, at most maxPlanNodes, no prose, no SQL, no new names. It parses the JSON
// array and DISCARDS any element that is not an exact IntentByName hit (the closed enum check). On any plane
// error or unparseable output it returns an EMPTY selection (the caller falls back to RAG), never a guess.
// With a nil client (the no plane posture) it returns empty so the planner degrades to RAG.
func (o *Orchestrator) proposePlan(ctx context.Context, question string, avail availableParams) ([]string, error) {
if o.client == nil {
return nil, nil // no plane: the planner degrades to RAG, never a guess
}
system := "You are a query PLANNER for a cleared workforce intelligence platform. You select which of a " +
"FIXED set of named, read only intents to run to answer a compound question. You author NO SQL and " +
"supply NO parameter values: you return ONLY intent names from the list below, the ones that are both " +
"relevant to the question AND bindable from the AVAILABLE parameters. Return a JSON array of intent " +
"name strings, at most " + itoa(maxPlanNodes) + ", no prose, no SQL, no names that are not in the list. " +
"If no intent is relevant and bindable, return an empty array."
prompt := "AVAILABLE PARAMETERS: " + describeAvail(avail) + "\n\nINTENTS:\n" + registryCatalog() +
"\n\nQUESTION: " + question + "\n\nReturn ONLY the JSON array of intent names."
out, err := o.client.Generate(ctx, inference.GenerateRequest{System: system, Prompt: prompt, MaxTokens: 200, Temperature: 0.1})
if err != nil {
return nil, nil // a plane error returns an empty selection so the caller falls back to RAG, never a guess
}
names := parseIntentNames(out.Text)
// The closed enum check: keep only names that are an exact registered intent, cap at maxPlanNodes. The
// model can never introduce an intent the platform did not author.
kept := make([]string, 0, len(names))
seen := map[string]bool{}
for _, n := range names {
n = strings.TrimSpace(n)
if n == "" || seen[n] {
continue
}
if _, ok := IntentByName(n); !ok {
continue // not a Registry name: dropped (the closed enum check)
}
seen[n] = true
kept = append(kept, n)
if len(kept) >= maxPlanNodes {
break
}
}
return kept, nil
}
// validatePlan turns a proposed name list into a bounded, ordered, fully bound Plan. For each registered
// name (in deterministic Registry order, then name) it looks up the intent, binds its args, and keeps the
// node ONLY if it binds. A name not in the Registry, or one whose required params are not available, never
// reaches the runner. The result is byte stable on a re run: the same proposed set yields the same ordered
// Plan.
func validatePlan(names []string, avail availableParams) Plan {
want := map[string]bool{}
for _, n := range names {
want[n] = true
}
var nodes []PlanNode
// Walk the Registry in its declared order so the surviving set is deterministic. Each node binds through
// the edge aware binder, so a node whose one missing param a catalog edge can supply from a selected
// upstream survives with that param marked edge bound (resolved at run time); a node the question and the
// catalog cannot complete drops.
for _, intent := range Registry {
if !want[intent.Name] {
continue
}
args, edge, edgeIdx, ok := bindIntentArgsWithEdges(intent, avail, want)
if !ok {
continue // unbindable: dropped, never reaches the runner
}
node := PlanNode{Intent: intent.Name, Args: args}
if edge != nil {
node.EdgeFrom, node.EdgeColumn, node.EdgeArgIdx = edge.FromIntent, edge.FromColumn, edgeIdx
}
nodes = append(nodes, node)
if len(nodes) >= maxPlanNodes {
break
}
}
// Deterministic topological order: an edge target runs AFTER its source. The order is byte stable so the
// composed answer is reproducible.
return Plan{Nodes: topoOrderEdges(nodes)}
}
// topoOrderEdges returns the nodes in a deterministic order that always runs an edge source before its target:
// the edge free nodes first (name sorted), then the edge bound nodes (name sorted). The catalog is a single
// level (a source binds from the question, a target binds from a source), so two tiers suffice and are acyclic
// by construction. The order is byte stable so the composed answer is reproducible.
func topoOrderEdges(nodes []PlanNode) []PlanNode {
var free, bound []PlanNode
for _, n := range nodes {
if n.EdgeFrom == "" {
free = append(free, n)
} else {
bound = append(bound, n)
}
}
sort.SliceStable(free, func(i, j int) bool { return free[i].Intent < free[j].Intent })
sort.SliceStable(bound, func(i, j int) bool { return bound[i].Intent < bound[j].Intent })
return append(free, bound...)
}
// proposeFn is the proposer signature, injected so the unit tests can drive self consistency with a stub and
// no live plane. proposeStablePlan calls it k times.
type proposeFn func(ctx context.Context, question string, avail availableParams) ([]string, error)
// proposeStablePlan samples the plan k times and runs only the STABLE intent set: the nodes that appear in a
// MAJORITY of the k validated plans, in the deterministic order. If the plans disagree entirely (no majority
// node), it returns an empty plan so the caller falls back to the deterministic single intent or RAG path.
// The proposer is injected so the tests drive it with a stub and no live plane.
func proposeStablePlan(ctx context.Context, question string, avail availableParams, k int, propose proposeFn) Plan {
if k < 1 {
k = 1
}
counts := map[string]int{}
for i := 0; i < k; i++ {
names, err := propose(ctx, question, avail)
if err != nil {
continue
}
plan := validatePlan(names, avail)
// Count each node once per validated plan (the plan already deduplicates names).
for _, node := range plan.Nodes {
counts[node.Intent]++
}
}
majority := (k / 2) + 1
stable := make([]string, 0, len(counts))
for name, c := range counts {
if c >= majority {
stable = append(stable, name)
}
}
if len(stable) == 0 {
return Plan{} // no majority node: fall back to the deterministic single intent or RAG path
}
return validatePlan(stable, avail)
}
// describeAvail renders the available parameter KINDS (never the values) for the proposer prompt and is also
// the shape the audit records. It tells the model which slots can bind without ever revealing a value (a
// requirement id, an agency name, a UEI), so a value can never leak into the prompt or the audit.
func describeAvail(avail availableParams) string {
kinds := availableKinds(avail)
if len(kinds) == 0 {
return "none"
}
return strings.Join(kinds, ", ")
}
// availableKinds returns the sorted list of param KIND names the question carries (never the values). It is
// the proposer prompt's available list and the audit's available_kinds field; it holds no value and no PII.
func availableKinds(avail availableParams) []string {
var kinds []string
if avail.UUIDOK {
kinds = append(kinds, "uuid")
}
if avail.AgencyOK {
kinds = append(kinds, "agency")
}
if avail.NAICSOK {
kinds = append(kinds, "naics")
}
if avail.HeldOK {
kinds = append(kinds, "held_clearance")
}
if avail.UEIOK {
kinds = append(kinds, "uei")
}
if avail.CompartmentsOK {
kinds = append(kinds, "compartments")
}
if avail.WindowMonths > 0 {
kinds = append(kinds, "window_months")
}
if avail.OccupationOK {
kinds = append(kinds, "occupation_code")
}
if avail.AreaOK {
kinds = append(kinds, "area")
}
sort.Strings(kinds)
return kinds
}
// registryCatalog lists every Registry intent NAME, its one line Description, and its required param names
// for the proposer prompt, in Registry order. The model sees only names, descriptions, and param names, the
// closed menu it selects from; it never sees SQL.
func registryCatalog() string {
var b strings.Builder
for _, it := range Registry {
b.WriteString("- ")
b.WriteString(it.Name)
if len(it.Params) > 0 {
names := make([]string, len(it.Params))
for i, p := range it.Params {
names[i] = p.Name
}
b.WriteString(" (params: ")
b.WriteString(strings.Join(names, ", "))
b.WriteString(")")
} else {
b.WriteString(" (no params)")
}
b.WriteString(": ")
b.WriteString(it.Description)
b.WriteString("\n")
}
return b.String()
}
// parseIntentNames parses the model's output into a list of candidate intent names. It accepts a bare JSON
// array of strings (the instructed shape) and tolerates surrounding prose by extracting the first bracketed
// array. On any parse failure it returns nil, so proposePlan returns an empty selection and the caller falls
// back to RAG.
func parseIntentNames(text string) []string {
text = strings.TrimSpace(text)
start := strings.IndexByte(text, '[')
end := strings.LastIndexByte(text, ']')
if start < 0 || end < 0 || end <= start {
return nil
}
var names []string
if err := json.Unmarshal([]byte(text[start:end+1]), &names); err != nil {
return nil
}
return names
}
// itoa is a tiny no dependency integer to string for the prompt; strconv would do but this keeps the prompt
// builder self contained and avoids an import for one cap value.
func itoa(n int) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var buf [20]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}
// capturedResult holds a node's rendered result columns and rows so a DOWNSTREAM edge can read a value from
// it. Only public identifier columns are ever read (the catalog guarantees this), so nothing PII is captured
// for a use it is not allowed to have; the map lives for one plan and is discarded.
type capturedResult struct {
Columns []string
Rows [][]any
}
// firstRowColumn returns the first row's value in the named column as a string, and ok. It reads ONLY a value
// the edge catalog authorized (a public identifier). It requires a string cell (a text identifier like a UEI),
// so a non text value never binds a text param; an empty result, a missing column, or a non string cell all
// return ok false, so the dependent node drops rather than binding a wrong or empty value.
func firstRowColumn(r capturedResult, col string) (string, bool) {
if len(r.Rows) == 0 {
return "", false
}
idx := -1
for i, c := range r.Columns {
if c == col {
idx = i
break
}
}
if idx < 0 || idx >= len(r.Rows[0]) {
return "", false
}
s, ok := r.Rows[0][idx].(string)
if !ok || s == "" {
return "", false
}
return s, true
}
// runPlan executes each surviving node through its CORRECT existing path, collects the union of rendered
// evidence, and returns the union, the suppressed cell count across bench nodes, and the ordered names that
// actually ran. A bench intent runs through the suppressing executor (runBenchIntent) rendered with
// RenderCount, so a suppressed cell is the sentinel and never a raw sub k number; the G6 capture composition
// runs through answerCapturePicture; every other graph or TAG intent runs through ExecuteIntent rendered
// with renderTable. A node that errors is dropped from the union with a logged note (the plan degrades,
// never hangs). The planner adds no new query: every node is an existing guarded path.
func (o *Orchestrator) runPlan(ctx context.Context, plan Plan) (evidence []string, suppressed int, ran []string, edges []string, err error) {
k := ConfiguredSuppressionThreshold()
results := map[string]capturedResult{} // upstream outputs captured so a downstream edge can read a value
for _, node := range plan.Nodes {
// Resolve an incoming dependency edge: the value comes from the upstream RESULT's named public column
// (first row), never the model and never the question. An upstream that produced no such value drops the
// dependent node, so the plan degrades and never guesses. The nodes run in topological order, so the
// source has already run and its result is in the map before its target reaches this point.
if node.EdgeFrom != "" {
val, ok := firstRowColumn(results[node.EdgeFrom], node.EdgeColumn)
if !ok {
continue // upstream yielded no value: drop the dependent node
}
if node.EdgeArgIdx >= 0 && node.EdgeArgIdx < len(node.Args) {
node.Args[node.EdgeArgIdx] = val
}
edges = append(edges, node.EdgeFrom+"->"+node.Intent)
}
switch {
case node.Intent == IntentCapturePictureForRequirement:
// The G6 capture composition is itself a person touching bench intent and already composed. Run it
// through its existing path and fold its deterministic answer into the union as one evidence line.
id, idErr := benchIDArg(node.Args)
if idErr != nil {
continue // a malformed id drops the node, the plan degrades
}
resp, cErr := o.answerCapturePicture(ctx, id)
if cErr != nil || resp == nil || resp.Refused {
continue
}
evidence = append(evidence, "capture_picture_for_requirement: "+resp.Answer)
ran = append(ran, node.Intent)
case IsModelIntent(node.Intent):
// A MAG (model backed) intent runs through answerModelIntent (NOT the raw ExecuteIntent path, which
// hard rejects a model intent). It gathers its features through the guarded feature source intents and
// scores a calibrated probability in decimal; its deterministic answer folds into the union as one
// evidence line. It computes an opportunity level number, never a per candidate score. A node that
// errors or refuses drops from the union, the plan degrades.
resp, mErr := o.answerModelIntent(ctx, "", node.Intent, node.Args)
if mErr != nil || resp == nil || resp.Refused {
continue
}
evidence = append(evidence, node.Intent+": "+resp.Answer)
ran = append(ran, node.Intent)
case IsBenchIntent(node.Intent):
// A bench intent runs through the suppressing executor. The bench plane reads RLS protected person
// data, so it REQUIRES a resolved authenticated tenant context (the same gate answerBench enforces);
// without one the node drops rather than relying only on the RLS empty result fail safe. Every cell
// renders through RenderCount, so a suppressed cell is the sentinel and no sub k number reaches the
// union.
if !guardTenantForBench(ctx) {
continue
}
labels, cells, bErr := o.runBenchIntent(ctx, node.Intent, node.Args, k)
if bErr != nil {
continue // the node drops, the plan degrades, never hangs
}
columns := []string{"bucket", "headcount"}
rows := make([][]any, len(cells))
for i, c := range cells {
if c.Suppressed {
suppressed++
}
rows[i] = []any{labels[i], RenderCount(c)}
}
evidence = append(evidence, node.Intent+": "+renderTable(node.Intent, columns, rows))
ran = append(ran, node.Intent)
default:
// Every other graph or TAG intent runs through ExecuteIntent (re guarded, RLS scoped) and renders
// with the existing deterministic table rendering.
intent, ok := IntentByName(node.Intent)
if !ok {
continue
}
res, eErr := ExecuteIntent(ctx, o.db, intent, node.Args)
if eErr != nil {
continue // the node drops, the plan degrades, never hangs
}
results[node.Intent] = capturedResult{Columns: res.Columns, Rows: res.Rows} // capture for a downstream edge
evidence = append(evidence, node.Intent+": "+renderTable(intent.Description, res.Columns, res.Rows))
ran = append(ran, node.Intent)
}
}
return evidence, suppressed, ran, edges, nil
}
// AnswerPlan answers a COMPOUND cross plane question by planning over the closed Registry. It runs the EEO
// and protected attribute guards FIRST (refuse on either), extracts the available params, gets the stable
// plan (sampled planSelfConsistencyK times, only the majority intent set), and falls back to AnswerRAG
// unchanged when the plan is empty (so a non compound or unbindable question is byte for byte unchanged).
// Otherwise it runs the plan, narrates ONE answer over the union of node evidence through o.generate (state
// only what the union supports, report a suppressed bench count as withheld and never guess a number for it,
// never compute a match score), and o.finalize behind the faithfulness gate. It emits the counts only
// aedt.ai.plan_answered audit: the ordered ran intent NAMES, the available param KINDS (never values), the
// node count, the suppressed count, the faithfulness verdict, and the self consistency outcome. The audit
// carries NO param value, NO candidate id, NO raw count, NO requirement id, NO answer text, and NO question.
func (o *Orchestrator) AnswerPlan(ctx context.Context, question string) (*Response, error) {
if ForbidEEO(question) != nil || MentionsProtectedAttribute(question) {
return o.refuse(ctx, RoutePlan, "the question references prohibited demographic data")
}
avail := extractAvailableParams(question)
plan := proposeStablePlan(ctx, question, avail, planSelfConsistencyK, o.proposePlan)
consistency := "stable"
if len(plan.Nodes) == 0 {
// No stable plan: a non compound or unbindable question is unchanged, it falls back to RAG.
return o.AnswerRAG(ctx, question, 8)
}
evidence, suppressed, ran, edges, err := o.runPlan(ctx, plan)
if err != nil {
return nil, err
}
if len(evidence) == 0 {
// Every node dropped (errors or empty results): degrade to RAG rather than narrate over nothing.
return o.AnswerRAG(ctx, question, 8)
}
deterministic := renderPlanUnion(ran, evidence)
model, genErr := o.generate(ctx,
"State ONE integrated answer using ONLY the listed evidence lines as evidence, one line per intent that ran. "+
"State only what the union of the evidence supports. A headcount shown as "+SuppressionSentinel+" was "+
"withheld to protect individuals: say it was withheld and NEVER guess or state a number for it. Never "+
"compute a match score and never assign a band. Do not invent an agency, a UEI, a dollar figure, a "+
"recompete, or a count the evidence does not contain.",
"QUESTION: "+question+"\n\nEVIDENCE:\n"+strings.Join(evidence, "\n"))
resp, aerr := o.finalize(ctx, RoutePlan, model, genErr, deterministic, evidence)
if aerr != nil {
return nil, aerr
}
// Counts only plan trace audit: the ordered ran names (the closed set, not PII), the available param KINDS
// (not values), the node count, the suppressed cell count, the faithfulness verdict, and the self
// consistency outcome. NO param value, NO candidate id, NO raw count, NO requirement id, NO answer text,
// NO question.
if err := o.audit(ctx, "aedt.ai.plan_answered", map[string]any{
"route": RoutePlan,
"intents": ran,
"available_kinds": availableKinds(avail),
"nodes": len(plan.Nodes),
"edges": edges, // upstream to downstream intent NAME pairs that fired, never a value
"suppressed": suppressed,
"faithful": resp.Faithful,
"rationale_source": resp.RationaleSource,
"self_consistency": consistency,
}); err != nil {
return nil, err
}
return resp, nil
}
// renderPlanUnion builds the deterministic union answer the planner ships when the model prose is withheld:
// one line per intent that ran, in the deterministic plan order, each line already its node's suppressed
// safe rendering. It names no candidate and computes no score; every bench count it contains already came
// through RenderCount, so the deterministic fallback is itself suppression safe.
func renderPlanUnion(ran []string, evidence []string) string {
var b strings.Builder
b.WriteString("Integrated plan answer across the planes. ")
b.WriteString(strings.Join(evidence, " "))
return b.String()
}
// benchPlaneWords are the conservative bench plane signal: a question that names one of these is asking about
// the caller's own bench (the suppressed person touching plane). Pairing a bench word with a market word is
// the compound signal that routes to the planner.
var benchPlaneWords = []string{"bench", "our staff", "our people", "who can staff", "clearance coverage", "structural gap"}
// marketPlaneWords are the conservative market plane signal: a question that names one of these is asking
// about the public market plane (agencies, awards, incumbents, recompetes, competitors, forecasts).
var marketPlaneWords = []string{"market", "incumbent", "competitor", "recompete", "award", "agency", "forecast", "vendor", "teaming", "buying pattern", "spend"}
// The additional planes so the planner (the multi step reasoner) engages ANY cross plane question, not only
// bench and market. Each group is kept semantically disjoint from bench and market and from each other, so a
// question that names several words WITHIN one plane still counts as one plane and is not compound.
var clearancePlaneWords = []string{"ts/sci", "polygraph", "compartment", "reciprocity", "sci access", "clearance level"}
var wagePlaneWords = []string{"wage", "salary", "labor rate", "bill rate", "oews", "pay band", "compensation", "prevailing wage"}
var skillPlaneWords = []string{"adjacent skill", "skill adjacency", "related skill", "capability", "competenc"}
var lifecyclePlaneWords = []string{"period of performance", "option year", "pop end", "renewal", "recompete timeline"}
// planeGroups is the full set of plane signals the planner can span. A compound question names words from
// TWO OR MORE distinct planes, so the single shot keyword router cannot bind it to one intent.
var planeGroups = [][]string{benchPlaneWords, marketPlaneWords, clearancePlaneWords, wagePlaneWords, skillPlaneWords, lifecyclePlaneWords}
// isCompoundQuestion reports whether a question spans TWO OR MORE distinct planes. Broadened from the original
// bench AND market rule so the planner engages the full closed Registry for any cross plane capture question,
// while a single plane question (however many words it names within that one plane) still falls through to the
// existing single shot path so a working route is never regressed. It is still conservative: fewer than two
// distinct planes returns false. It never refuses; the EEO guards run in AnswerPlan.
func isCompoundQuestion(question string) bool {
q := strings.ToLower(question)
planes := 0
for _, g := range planeGroups {
if containsAny(q, g) {
planes++
if planes >= 2 {
return true
}
}
}
return false
}
// guardTenantForBench reports whether the context carries a resolved authenticated tenant, the gate a bench
// node needs before it runs a person touching read. The runner drops a bench node without one so the plan
// path keeps the same auth posture answerBench enforces, rather than relying only on the RLS empty result
// fail safe. The G6 capture composition checks the same gate inside answerCapturePicture.
func guardTenantForBench(ctx context.Context) bool {
_, ok := tenant.OrgFromContext(ctx)
return ok
}