Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/graph_skill.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ai/graph_skill.go |
| Original SHA-256 | 272ea68f4a94e03de4317a86240042e56154367f7b3443b45b75fbd00d00ded8 |
| Git blob SHA | 9d70e63fbddd87fa4361f3390c0c7391ec1610a3 |
| Reading-copy SHA-256 | 272ea68f4a94e03de4317a86240042e56154367f7b3443b45b75fbd00d00ded8 |
Every recorded occurrence
These 12 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/graph_skill.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / internal/modules/ai/graph_skill.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓279 lines · 12.9 KB
package ai
// G3 skill and role adjacency graph. This file adds the curated, multi hop skill traversal as FIXED,
// named, parameterized QueryIntents. It is the deterministic counterpart to the live single hop
// SkillAdjacency in scaffolds.go: where SkillAdjacency reads the semantic embedding neighborhood, this
// walks the CURATED skill_relationships graph (the WS2 editorially reviewed substitutes_for and
// broader_than edges) and the ontology_crosswalk taxonomy, returning structure (the reachable skills,
// the accumulated curated strength, and the evidence path) that a caller routes to the frozen engine.
// The graph NEVER computes a match score, a band, or any employment decision number; it returns the
// adjacency a downstream scoring call may consult, exactly as the curated relationship layer already
// feeds the engine's partial credit. The model never authors this SQL: the recursive CTE text is static,
// reviewed, registered in the closed Registry, asserted by ValidateRegistry, and re guarded by
// GuardReadOnlyQuery on every execution.
//
// Recursive determinism (the AEDT reproducibility mandate). Both traversals carry, by construction:
// - a hard depth cap, bound as a parameter (default 3 hops), enforced in the recursive arm,
// - a cycle guard, a visited skill id array carried per row plus UNION (not UNION ALL), so a cycle in
// the curated graph can never loop and the walk always terminates,
// - a unique id tiebreaker on every ORDER BY, so the same inputs yield byte identical rows.
//
// The accumulated curated strength is carried through the recursion as Postgres numeric and is scanned
// into shopspring/decimal in Go, NEVER float64: a coverage or strength figure shown as evidence is a
// decision adjacent number and the platform's decimal rule applies. Cosine and embedding are NOT used
// here; this is the curated graph, not the semantic matcher (which lives in SkillAdjacency).
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/shopspring/decimal"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// IntentSkillSubstitutionClosure is the registered name of the transitive substitution closure intent.
const IntentSkillSubstitutionClosure = "skill_substitution_closure"
// IntentSkillGapBridge is the registered name of the gap bridge intent.
const IntentSkillGapBridge = "skill_gap_bridge"
// DefaultGraphDepth is the default hard depth cap for the recursive walks when a caller does not bind one.
const DefaultGraphDepth = 3
// maxGraphDepth bounds the depth a caller may request, so an oversized depth cannot turn a bounded walk
// into an unbounded one. The curated graph is shallow; three to five hops is the meaningful range.
const maxGraphDepth = 5
// skillSubstitutionClosureSQL is the FIXED recursive CTE for the substitution and broadening closure. It
// starts from a skill id ($1) and walks skill_relationships over the curated substitutes_for and
// broader_than edges (verified_at IS NOT NULL), to a bound depth ($2).
//
// Determinism, line by line:
// - the base arm seeds depth 1, the starting accumulated strength 1.0 (numeric), and the visited path
// array with the start id, so the first hop's strength is the edge strength itself,
// - the recursive arm multiplies the carried numeric acc_strength by the next edge strength (a
// multiplicative transitive confidence that stays inside 0 to 1), appends the next skill id to the
// visited path, and admits a hop only when c.depth < $2 (the hard depth cap) AND the next skill id is
// NOT already in the visited path (the cycle guard), so a cycle cannot loop,
// - UNION (not UNION ALL) is the additional duplicate defense the mandate requires,
// - the final SELECT joins skills for the readable name and orders by depth, then acc_strength
// descending, then the skill_id tiebreaker, then the full visited path, so the order is total even
// when two distinct evidence paths reach the same skill at the same depth and strength, and the
// output is byte stable on a re run.
//
// acc_strength is Postgres numeric end to end and is scanned into decimal.Decimal in Go, never float64.
const skillSubstitutionClosureSQL = `
WITH RECURSIVE closure AS (
SELECT
sr.to_skill_id AS skill_id,
1 AS depth,
sr.strength::numeric AS acc_strength,
ARRAY[sr.from_skill_id, sr.to_skill_id] AS path,
ARRAY[sr.relation] AS relations
FROM skill_relationships sr
WHERE sr.from_skill_id = $1
AND sr.verified_at IS NOT NULL
AND sr.relation IN ('substitutes_for', 'broader_than')
UNION
SELECT
sr.to_skill_id,
c.depth + 1,
c.acc_strength * sr.strength,
c.path || sr.to_skill_id,
c.relations || sr.relation
FROM skill_relationships sr
JOIN closure c ON sr.from_skill_id = c.skill_id
WHERE sr.verified_at IS NOT NULL
AND sr.relation IN ('substitutes_for', 'broader_than')
AND c.depth < $2
AND NOT (sr.to_skill_id = ANY(c.path))
)
SELECT
c.skill_id,
s.name AS skill_name,
c.depth,
c.acc_strength,
c.relations AS evidence_relations,
c.path AS evidence_path
FROM closure c
JOIN skills s ON s.id = c.skill_id
ORDER BY c.depth, c.acc_strength DESC, c.skill_id, c.path`
// skillGapBridgeSQL is the FIXED bridge intent. Given a required skill id ($1) that is not exactly held,
// it returns the nearest curated skills one or two hops away (the same bounded substitution and
// broadening closure, depth $2) UNIONed with any taxonomy or certification bridge from ontology_crosswalk
// that maps to the required skill's source soc_code, each row labeled with its bridge_kind and carrying
// the evidence path. The curated graph hops carry their accumulated numeric strength; the crosswalk hop
// is a categorical taxonomy link, so it carries the neutral strength 1.0 (an exact crosswalk equivalence)
// and a one element relation chain naming the crosswalk relation. Both arms are read only over
// allowlisted GLOBAL reference tables.
//
// The recursive determinism is identical to the closure: depth cap $2, the visited path cycle guard,
// UNION not UNION ALL, and the skill_id tiebreaker followed by the bridge_kind and the full evidence
// path on the final ORDER BY, so the order is total. acc_strength stays numeric.
const skillGapBridgeSQL = `
WITH RECURSIVE closure AS (
SELECT
sr.to_skill_id AS skill_id,
1 AS depth,
sr.strength::numeric AS acc_strength,
ARRAY[sr.from_skill_id, sr.to_skill_id] AS path,
ARRAY[sr.relation] AS relations
FROM skill_relationships sr
WHERE sr.from_skill_id = $1
AND sr.verified_at IS NOT NULL
AND sr.relation IN ('substitutes_for', 'broader_than')
UNION
SELECT
sr.to_skill_id,
c.depth + 1,
c.acc_strength * sr.strength,
c.path || sr.to_skill_id,
c.relations || sr.relation
FROM skill_relationships sr
JOIN closure c ON sr.from_skill_id = c.skill_id
WHERE sr.verified_at IS NOT NULL
AND sr.relation IN ('substitutes_for', 'broader_than')
AND c.depth < $2
AND NOT (sr.to_skill_id = ANY(c.path))
)
SELECT
c.skill_id,
s.name AS skill_name,
c.depth,
c.acc_strength,
'curated_relationship' AS bridge_kind,
c.relations AS evidence_relations,
c.path AS evidence_path
FROM closure c
JOIN skills s ON s.id = c.skill_id
UNION
SELECT
b.id AS skill_id,
b.name AS skill_name,
1 AS depth,
1.0 AS acc_strength,
'ontology_crosswalk' AS bridge_kind,
ARRAY[ox.relation] AS evidence_relations,
ARRAY[$1::uuid, b.id] AS evidence_path
FROM skills req
JOIN ontology_crosswalk ox ON ox.from_code = req.soc_code AND req.soc_code <> ''
JOIN skills b ON b.soc_code = ox.to_code AND b.soc_code <> '' AND b.id <> req.id
WHERE req.id = $1
ORDER BY depth, acc_strength DESC, skill_id, bridge_kind, evidence_path`
// The two intents above are registered in the closed Registry in tag.go (the FIXED entries
// skill_substitution_closure and skill_gap_bridge), so ValidateRegistry asserts them at boot (read only,
// eeo free, allowlisted) and ExecuteIntent runs them through the same RunInTenantTx, guard, and audit
// posture as every other TAG intent. The recursive SQL stays here, next to the typed read helpers, and
// the Registry entries reference these constants by name.
// ClampGraphDepth normalizes a requested depth to the bounded, deterministic range: a non positive or
// missing depth becomes DefaultGraphDepth, and a depth above maxGraphDepth is capped, so the walk is
// always bounded no matter what a caller binds.
func ClampGraphDepth(depth int) int {
if depth <= 0 {
return DefaultGraphDepth
}
if depth > maxGraphDepth {
return maxGraphDepth
}
return depth
}
// SkillGraphNode is one reachable skill from a curated traversal: the skill, the hop depth at which it was
// reached, the accumulated curated strength along the path (decimal, never float), the bridge kind that
// reached it, and the evidence path (the ordered relation chain and the ordered skill id chain). It is
// pure structure: there is no score, no band, and no employment decision number on it.
type SkillGraphNode struct {
SkillID string `json:"skill_id"`
SkillName string `json:"skill_name"`
Depth int `json:"depth"`
Strength decimal.Decimal `json:"strength"`
BridgeKind string `json:"bridge_kind,omitempty"`
Relations []string `json:"relations"`
Path []string `json:"path"`
}
// SkillSubstitutionClosure runs the registered skill_substitution_closure intent and scans the result into
// typed nodes, carrying the accumulated curated strength as decimal.Decimal (the numeric column is scanned
// directly into decimal, never through float64). It is the typed read for callers that want the curated
// neighborhood as structure; the generic ExecuteIntent path remains available for the narration layer. It
// opens the same RLS scoped, app role transaction as every other read; the tables are GLOBAL reference, so
// the walk is correct without any tenant data, and the uniform tenant posture gives one connection, audit,
// and timeout discipline.
func SkillSubstitutionClosure(ctx context.Context, db *tenant.DB, startSkillID string, depth int) ([]SkillGraphNode, error) {
if db == nil {
return nil, errors.New("ai: no database")
}
var out []SkillGraphNode
err := db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var e error
out, e = SkillSubstitutionClosureOnTx(ctx, tx, startSkillID, depth)
return e
})
return out, err
}
// SkillSubstitutionClosureOnTx is SkillSubstitutionClosure on a caller supplied transaction, so a module
// already inside a tenant transaction reuses the curated walk without nesting a transaction. It re guards
// the static SQL and scans the numeric strength into decimal.
func SkillSubstitutionClosureOnTx(ctx context.Context, tx pgx.Tx, startSkillID string, depth int) ([]SkillGraphNode, error) {
if err := GuardReadOnlyQuery(skillSubstitutionClosureSQL, ReadAllowlist); err != nil {
return nil, err
}
rows, err := tx.Query(ctx, skillSubstitutionClosureSQL, startSkillID, ClampGraphDepth(depth))
if err != nil {
return nil, err
}
defer rows.Close()
var out []SkillGraphNode
for rows.Next() {
var n SkillGraphNode
if err := rows.Scan(&n.SkillID, &n.SkillName, &n.Depth, &n.Strength, &n.Relations, &n.Path); err != nil {
return nil, err
}
out = append(out, n)
}
return out, rows.Err()
}
// SkillGapBridge runs the registered skill_gap_bridge intent and scans the result into typed nodes. Like
// the closure, the accumulated curated strength is carried as decimal.Decimal, never float64.
func SkillGapBridge(ctx context.Context, db *tenant.DB, requiredSkillID string, depth int) ([]SkillGraphNode, error) {
if db == nil {
return nil, errors.New("ai: no database")
}
var out []SkillGraphNode
err := db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var e error
out, e = SkillGapBridgeOnTx(ctx, tx, requiredSkillID, depth)
return e
})
return out, err
}
// SkillGapBridgeOnTx is SkillGapBridge on a caller supplied transaction. It re guards the static SQL and
// scans the numeric strength into decimal.
func SkillGapBridgeOnTx(ctx context.Context, tx pgx.Tx, requiredSkillID string, depth int) ([]SkillGraphNode, error) {
if err := GuardReadOnlyQuery(skillGapBridgeSQL, ReadAllowlist); err != nil {
return nil, err
}
rows, err := tx.Query(ctx, skillGapBridgeSQL, requiredSkillID, ClampGraphDepth(depth))
if err != nil {
return nil, err
}
defer rows.Close()
var out []SkillGraphNode
for rows.Next() {
var n SkillGraphNode
if err := rows.Scan(&n.SkillID, &n.SkillName, &n.Depth, &n.Strength, &n.BridgeKind, &n.Relations, &n.Path); err != nil {
return nil, err
}
out = append(out, n)
}
return out, rows.Err()
}