Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / docs/plans/2026-06-29-ws2-alias-relationship.md |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | docs/plans/2026-06-29-ws2-alias-relationship.md |
| Original SHA-256 | 1b2b2d5e6b39b548a9e08972a80af07ed08dc0bb55862e00fa275bcf91952317 |
| Git blob SHA | 5b490ed7dc706a0024ce5303d934309ea4999845 |
| Reading-copy SHA-256 | 1b2b2d5e6b39b548a9e08972a80af07ed08dc0bb55862e00fa275bcf91952317 |
Every recorded occurrence
These 3 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 / docs/plans/2026-06-29-ws2-alias-relationship.md | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / docs/plans/2026-06-29-ws2-alias-relationship.md | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / docs/plans/2026-06-29-ws2-alias-relationship.md | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓259 lines · 20.4 KB
# WS2 Alias and Relationship Layer Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task by task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the deterministic engine actually use the WS1 ontology, by resolving curated skill aliases (SIGINT to Signals Intelligence, as an exact match) and curated skill relationships (substitution and broader than, as partial credit) BEFORE the semantic matcher, without changing the frozen weights, bands, threshold, or the three golden scores.
**Architecture:** Add two global reference tables (`skill_aliases`, `skill_relationships`) that FK to the global `skills` table. Add a `DeterministicAliasResolver` (loads the two tables into a lazy cache, the same shape as `StoredSkillMatcher`) and a `ChainedResolver` that runs the resolver first and the existing semantic `StoredSkillMatcher` second. `ChainedResolver` satisfies the engine's existing `SkillMatcher` interface, so the engine package changes zero lines; the swap happens at service init. Promote a curated set of WS1 ontology terms into the matcher facing `skills` table so there are real skills to alias and relate. Bump the engine `ModelVersion` to v1.2.0 and register it.
**Tech stack:** Go 1.26, pgx/v5, goose, shopspring/decimal (all match math stays decimal), bge embeddings via the existing embed-backfill, the existing engine `SkillMatcher` seam. CGO_ENABLED=0, GOFIPS140=v1.0.0, no Python.
**The hard rule:** `go test ./internal/modules/matching/engine/...` (golden_test.go and frozen_test.go) MUST stay green at every step. The three frozen cases score byte for byte identically because they reference no alias or relationship edge and run with no matcher.
---
## Decisions locked for this plan (flag at review)
1. **Promotion scope (DECIDED, curated cleared set plus key tools):** promote into `skills` a curated GovCon relevant set, not the full 2797 skill kind rows. The batch is: the 15 Corpus Mining skills, the 10 mission_spine IC terms, an authored cleared workforce seed (RMF, ATO, Cross Domain Solutions, SIGINT, GEOINT, and the like, roughly 60 to 100 rows), AND the genuinely useful O*NET technology tools filtered to cleared tech relevance by tech category (development environment, operating system, network and security, database, programming language, configuration management, virtualization, cloud, analytical, so Splunk, Python, Kubernetes, Linux, Wireshark land but veterinary and sewing tools do not). Net `skills` table goes from 13 to roughly 150 to 300 clean rows. The bulk O*NET tools, NICE KSAs, and PSC codes stay in `ontology_terms` as the reference pool.
2. Alias hit scores Similarity 1.0, Exact true, and the trace flags it alias sourced for audit. Relationship `substitutes_for` and `broader_than` score the curated `strength` as a partial (Exact false), gated `strength >= 0.62`. `requires` and `related_to` never score. Only `verified_at IS NOT NULL` edges score.
3. Engine integration is the `ChainedResolver` (zero engine edits), not new branches inside matchOne.
4. ModelVersion bumps to v1.2.0, registered in `aedt_model_versions` by the migration, stamped by the engine.
---
## File structure
| File | Responsibility | New or Modify |
| --- | --- | --- |
| `db/migrations/0022_skill_alias_relationship.sql` | the two global tables, skills column adds, v1.2.0 registry row | Create |
| `db/seed/cleared_skills.sql` | the authored cleared workforce skill seed | Create |
| `db/seed/skill_aliases.sql` | curated alias and relationship seed | Create |
| `cmd/promote-ontology/main.go` | promote curated ontology_terms into skills (idempotent) | Create |
| `internal/modules/matching/aliasresolver.go` | `DeterministicAliasResolver` (lazy cache, ResolveAlias) | Create |
| `internal/modules/matching/aliasresolver_test.go` | resolver unit tests (alias and relationship) | Create |
| `internal/modules/matching/chainedresolver.go` | `ChainedResolver` satisfies `SkillMatcher` | Create |
| `internal/modules/matching/chainedresolver_test.go` | chained order tests | Create |
| `internal/modules/matching/engine/types.go` | add `AliasMatched bool` to `SkillMatch`; bump `ModelVersion` to v1.2.0 | Modify |
| `internal/modules/matching/engine/golden_test.go` | add the alias and relationship golden cases (3 frozen unchanged) | Modify |
| `internal/modules/matching/service.go` / `cmd/api/main.go` | inject `ChainedResolver` in place of bare `StoredSkillMatcher` | Modify |
---
## Task 1: Migration 0022, the alias and relationship tables and v1.2.0
**Files:** Create `db/migrations/0022_skill_alias_relationship.sql`
- [ ] **Step 1: Write the migration** (global tables mirroring the skills grant pattern; additive skills columns; register the model version like 0019)
```sql
-- +goose Up
-- WS2 deterministic alias and relationship layer. Two GLOBAL reference tables, like skills they carry no
-- organization_id and sit outside RLS, app role SELECT only, migrator writes. They FK to the global skills
-- table. The engine consults these (curated, ontology backed) before the semantic matcher: an alias is an
-- exact equivalence, a relationship is a curated partial credit. Weights, bands, and the 0.62 threshold are
-- unchanged; this registers ModelVersion v1.2.0 for the new curated match capability.
ALTER TABLE skills ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT '';
ALTER TABLE skills ADD COLUMN IF NOT EXISTS soc_code text NOT NULL DEFAULT '';
CREATE TABLE skill_aliases (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
skill_id uuid NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
alias_text text NOT NULL,
alias_type text NOT NULL CHECK (alias_type IN ('acronym','synonym','vendor_term','legacy')),
source text NOT NULL DEFAULT '',
confidence numeric(3,2) NOT NULL DEFAULT 1.0 CHECK (confidence >= 0 AND confidence <= 1),
verified_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (lower(alias_text))
);
CREATE INDEX idx_skill_aliases_skill ON skill_aliases (skill_id);
CREATE TABLE skill_relationships (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
from_skill_id uuid NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
to_skill_id uuid NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
relation text NOT NULL CHECK (relation IN ('requires','substitutes_for','broader_than','related_to')),
strength numeric(3,2) NOT NULL DEFAULT 1.0 CHECK (strength >= 0 AND strength <= 1),
source text NOT NULL DEFAULT '',
verified_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (from_skill_id, to_skill_id, relation)
);
CREATE INDEX idx_skill_rel_from ON skill_relationships (from_skill_id);
CREATE INDEX idx_skill_rel_to ON skill_relationships (to_skill_id);
GRANT SELECT ON skill_aliases TO missionmatch_app;
GRANT SELECT ON skill_relationships TO missionmatch_app;
-- Register the recalibrated model capability. The aedt_model_versions table (added by 0019, v1.1.0) has
-- columns version, weights, band_cuts, notes; the weights and band cuts are unchanged from v1.1.0.
INSERT INTO aedt_model_versions (version, weights, band_cuts, notes) VALUES (
'v1.2.0',
'{"clearance":0.25,"skills":0.25,"certifications":0.15,"customer_experience":0.15,"experience":0.10,"location_availability":0.10}',
'{"green":90,"yellow":75}',
'Adds the curated deterministic alias and relationship match layer (skill_aliases and skill_relationships). An alias resolves a curated ontology backed equivalence as an exact match (similarity 1.0); a substitutes_for or broader_than relationship contributes its curated strength as a partial match, gated at or above the 0.62 semantic threshold. The six component weights, the green and yellow band cuts, and the 0.62 threshold are unchanged from v1.1.0. Clearance remains a self reported indicator, never an input proxy for a protected attribute.'
) ON CONFLICT (version) DO NOTHING;
-- +goose Down
DELETE FROM aedt_model_versions WHERE version = 'v1.2.0';
DROP TABLE IF EXISTS skill_relationships;
DROP TABLE IF EXISTS skill_aliases;
ALTER TABLE skills DROP COLUMN IF EXISTS soc_code;
ALTER TABLE skills DROP COLUMN IF EXISTS source;
```
- [ ] **Step 2:** Confirm the `aedt_model_versions` columns by reading `db/migrations/0019_model_version_v1_1_0.sql`; if its columns differ (for example a different notes column name), match them exactly. Do NOT apply live (no local Postgres); the apply and rollback are validated in Task 9 on M4 staging.
- [ ] **Step 3: Commit** `git add db/migrations/0022_skill_alias_relationship.sql` with the no dash rules and the Co-Authored-By trailer.
---
## Task 2: Promote curated ontology terms into skills
**Files:** Create `cmd/promote-ontology/main.go`, `db/seed/cleared_skills.sql`
The matcher needs real skills to alias and relate. Promote, deduped by lowercased name, into `skills`: every Corpus Mining skill, every mission_spine term, and an authored cleared workforce seed.
- [ ] **Step 1:** Author `db/seed/cleared_skills.sql`: INSERT roughly 60 to 100 cleared workforce skills and tools into `skills (name, category, source)` with `ON CONFLICT (name) DO NOTHING`, covering the Galapagos lanes (SIGINT, GEOINT, HUMINT, Cross Domain Solutions, COMSEC, RMF, ATO, Splunk, Python, AWS, Azure, Kubernetes, Linux, Wireshark, SIEM, Intelligence Analysis, Cybersecurity, and the like). Source `'curated'`.
- [ ] **Step 2:** Write `cmd/promote-ontology/main.go` (Go, FIPS, migrator DSN), which runs the curated promotion SQL idempotently:
```sql
INSERT INTO skills (name, category, source)
SELECT DISTINCT ot.term, COALESCE(NULLIF(ot.category,''),'mined'), ot.source_name
FROM ontology_terms ot
WHERE ot.term_kind = 'skill' AND ot.source_name = 'Corpus Mining'
AND NOT EXISTS (SELECT 1 FROM skills s WHERE lower(s.name) = lower(ot.term))
ON CONFLICT (name) DO NOTHING;
INSERT INTO skills (name, category, source)
SELECT ms.term, 'mission_area', 'mission_spine'
FROM mission_spine ms WHERE ms.version='v1'
AND NOT EXISTS (SELECT 1 FROM skills s WHERE lower(s.name) = lower(ms.term))
ON CONFLICT (name) DO NOTHING;
-- O*NET technology tools filtered to cleared tech relevance by category, so Splunk, Python, Kubernetes,
-- Linux, Wireshark land but veterinary and sewing tools do not.
INSERT INTO skills (name, category, source, soc_code)
SELECT DISTINCT ot.term, COALESCE(NULLIF(ot.category,''),'tool'), 'onet', ot.code
FROM ontology_terms ot
WHERE ot.source_name = 'O*NET Web Services' AND ot.source_record_id LIKE 'onet-tech:%'
AND lower(ot.category) ~ 'development|operating system|network|security|database|program|configuration|virtualiz|cloud|analytical|data base|object or component'
AND NOT EXISTS (SELECT 1 FROM skills s WHERE lower(s.name) = lower(ot.term))
ON CONFLICT (name) DO NOTHING;
```
It also writes one `aedt.ontology.promoted` audit event with the inserted count. It runs as the migrator role. The implementer should first query `SELECT DISTINCT category FROM ontology_terms WHERE source_record_id LIKE 'onet-tech:%'` on a populated database to confirm the real category titles and tune the keyword set so the cleared tech tools land and the noise does not.
- [ ] **Step 3:** After promotion the new skill rows have NULL embeddings. The plan runs `embed-backfill -what skills` (existing command) to embed them via bge, then VectorGuard re verifies. This is an operator step in Task 9, not code here.
- [ ] **Step 4: Commit.**
---
## Task 3: The DeterministicAliasResolver
**Files:** Create `internal/modules/matching/aliasresolver.go`, `aliasresolver_test.go`
It lazily loads `skills` (name to id), `skill_aliases` (lower(alias_text) to canonical skill_id, verified only), and `skill_relationships` (from_id to list of (to_id, relation, strength), verified only) into a global cache, the same shape and graceful degradation as `StoredSkillMatcher`. It exposes `ResolveAlias(ctx, required, candidateSkills) (engine.SkillMatch, bool)`.
Resolution logic (deterministic, decimal):
1. Resolve the required skill to a canonical skill id: `required.SkillID` if set, else the id for `lower(required.Name)` via the skills map, else the canonical id for `lower(required.Name)` via the alias map. If none, return no match.
2. For each candidate skill, resolve its canonical id the same way (id, then name, then alias).
3. If a candidate's canonical id equals the required canonical id (and it was NOT already an exact id or name match, which the engine handled), return `SkillMatch{CandidateSkillID, CandidateName, Similarity: 1.0, Exact: true, AliasMatched: true, Matched: true}`.
4. Else, for each candidate canonical id, look up relationships from that id to the required id with relation in (`substitutes_for`, `broader_than`); if the best `strength >= 0.62`, return `SkillMatch{..., Similarity: strength, Exact: false, AliasMatched: false, Matched: true}` (record the relation in a trace friendly field).
5. Else return no match (the engine falls through to semantic).
- [ ] **Step 1:** Write `aliasresolver_test.go` first with an in memory resolver constructor (inject the three maps directly, no DB) and tests: (a) a candidate whose name is an alias of the required skill returns Similarity 1.0 Exact true AliasMatched true; (b) a candidate with a `substitutes_for` edge at strength 0.80 returns Similarity 0.80 Exact false; (c) a `substitutes_for` at strength 0.50 returns no match (below 0.62); (d) a `requires` or `related_to` edge returns no match; (e) an unverified alias (verified_at nil) is not loaded so returns no match. Run, confirm fail.
- [ ] **Step 2:** Write `aliasresolver.go`: the cache struct, a DB backed loader (`SELECT ... WHERE verified_at IS NOT NULL`), the in memory constructor for tests, and `ResolveAlias`. All comparisons in `shopspring/decimal`. Run, confirm pass.
- [ ] **Step 3:** `go test ./internal/modules/matching/...` and `go build ./...`, confirm green. Commit.
---
## Task 4: The ChainedResolver (satisfies SkillMatcher, zero engine edits)
**Files:** Create `internal/modules/matching/chainedresolver.go`, `chainedresolver_test.go`
```go
// ChainedResolver runs the deterministic alias and relationship resolver first, then the semantic
// matcher, and satisfies the engine SkillMatcher interface so the engine code is unchanged.
type ChainedResolver struct {
resolver *DeterministicAliasResolver
semantic engine.SkillMatcher
}
func NewChainedResolver(resolver *DeterministicAliasResolver, semantic engine.SkillMatcher) *ChainedResolver { ... }
func (c *ChainedResolver) Match(ctx context.Context, required engine.RequiredSkill, cands []engine.CandidateSkill, threshold decimal.Decimal) (engine.SkillMatch, bool) {
if m, ok := c.resolver.ResolveAlias(ctx, required, cands); ok {
return m, true
}
if c.semantic == nil {
return engine.SkillMatch{}, false
}
return c.semantic.Match(ctx, required, cands, threshold)
}
```
- [ ] **Step 1:** Write `chainedresolver_test.go`: a fake resolver and a fake semantic matcher prove the order (resolver wins when it matches; semantic runs only on resolver miss; nil semantic degrades to resolver only). Run, confirm fail.
- [ ] **Step 2:** Write `chainedresolver.go`. Run, confirm pass. `go build ./...`. Commit.
---
## Task 5: SkillMatch.AliasMatched and ModelVersion v1.2.0
**Files:** Modify `internal/modules/matching/engine/types.go`
- [ ] **Step 1:** Add `AliasMatched bool` to the `SkillMatch` struct (default false, so every existing construction is unchanged and the frozen tests still pass). Bump `ModelVersion` constant to `"v1.2.0"`.
- [ ] **Step 2:** Run `go test ./internal/modules/matching/engine/...`. The frozen test asserts `ModelVersion`; update that single assertion in `frozen_test.go` to `v1.2.0` (this is the deliberate, audited version bump, the only frozen change allowed). The three golden SCORES stay byte identical. Confirm green.
- [ ] **Step 3:** Commit.
---
## Task 6: New golden cases proving the new behavior (frozen three unchanged)
**Files:** Modify `internal/modules/matching/engine/golden_test.go`
- [ ] **Step 1:** Add two new golden cases to the snapshot, run through the engine WITH a ChainedResolver wired over an in memory resolver:
- Alias case: a requirement for skill SIGINT, a candidate holding Signals Intelligence (aliased to SIGINT). Skills component must be 100 and the total must equal the same requirement met by the exact skill. Assert the matched skill has `AliasMatched true`, `Similarity 1.0`.
- Relationship case: a requirement for skill B, a candidate holding skill A where A substitutes_for B at strength 0.80. Skills component reflects the partial; assert `Similarity 0.80`, `Exact false`.
- [ ] **Step 2:** Run `go test ./internal/modules/matching/engine/ -run TestGolden`. Confirm the three frozen cases are byte identical and the two new cases pass. Commit.
---
## Task 7: Seed the curated aliases and relationships
**Files:** Create `db/seed/skill_aliases.sql`
- [ ] **Step 1:** Author the seed: INSERT the load bearing GovCon aliases keyed to the promoted skills (resolve skill_id by name subselect), all `verified_at = now()`, for example:
```sql
INSERT INTO skill_aliases (skill_id, alias_text, alias_type, source, verified_at)
SELECT id, 'Signals Intelligence', 'acronym', 'curated', now() FROM skills WHERE name='SIGINT'
ON CONFLICT (lower(alias_text)) DO NOTHING;
```
Cover SIGINT, GEOINT, HUMINT, COMSEC (Communications Security), CDS (Cross Domain Solution), ATO (Authority to Operate), RMF (Risk Management Framework), and the other high frequency acronyms. Add a handful of substitutes_for and broader_than relationships (for example Kubernetes broader_than Docker if both are skills) with explicit strengths and `verified_at = now()`.
- [ ] **Step 2: Commit.** (This seed is applied in Task 9 on staging then production.)
---
## Task 8: Wire the ChainedResolver into the service
**Files:** Modify `internal/modules/matching/service.go` and/or `cmd/api/main.go`
- [ ] **Step 1:** Find where `matching.NewStoredSkillMatcher(db)` is constructed and passed to the engine path. Replace it with `matching.NewChainedResolver(matching.NewDeterministicAliasResolver(db), matching.NewStoredSkillMatcher(db))`. The engine still sees a `SkillMatcher`, so nothing else changes.
- [ ] **Step 2:** `go build ./...`, `go test ./...`, confirm green. Commit.
---
## Task 9: Validate on M4 staging, then load production
- [ ] **Step 1:** On a fresh `brain_staging` apply 0021 then 0022 (goose from M2 as migrator, the WS1 pattern), confirm both up and the 0022 down both work.
- [ ] **Step 2:** Provision a real skills set: apply `db/seed/cleared_skills.sql`, run `cmd/promote-ontology` (needs ontology_terms; if staging is empty, run ontology-sync first or accept corpus 0), then `db/seed/skill_aliases.sql`, then `embed-backfill -what skills`, then verify VectorGuard passes.
- [ ] **Step 3:** Prove the resolver end to end with a tiny screen: a candidate holding Signals Intelligence scores Skills 100 against a SIGINT requirement (alias), and a substitute skill scores the partial. Verify the served score audit records the AliasMatched flag.
- [ ] **Step 4:** Production load (gated, backup first, like the WS1 cutover): apply 0022 to production via goose, apply the two seeds, run promote-ontology, run embed-backfill -what skills, verify. Restart mm-api so the resolver cache loads (the matcher caches on first use; a restart picks up the new skills and edges).
- [ ] **Step 5:** Final commit and a final whole branch code review, then the finishing handoff.
---
## Self-Review
1. **Spec coverage:** WS2 in `docs/AI_GROUNDING_PROGRAM.md` calls for the alias and relationship layer with exact match before semantic, prerequisites and substitutions, and the engine staying the floor. Covered: tables (Task 1), promotion so there are skills to relate (Task 2), the resolver (Task 3), the zero edit engine integration (Tasks 4, 5, 8), the frozen preservation and new golden proof (Tasks 5, 6), the curated seed (Task 7), staging then production (Task 9). WS3 clearance is a separate plan.
2. **Frozen contract:** the only allowed frozen change is the ModelVersion assertion bump to v1.2.0 (Task 5); the three golden SCORES stay byte identical because they use no matcher and no alias or relationship edge.
3. **Type consistency:** `SkillMatch` (with the new `AliasMatched bool`), the `SkillMatcher` interface, `ChainedResolver`, and `DeterministicAliasResolver.ResolveAlias` are used identically across Tasks 3, 4, 6, 8.
## Open decision for owner review
The promotion scope (decision 1 above): curated roughly 100 to 150 skills, not the full 2797 skill kind rows. Confirm, or widen to include the broad O*NET tool set.