ThatAIguyA world of ideas

THE CONSCIOUSNESS ATLAS
Read. Experiment. Follow the evidence.

ConsciousnessLearn · Explore · Joseph Anady

Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.md

Source identity, revision, and recovered contents.

GitHub sourceSource text recovered

Source identity

FieldRecorded value
Source typeGitHub source
Original recordJanady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.md
DateNot specified in source
RepositoryJanady13/missionmatch
Revision1e9249840a4302ec82e123b9c9cb6f75badea3e4
Branchmaster
Path or archive memberdocs/plans/2026-06-30-graphrag-phase2-bench-clearance.md
Original SHA-2565ccea9824e3db3c827eced7176c270774c4c27ef24de37f06912b7909656d138
Git blob SHA0cbf82accf8eda13114d01336fe085f775ba003b
Reading-copy SHA-2565ccea9824e3db3c827eced7176c270774c4c27ef24de37f06912b7909656d138

Original source ↗

Every recorded occurrence

These 21 records contain identical original source bytes. Their paths and revisions remain part of the archive.

Original recordBranch or archiveRevision / identifierOriginal location
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdmaster1e9249840a4302ec82e123b9c9cb6f75badea3e4Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdfeature/aioe-upgrade2178e7298d0e5d9af83afa12402fd587f0ba6ed8Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdm5-price-to-win41000ad30fff4129fdaf5ec84e15ba14c27d4890Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdcollision-deconfliction4ee5a09972608ed89f8377c6665542d5f450f0a6Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdgraphrag-foundationb0ddeb9f633fd138d09309d1a17b7d13bc3a67f7Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdtime-to-fill-survivalb66e1e505863d4f17741a4b9ede73509e45bd6c6Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdmag-substrateb82c7100a492dec67dfd1eda6b3f53def859be35Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdcapture-readinessbae6af7cb2997e819174a9e9cfff5e16067a1975Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdphase2-connectorse038d4c687aec16f8bd9bc4d907eea14d91f8704Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdpresentation-readinesseb574acaa29d71ce4d0f852572e87c275c79fdb7Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdvendor-intelligencefe9796a19c261951d3f2565206c0c1b5ac294465Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdcollision-deconfliction4ee5a09972608ed89f8377c6665542d5f450f0a6Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdm5-price-to-win41000ad30fff4129fdaf5ec84e15ba14c27d4890Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdmaster1e9249840a4302ec82e123b9c9cb6f75badea3e4Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdmag-substrateb82c7100a492dec67dfd1eda6b3f53def859be35Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdtime-to-fill-survivalb66e1e505863d4f17741a4b9ede73509e45bd6c6Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdgraphrag-foundationb0ddeb9f633fd138d09309d1a17b7d13bc3a67f7Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdpresentation-readinesseb574acaa29d71ce4d0f852572e87c275c79fdb7Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdphase2-connectorse038d4c687aec16f8bd9bc4d907eea14d91f8704Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdfeature/aioe-upgrade2178e7298d0e5d9af83afa12402fd587f0ba6ed8Open original
Janady13/missionmatch / docs/plans/2026-06-30-graphrag-phase2-bench-clearance.mdcapture-readinessbae6af7cb2997e819174a9e9cfff5e16067a1975Open original

Recovered source transcript

# GraphRAG Phase 2: Bench and Clearance Planes Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax. **Goal:** Bring the FIRST person touching graph plane online under RLS: G4 the bench graph (structural questions about a tenant's own cleared workforce) and G5 the clearance lattice (reachability over the global clearance reference tables), guarded by a new deterministic small cell suppression layer so aggregate bench answers can never re identify an individual. **Architecture:** Two new intent files (`graph_clearance.go` for G5, `graph_bench.go` for G4) of named parameterized QueryIntents in the existing CLOSED Registry, plus one new cross cutting security control (`suppression.go`). G5 reference intents read the four GLOBAL clearance reference tables (no person data, no suppression). Every intent that touches the RLS protected person tables (`candidates`, `candidate_skills`, `candidate_evaluations`) runs under `RunInTenantTx` and pipes its aggregate cells through the suppression layer before any value leaves the process. The frozen engine stays the only scorer; suppression touches ONLY aggregate structural answers, NEVER the per candidate engine, score, or rationale. **Tech Stack:** Go 1.26, pgx/v5, sqlc, shopspring/decimal, FIPS (CGO_ENABLED=0 GOFIPS140=v1.0.0), no Python. **Branch:** `graphrag-foundation` (continues Phase 0 and Phase 1). Migration head 0032; Phase 2 adds NO migration (G5 reuses the 0023 clearance reference tables; G4 reuses the 0002 person tables; the suppression threshold is config, not schema). The ModelVersion stays v1.4.0 (no scoring change). Validate on M4 `brain_staging`. --- ## The suppression policy this plan implements (decided 2026-06-30) A four angle de identification research workflow (NIST SP 800-188, FCSM Working Paper 22, Sweeney k anonymity, Machanavajjhala l diversity, NCES and OPM and Census and EEOC precedent, Aggarwal high dimensionality, Dinur Nissim reconstruction, NYC LL144) set the policy. The findings and the founder threat model refinement: 1. **k = 10 hard floor.** Any aggregate structural bench cell contributed to by fewer than 10 distinct individuals is suppressed. This satisfies every standard's floor at once (FCSM and NIST endorse 3 or 5, UK Data Service defaults to 10 for queryable surfaces). Override is `MM_BENCH_SUPPRESSION_K`, clamped to never go below 5. 2. **k is the least important control; the architecture carries the protection.** On a 50 to 200 person bench with about six rich quasi identifiers, every individual is near population unique, so a per query threshold is theater against the binding attack, query differencing. The load bearing controls are query auditing, fixed generalization, and (for the cross tenant surface) a privacy budget. 3. **Threat model scope.** G4 answers a tenant's questions about ITS OWN bench to ITS OWN authenticated owners, who already own those candidate records. That is NOT the published statistics threat. So Phase 2 ships the deterministic owner queries own data controls: the k = 10 floor with a value free sentinel, complementary (secondary) suppression with a margin back solve check, a fixed pre published generalization basis, and counts only auditing. The heavyweight defenses (a per querier differential privacy epsilon budget with composition, tracker and overlap refusal, calibrated noise) are DEFERRED to the cross tenant market surface, the WS8 cross tenant DP path that is flag off today, where differencing on a crowd you do NOT own actually binds. This matches the locked WS8 Posture B. 4. **l diversity is not computed in Phase 2, by design.** The homogeneity attack needs a sensitive attribute inside the released group. The graph carries NO protected attribute: the EEO module is unreachable (ForbidEEO plus the allowlist excludes the eeo schema). So there is no sensitive value to infer within G4. The l diversity guard would matter only if a protected attribute entered the graph, which a stronger control already forbids. This is documented in `suppression.go`, not implemented as dead code. --- ## Hard invariants (every task) 1. **Suppression boundary, never crossed.** Suppression touches ONLY aggregate STRUCTURAL bench answers (counts and distributions over bench attributes). It MUST NEVER alter, round, noise, or withhold: (a) any individual candidate's own match score, rationale, decision trace, or AEDT notice; (b) the deterministic per candidate scoring engine path (`internal/modules/matching/engine`, ModelVersion pinned), which runs on the full record with zero suppression; (c) the human in the loop, never auto reject decision. The privacy floor and the explainability mandate act on different objects, so there is no conflict. 2. **RLS for every person touching intent.** Every G4 intent and the one G5 bench intersection intent takes `db *tenant.DB` and runs inside `RunInTenantTx`, so the app role reads only the caller's org (composite FK plus `app.current_org_id`). No intent reads a person table through the migrator pool. G5 reference intents read only the global reference tables and may run on either pool, but follow the same signature for uniformity. 3. **The frozen engine is the only scorer.** No intent computes a match score or band. Bench aggregates (counts, readiness distributions sourced from the already audited `candidate_evaluations`, coverage ratios) are computed in `shopspring/decimal` and are explicitly NOT match scores. `internal/modules/matching/engine` is untouched; golden and frozen tests stay byte identical. 4. **Closed Registry, deterministic, EEO unreachable.** Every traversal is a FIXED named QueryIntent, asserted by `ValidateRegistry`, re guarded by `GuardReadOnlyQuery`. Any recursive CTE carries a depth cap, a cycle guard (visited path plus `UNION`), a unique id tiebreaker, and a base term `::numeric` cast. Every list ORDER BY ends with a unique id tiebreaker for byte stable output. No intent references the eeo schema; the `safety_test.go` EEO walk covers the new files; the audit carries counts only. 5. **Decimal not float.** No `float64` or `::float8`/`::real`/`::double precision` touches a count, ratio, or readiness aggregate surfaced as evidence; scan numeric into `shopspring/decimal`. Headcounts are `int` (a headcount is an integer, suppressed as a unit). 6. **Fixed generalization basis (pre published, never per query adaptive).** Quasi identifiers are generalized to a fixed lattice before counting: `clearance_level` and `clearance_compartments` and `available` are already coarse enums or boolean and are used as is; `location` generalizes to a US state code through the existing location resolver, else the bucket `"unspecified"`; a skill family grouping generalizes to the ontology parent term through `skill_relationships` broader edges. Per query adaptive coarsening is itself a leak and is forbidden. 7. Standard rules: no Python; no dashes in written output except URLs, CLI flags, import paths, identifiers; both cloud core ANSI SQL plus the existing pgvector, no Neptune/Cosmos/cloud specific graph primitive; FIPS. --- ## File Structure - Create `internal/modules/ai/suppression.go` (+ `suppression_test.go`): the deterministic small cell suppression control. The security critical unit; exhaustively tested. - Create `internal/modules/ai/graph_clearance.go` (+ `_test.go`): G5 intents. Reference reachability over the global clearance tables, plus one suppressed bench coverage intent. - Create `internal/modules/ai/graph_bench.go` (+ `_test.go`): G4 bench aggregate intents, all suppressed. - Modify `internal/modules/ai/safety.go`: add the four clearance reference tables to `ReadAllowlist`. - Modify `internal/modules/ai/tag.go`: register the new intents. - Modify `internal/modules/ai/graph_route.go`: add the new intents to `graphIntents` and a few conservative NL classifier keywords; gate bench intents behind an authenticated tenant context. - Integration tests under `internal/integration/`: RLS scoped, suppression applied, differencing refused, proven live on `brain_staging`. **Reference template:** follow `internal/modules/ai/graph_market.go` for the intent file shape (a `const xxxSQL` string, a `type XxxRow struct`, a `func XxxName(ctx, db *tenant.DB, params) ([]XxxRow, error)` executor that scans into decimal), and `internal/modules/ai/tag.go` for registration. --- ## Phase 2.A: the suppression layer (the new security control) ### Task A1: the deterministic small cell suppression function **Files:** - Create: `internal/modules/ai/suppression.go` - Test: `internal/modules/ai/suppression_test.go` - [ ] **Step 1: Write the failing tests** ```go package ai import ( "testing" ) func TestSuppressBelowThresholdHidesSmallCells(t *testing.T) { // Total is 40, above k, so the answer is allowed. Cells under 10 are hidden. cells := []Cell{ {Key: []string{"secret"}, Count: 22}, {Key: []string{"top_secret"}, Count: 12}, {Key: []string{"ts_sci"}, Count: 6}, } out := Suppress(cells, 10) if out[0].Suppressed || out[1].Suppressed { t.Fatal("cells at or above k must not be suppressed") } if !out[2].Suppressed { t.Fatal("a cell of 6 (below k=10) must be suppressed") } } func TestSuppressComplementaryHidesASecondCell(t *testing.T) { // Exactly one cell is naturally below k. If only it were hidden, it could be recovered as // total minus the published cells. So a second (the smallest published) cell must also hide. cells := []Cell{ {Key: []string{"secret"}, Count: 30}, {Key: []string{"top_secret"}, Count: 14}, {Key: []string{"ts_sci"}, Count: 6}, } out := Suppress(cells, 10) suppressed := 0 for _, c := range out { if c.Suppressed { suppressed++ } } if suppressed < 2 { t.Fatalf("complementary suppression must hide at least 2 cells, got %d", suppressed) } // The smallest published cell (top_secret, 14) is the one chosen to also hide, not the larger. if out[1].Key[0] != "top_secret" || !out[1].Suppressed { t.Fatal("the smallest published cell must be the complementary suppression") } } func TestSuppressRefusesWhenTotalBelowThreshold(t *testing.T) { // If the whole matched set is below k, even its existence is sub threshold: hide everything. cells := []Cell{ {Key: []string{"secret"}, Count: 4}, {Key: []string{"ts_sci"}, Count: 3}, } out := Suppress(cells, 10) for _, c := range out { if !c.Suppressed { t.Fatal("when the total is below k every cell must be suppressed") } } } func TestSuppressNoSmallCellsLeavesAllPublished(t *testing.T) { cells := []Cell{ {Key: []string{"secret"}, Count: 22}, {Key: []string{"top_secret"}, Count: 18}, } out := Suppress(cells, 10) for _, c := range out { if c.Suppressed { t.Fatal("no cell below k means nothing is suppressed") } } } func TestSuppressIsDeterministic(t *testing.T) { cells := []Cell{ {Key: []string{"a"}, Count: 6}, {Key: []string{"b"}, Count: 6}, {Key: []string{"c"}, Count: 28}, } a := Suppress(append([]Cell(nil), cells...), 10) b := Suppress(append([]Cell(nil), cells...), 10) for i := range a { if a[i].Suppressed != b[i].Suppressed { t.Fatal("suppression must be deterministic across runs") } } } ``` - [ ] **Step 2: Run the tests to verify they fail** Run: `go test ./internal/modules/ai/ -run TestSuppress -v` Expected: FAIL (Cell, Suppress undefined). - [ ] **Step 3: Write the implementation** ```go package ai import ( "os" "sort" "strconv" "github.com/shopspring/decimal" ) // BenchSuppressionThreshold is the small cell suppression floor k for aggregate structural answers about // a tenant bench. Cells contributed to by fewer than k distinct individuals are suppressed. Set by the // 2026-06-30 de identification research (NIST SP 800-188, FCSM WP22, l diversity, NYC LL144): k = 10 is // the hard floor; the load bearing protection is the architecture, not k. l diversity is intentionally // NOT computed here: the graph carries no protected attribute (the EEO module is unreachable), so there // is no sensitive value for the homogeneity attack to infer. The heavyweight differencing defenses (a per // querier DP epsilon budget, tracker and overlap refusal, calibrated noise) live on the cross tenant // market surface (flag off today), not on this owner queries own data path. const BenchSuppressionThreshold = 10 // minSuppressionThreshold is the floor below which k may never be configured (the research dissent's hard // line). Any MM_BENCH_SUPPRESSION_K under this is ignored. const minSuppressionThreshold = 5 // SuppressionSentinel is the single value free token rendered for every suppressed cell. It is identical // for a true zero and any cell of 1 to k-1, so the token itself carries no count signal: a real 0 and a // small nonzero are indistinguishable. const SuppressionSentinel = "below reporting threshold" // Cell is one aggregate cell in a bench answer: a generalized quasi identifier bucket and the count of // DISTINCT individuals contributing to it. Value is an optional decimal aggregate (for example a median // readiness) suppressed alongside the count. Cells of a single intent form a PARTITION of the matched set // (each matched individual lands in exactly one bucket), so the counts sum to the matched total. type Cell struct { Key []string Count int Value decimal.Decimal HasValue bool Suppressed bool } // ConfiguredSuppressionThreshold reads MM_BENCH_SUPPRESSION_K, clamps it to never drop below // minSuppressionThreshold, and defaults to BenchSuppressionThreshold. func ConfiguredSuppressionThreshold() int { k := BenchSuppressionThreshold if v := os.Getenv("MM_BENCH_SUPPRESSION_K"); v != "" { if n, err := strconv.Atoi(v); err == nil && n >= minSuppressionThreshold { k = n } } return k } // Suppress applies deterministic small cell suppression to one intent's partition of cells. The input // order is preserved; only the Suppressed flag is set. The rules: // // 1. Refuse the whole answer when the matched total is below k: even the existence of the set is sub // threshold, so every cell is suppressed. // 2. Primary: suppress any cell with Count < k (Count of 0 to k-1). // 3. Complementary: if exactly one cell ends up suppressed, its value is recoverable as the total minus // the published cells, so suppress the smallest published cell as well. This guarantees at least two // hidden cells whenever any cell is hidden, the FCSM Working Paper 22 secondary suppression rule. // // Suppress is pure and deterministic: the complementary choice breaks ties by smallest count then by Key // order, so the same input always yields the same suppression pattern. func Suppress(cells []Cell, k int) []Cell { if k < minSuppressionThreshold { k = minSuppressionThreshold } total := 0 for _, c := range cells { total += c.Count } if total < k { for i := range cells { cells[i].Suppressed = true } return cells } suppressedCount := 0 for i := range cells { if cells[i].Count < k { cells[i].Suppressed = true suppressedCount++ } } if suppressedCount == 0 { return cells } if suppressedCount == 1 { // Choose the smallest published cell to also hide (least utility lost), tie broken by Key order // so the pattern is deterministic. Index order of published cells. published := make([]int, 0, len(cells)) for i := range cells { if !cells[i].Suppressed { published = append(published, i) } } sort.SliceStable(published, func(a, b int) bool { ia, ib := published[a], published[b] if cells[ia].Count != cells[ib].Count { return cells[ia].Count < cells[ib].Count } return keyLess(cells[ia].Key, cells[ib].Key) }) if len(published) > 0 { cells[published[0]].Suppressed = true } } return cells } // keyLess is a total order over generalized bucket keys for deterministic tie breaking. func keyLess(a, b []string) bool { for i := 0; i < len(a) && i < len(b); i++ { if a[i] != b[i] { return a[i] < b[i] } } return len(a) < len(b) } // RenderCount returns the displayable count for a cell: the sentinel when suppressed, else the number. // The audit and the faithfulness narration use this so a suppressed count never leaves the process. func RenderCount(c Cell) string { if c.Suppressed { return SuppressionSentinel } return strconv.Itoa(c.Count) } ``` - [ ] **Step 4: Run the tests to verify they pass** Run: `go test ./internal/modules/ai/ -run TestSuppress -v` Expected: PASS (all 5). - [ ] **Step 5: Add adversarial differencing and property tests** ```go func TestSuppressDifferencingTwoQueriesCannotIsolateOne(t *testing.T) { // The classic differencing attack: a broad query and a narrow query that differ by one person. With // suppression, the narrow query's small residual cell is hidden, so subtraction cannot isolate the // individual. This proves the per answer floor blocks the naive single subtraction (the cross query // budget that blocks the iterated attack is the deferred cross tenant control, documented). broad := []Cell{{Key: []string{"secret"}, Count: 11}} narrow := []Cell{{Key: []string{"secret"}, Count: 1}} // the broad set minus ten others, one person bo := Suppress(broad, 10) no := Suppress(narrow, 10) if bo[0].Suppressed { t.Fatal("a published cell of 11 should remain") } if !no[0].Suppressed { t.Fatal("a residual cell of 1 must be suppressed so subtraction cannot isolate the person") } } func TestSuppressNeverPublishesBelowK(t *testing.T) { // Property: after Suppress, no published cell has a count below k. Exhaustive over small grids. k := 10 for total := 0; total <= 60; total++ { for a := 0; a <= total; a++ { cells := []Cell{{Key: []string{"a"}, Count: a}, {Key: []string{"b"}, Count: total - a}} out := Suppress(cells, k) for _, c := range out { if !c.Suppressed && c.Count < k { t.Fatalf("published a cell of %d below k=%d (total=%d)", c.Count, k, total) } } } } } func TestSuppressAtLeastTwoOrZeroHidden(t *testing.T) { // Property: the hidden count is never exactly one (complementary suppression invariant). k := 10 for total := 0; total <= 60; total++ { for a := 0; a <= total; a++ { cells := []Cell{{Key: []string{"a"}, Count: a}, {Key: []string{"b"}, Count: total - a}} out := Suppress(cells, k) hidden := 0 for _, c := range out { if c.Suppressed { hidden++ } } if hidden == 1 { t.Fatalf("exactly one cell hidden (recoverable) at total=%d a=%d", total, a) } } } } ``` - [ ] **Step 6: Run the full suppression suite** Run: `go test ./internal/modules/ai/ -run TestSuppress -v` Expected: PASS (all 8). - [ ] **Step 7: Commit** ```bash git add internal/modules/ai/suppression.go internal/modules/ai/suppression_test.go git commit -m "feat(graphrag): deterministic small cell suppression for the bench plane" ``` --- ## Phase 2.B: G5 the clearance lattice The G5 reference intents read only the four GLOBAL clearance reference tables (`clearance_levels`, `polygraph_types`, `clearance_compartments`, `clearance_reciprocity`), so they carry no person data and no suppression. The reciprocity table is seeded empty, so the non rank reciprocity half is a documented scaffold that becomes richer when an FSO curates exceptions; the rank ladder is the live behavior today. One bench intersection intent (`bench_clearance_coverage`) touches `candidates` and is therefore suppressed and RLS scoped. ### Task B1: add the clearance reference tables to the read allowlist **Files:** - Modify: `internal/modules/ai/safety.go:124-150` - Test: `internal/modules/ai/safety_test.go` - [ ] **Step 1: Add the four tables to `ReadAllowlist`** (after the `award_incumbent_roles` entry): ```go // Global clearance reference tables (no PII, FK enforced reference data) for the G5 clearance lattice. "clearance_levels": true, "polygraph_types": true, "clearance_compartments": true, "clearance_reciprocity": true, ``` - [ ] **Step 2: Confirm the EEO walk still rejects** (no change needed, just run): `go test ./internal/modules/ai/ -run TestSafety -v` Expected: PASS. ### Task B2: the G5 clearance reachability intents **Files:** - Create: `internal/modules/ai/graph_clearance.go` - Test: `internal/modules/ai/graph_clearance_test.go` - Modify: `internal/modules/ai/tag.go` (register), `internal/modules/ai/graph_route.go` (graphIntents + keywords) Reference the `graph_market.go` shape. Define intent name constants, the SQL, the row types, and the executors. - [ ] **Step 1: Write `clearance_reachability`** (which required levels a held level satisfies, by the rank ladder, plus any seeded reciprocity exception): ```go package ai import ( "context" "github.com/missionworkforce/missionmatch/internal/platform/tenant" ) const ( IntentClearanceReachability = "clearance_reachability" IntentCompartmentRequirement = "compartment_requirement_closure" ) // clearanceReachabilitySQL returns, for a held clearance code, every required level it satisfies. The // base rule is the rank ladder (a held rank satisfies any required rank at or below it). A seeded // clearance_reciprocity row can ADD a satisfaction edge that the ladder would not grant (an agency // specific exception); it never removes one. Today clearance_reciprocity is empty, so this returns the // pure ladder, the live engine behavior. Deterministic ORDER BY on the required rank then code. const clearanceReachabilitySQL = ` SELECT held.code AS held_code, req.code AS required_code, req.rank AS required_rank, (held.rank >= req.rank OR EXISTS (SELECT 1 FROM clearance_reciprocity cr WHERE cr.held_code = held.code AND cr.required_code = req.code AND cr.satisfies)) AS satisfies FROM clearance_levels held CROSS JOIN clearance_levels req WHERE held.code = $1 ORDER BY req.rank DESC, req.code ASC ` type ClearanceReach struct { HeldCode string RequiredCode string RequiredRank int Satisfies bool } func ClearanceReachability(ctx context.Context, db *tenant.DB, heldCode string) ([]ClearanceReach, error) { var out []ClearanceReach err := db.RunInTenantTx(ctx, func(q tenant.Queryer) error { rows, err := q.Query(ctx, clearanceReachabilitySQL, heldCode) if err != nil { return err } defer rows.Close() for rows.Next() { var r ClearanceReach if err := rows.Scan(&r.HeldCode, &r.RequiredCode, &r.RequiredRank, &r.Satisfies); err != nil { return err } out = append(out, r) } return rows.Err() }) return out, err } ``` (NOTE to implementer: confirm the exact `tenant.DB` transaction helper name and `Queryer` type against `graph_market.go`; reuse whatever that file uses verbatim. Add `compartment_requirement_closure` over `clearance_compartments` in the same file, returning the kind sci or sap for each compartment a requirement parameter lists, used to explain a compartment gap. Both are global reference reads.) - [ ] **Step 2: Register both intents in `tag.go`** with `ParamEnum` for `held_code` (Allowed the six clearance codes) and `ParamText` for the requirement compartment list. Add both names to `graphIntents` in `graph_route.go` and a couple of conservative classifier keywords ("clearance satisfies", "compartment required"). Add a unit test asserting the ladder is monotonic (a higher held rank satisfies every required code a lower held rank does) so the intent stays pinned to the engine's clearance hierarchy. Commit. ### Task B3: the G5 bench clearance coverage intent (suppressed, RLS) **Files:** - Modify: `internal/modules/ai/graph_clearance.go` - Test: `internal/modules/ai/graph_clearance_test.go` - [ ] **Step 1: Write `bench_clearance_coverage`** (how many bench candidates hold each clearance level), running under RLS and piped through `Suppress`: ```go const IntentBenchClearanceCoverage = "bench_clearance_coverage" // benchClearanceCoverageSQL counts DISTINCT candidates per held clearance level for the caller's org. // RLS scopes it to the caller's bench. The COUNT is a headcount; the result is a partition of the bench // (every candidate has exactly one clearance_level), so the cells sum to the bench size and Suppress can // apply complementary suppression correctly. Deterministic ORDER BY on the level rank. const benchClearanceCoverageSQL = ` SELECT cl.code AS level_code, cl.rank AS level_rank, count(DISTINCT c.id) AS headcount FROM clearance_levels cl LEFT JOIN candidates c ON c.clearance_level = cl.code GROUP BY cl.code, cl.rank ORDER BY cl.rank DESC, cl.code ASC ` type BenchCoverageRow struct { LevelCode string LevelRank int Cell Cell } func BenchClearanceCoverage(ctx context.Context, db *tenant.DB, k int) ([]BenchCoverageRow, error) { var out []BenchCoverageRow err := db.RunInTenantTx(ctx, func(q tenant.Queryer) error { rows, err := q.Query(ctx, benchClearanceCoverageSQL) if err != nil { return err } defer rows.Close() for rows.Next() { var r BenchCoverageRow var headcount int if err := rows.Scan(&r.LevelCode, &r.LevelRank, &headcount); err != nil { return err } r.Cell = Cell{Key: []string{r.LevelCode}, Count: headcount} out = append(out, r) } return rows.Err() }) if err != nil { return nil, err } // Apply suppression over the partition before returning. The caller renders RenderCount, never the // raw Count, for a suppressed cell. cells := make([]Cell, len(out)) for i := range out { cells[i] = out[i].Cell } cells = Suppress(cells, k) for i := range out { out[i].Cell = cells[i] } return out, nil } ``` - [ ] **Step 2: Unit test the partition and suppression wiring** (a fake or a staging row set asserting a sub k level renders the sentinel and the partition sums to the bench size). Register the intent, add to graphIntents and keywords. Commit. --- ## Phase 2.C: G4 the bench graph Every G4 intent reads the RLS protected person tables, runs under `RunInTenantTx`, partitions the bench, and pipes the partition through `Suppress`. Follow the `BenchClearanceCoverage` shape exactly. ### Task C1: bench skill coverage **Files:** - Create: `internal/modules/ai/graph_bench.go` - Test: `internal/modules/ai/graph_bench_test.go` - [ ] **Step 1: Write `bench_skill_coverage`** (how many bench candidates hold a given skill, or each skill in an ontology family), partitioned to hold or not hold so the cells sum to the bench size, suppressed. Use `candidate_skills` joined to `candidates` (RLS), and generalize the skill to its ontology parent through `skill_relationships` broader edges when a family is asked. Provide `IntentBenchSkillCoverage`, the SQL, a `BenchSkillRow` with an embedded `Cell`, and the executor that calls `Suppress`. - [ ] **Step 2: Test, register, route, commit.** ### Task C2: bench readiness distribution **Files:** - Modify: `internal/modules/ai/graph_bench.go` - Test: `internal/modules/ai/graph_bench_test.go` - [ ] **Step 1: Write `bench_readiness_distribution`** (how many bench candidates fall in each band green, yellow, red FOR A GIVEN requirement), reading the ALREADY AUDITED `candidate_evaluations` (never recomputing a score; the band came from the frozen engine at evaluation time). Partition by band so the cells sum to the evaluated count, suppressed. This is the honest structural view: it reports the distribution of decisions the engine already made and audited, it does not score anyone. Provide `IntentBenchReadinessDistribution`, the SQL filtered by `requirement_id` under RLS, the row with an embedded `Cell` keyed by band, and the suppressed executor. - [ ] **Step 2: Test, register, route, commit.** ### Task C3: bench gap for a requirement (structural, not a score) **Files:** - Modify: `internal/modules/ai/graph_bench.go` - Test: `internal/modules/ai/graph_bench_test.go` - [ ] **Step 1: Write `bench_gap_for_requirement`** (how many bench candidates hold at least the required clearance AND a given fraction of the required skills, partitioned into meets and does not meet buckets), a STRUCTURAL coverage count, explicitly NOT a match score. State in the doc comment that the per candidate score and ranking remain the frozen engine; this intent only counts structural coverage to size a gap. Suppressed, RLS. - [ ] **Step 2: Test, register, route, commit.** --- ## Phase 2.D: routing, audit, and the least privilege gate ### Task D1: gate the bench intents behind an authenticated tenant context and counts only audit **Files:** - Modify: `internal/modules/ai/graph_route.go` - Modify: `internal/modules/ai/orchestrator.go` (audit emission) - Test: `internal/modules/ai/graph_route_test.go` - [ ] **Step 1: Require a tenant context for bench intents.** In `AnswerGraph`, a bench or bench coverage intent (the suppressed set) requires a resolved `(org_id, user_id)` from the session; with no authenticated tenant context it refuses (the G5 reference intents do not). Add the suppressed intent names to a `benchIntents` set used for this check. - [ ] **Step 2: Emit a counts only, suppression aware audit.** The audit row for a bench intent records the intent name, the number of cells returned, the number suppressed, and the configured k, and carries NO candidate id, NO PII, and NO raw sub k count. Assert in a test that the audit payload contains no candidate id and that a suppressed cell's count is absent. - [ ] **Step 3: Add the faithfulness narration rule** so a suppressed answer narrates only published cells plus a fixed line "N buckets withheld to protect individuals (fewer than k)". Commit. --- ## Phase 2.E: live integration on M4 brain_staging ### Task E1: RLS, suppression, and differencing proven live **Files:** - Create: `internal/integration/graph_bench_integration_test.go` - Create: `internal/integration/graph_clearance_integration_test.go` - [ ] **Step 1: Seed a per run org** with a bench large enough to exercise both sides of the floor: at least 10 candidates at one clearance level (published) and fewer than 10 at another (suppressed). Use a unique per run org id and unique candidate ids (the lesson from the G2 cross run accumulation bug, since some tables are global, though candidates is RLS scoped per org). - [ ] **Step 2: Assert RLS isolation** (a second org sees only its own bench; the coverage counts differ), **suppression** (the sub k level renders `SuppressionSentinel`, never the raw number, and at least two cells are hidden when any is), and **the partition sums to the seeded bench size for published plus suppressed cells**. - [ ] **Step 3: Assert the differencing refusal** end to end: a broad query that is published and a narrow query whose residual cell is one person both go through the live route, and the narrow residual is suppressed (the per answer floor holds live). - [ ] **Step 4: Assert the G5 reachability ladder live** (ts_sci satisfies secret, secret does not satisfy ts_sci) against the seeded reference tables, with `clearance_reciprocity` empty. - [ ] **Step 5: Run on staging.** Run (with the brain_staging DSNs exported as in Phase 1): ```bash GOFIPS140=v1.0.0 CGO_ENABLED=0 go test -tags=integration -count=1 -run "TestG4|TestG5|TestBench|TestClearance" ./internal/integration/... -v ``` Expected: PASS. - [ ] **Step 6: Run the full graph suite to confirm no regression**, then commit. ```bash GOFIPS140=v1.0.0 CGO_ENABLED=0 go test -tags=integration -count=1 -run "TestG1|TestG2|TestG3|TestG4|TestG5|TestG7|TestG9|TestGraphRoute|TestBench|TestClearance" ./internal/integration/... -v git add internal/integration/graph_bench_integration_test.go internal/integration/graph_clearance_integration_test.go git commit -m "test(graphrag): live RLS, suppression, and differencing proof for the bench and clearance planes" ``` --- ## Self review checklist (run before the final review subagent) 1. **Suppression boundary:** grep the new files for any path that suppresses or alters an individual candidate's own score or rationale. There must be NONE; suppression touches only the aggregate `Cell` partitions. The frozen engine and the per candidate handler are untouched. 2. **RLS:** every G4 intent and `bench_clearance_coverage` runs under `RunInTenantTx` and takes `db *tenant.DB`. No person table is read through the migrator pool. 3. **No float:** no `float64` or float SQL cast on any surfaced count, ratio, or readiness aggregate. 4. **EEO unreachable:** `safety_test.go` EEO walk covers the new files; no intent references the eeo schema; the allowlist still excludes it. 5. **Determinism:** every list ORDER BY ends with a unique tiebreaker; `Suppress` is pure and order preserving; no `Date.now`/random. 6. **Migration free:** Phase 2 adds no migration; the threshold is config (`MM_BENCH_SUPPRESSION_K`, floored at 5). 7. **Frozen golden held:** `go test ./internal/modules/matching/...` byte identical.

Search equations, builds, manuscript text, and recovered sources.