# GraphRAG Phase 3 (G6): The Loop Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax. **Goal:** Build G6 the keystone that closes the loop, requirement to bench to market, so the brain answers one integrated capture question for a requirement: who on our bench can structurally staff it, what is the market context (incumbent of record and the recompete clock for the customer), and where is the gap. This is the functional ANI milestone: the first layer that REASONS ACROSS the planes rather than within one. **Architecture:** G6 is an ORCHESTRATOR COMPOSITION, not new SQL. It reuses the already validated planes so each keeps its guarantees: the suppressing bench executors (G4 `BenchGapForRequirement`, G5 `BenchClearanceCoverage`) for the person touching legs, and the public market intents (G1 `IncumbentOfRecord` style segment reads, G9 `RecompeteClock`) for the market leg, resolving the requirement's market segment from `requirements.customer_agency` and, when present, the linked opportunity. It is a person touching bench intent (it reads the bench), so it is auth gated, suppressed, and counts only audited exactly like the G4 intents. It NEVER computes a match score: the per candidate score and ranking stay the frozen engine; G6 reports structural coverage counts and public market structure and narrates the gap. **Tech Stack:** Go 1.26, pgx/v5, shopspring/decimal, FIPS, no Python. **Branch:** `graphrag-foundation` (continues Phase 2). NO migration (reuses existing tables and intents). ModelVersion stays v1.4.0. Validate on M4 `brain_staging` (it has 2079 opportunity_records; the bench is seeded per test). --- ## Hard invariants (carry Phase 2 forward) 1. **G6 is person touching: auth gated and suppressed.** `capture_picture_for_requirement` reads the bench, so it is registered in `benchIntents`, requires `tenant.OrgFromContext` (refuse without it), and every bench count it surfaces comes from the suppressing executors through `RenderCount`, never a raw count. The market leg is public (no suppression). The composed audit is counts only, no candidate id, no raw sub k count. 2. **The frozen engine is the only scorer.** G6 reports the G4 STRUCTURAL gap (suppressed counts) and the public market structure. It computes NO match score and NO band. `internal/modules/matching/engine` is untouched; golden tests stay byte identical. 3. **Reuse, do not reimplement.** G6 calls the existing executors (`BenchGapForRequirement`, `BenchClearanceCoverage`, the market segment reads). It adds NO new SQL intent in the closed Registry beyond the one composed `capture_picture_for_requirement` name, and that name dispatches to a composition, not to `ExecuteIntent`. 4. **Honest insufficient data floor.** When a requirement has no resolvable market segment (no customer_agency and no linked opportunity), the market leg states that plainly rather than guessing. The price to win leg is a fixed honest floor ("labor rate data not yet ingested") until M5 lands. No leg is fabricated. 5. **Closed Registry, deterministic, EEO unreachable.** `capture_picture_for_requirement` is a FIXED registered name; the composition runs `ForbidEEO` and `MentionsProtectedAttribute` on the question first (inherited from `AnswerGraph`), references no eeo schema, and renders a deterministic, byte stable narrative. Decimal not float on any surfaced number. 6. Standard rules: no Python; no dashes in written output except URLs, CLI flags, import paths, identifiers; FIPS. --- ## File Structure - Create `internal/modules/ai/graph_loop.go` (+ test): the G6 composition `answerCapturePicture`, the requirement segment resolver, the deterministic capture narrative builder. - Modify `internal/modules/ai/tag.go`: register `capture_picture_for_requirement` (Param: requirement_id, ParamUUID). - Modify `internal/modules/ai/graph_route.go`: add the name to `graphIntents` AND `benchIntents` (it touches the bench); route it in `AnswerGraph` to `answerCapturePicture` (it is a bench intent, so the existing `IsBenchIntent` fork already catches it, dispatch from `answerBench` or a sibling); add conservative classifier keywords ("capture picture", "can we staff", "staffing and market for the requirement"). - Modify `internal/modules/ai/tag.go` ExecuteIntent guard already rejects bench intents, so `capture_picture_for_requirement` (a bench intent) is rejected on the raw path automatically; keep that. - Create `internal/integration/graph_loop_integration_test.go`: the live keystone proof. --- ## The segment resolver `requirements` carries `organization_id`, `opportunity_id` (nullable FK), `customer_agency` (text, nullable), `required_level`, `work_location`. It does NOT carry naics. Resolve the market segment for a requirement under RLS: - agency: `requirements.customer_agency` when non empty. - naics: when `opportunity_id` is set, read the linked opportunity's naics (join `opportunities` then, if the opportunity links an `opportunity_records` row, its naics); else empty. The agency keyed market intents (the recompete clock, the agency buying read) run on the agency alone; the (agency, naics) keyed intents (the competitor and incumbent reads) run only when BOTH are resolved, else that leg states the segment could not be bound (the honest floor). This is the same honesty discipline as the public market planes. --- ## Phase 3.G6.A: the segment resolver and the bench legs ### Task A1: resolve the requirement segment under RLS **Files:** - Create: `internal/modules/ai/graph_loop.go` - Test: `internal/modules/ai/graph_loop_test.go` - [ ] **Step 1: Write `RequirementSegment` and `resolveRequirementSegment`** under `RunInTenantTx`, reading `requirements` (RLS) joined left to the linked opportunity for agency and naics. Return `{Agency string; NAICS string; RequiredLevel string; Found bool}`. Use the authoritative tenant.DB pattern (see graph_clearance.go). Allowlist: `requirements`, `opportunities`, `opportunity_records` are already allowed; if the opportunity join needs a table not in the allowlist, STOP and report rather than adding one. - [ ] **Step 2: Unit test the guard** accepts the resolver SQL; register nothing yet. Commit. ### Task A2: the composed capture picture **Files:** - Modify: `internal/modules/ai/graph_loop.go` - Modify: `internal/modules/ai/tag.go` (register `capture_picture_for_requirement`), `internal/modules/ai/graph_route.go` (graphIntents + benchIntents + route + keywords) - [ ] **Step 1: Write `answerCapturePicture(ctx, requirementID)`** that, under the caller's tenant context: 1. requires `tenant.OrgFromContext` (refuse without it), 2. resolves the segment (Task A1), 3. runs the SUPPRESSED bench legs: `BenchGapForRequirement(ctx, o.db, requirementID, k)` and `BenchClearanceCoverage(ctx, o.db, k)`, building the suppressed cells exactly as `answerBench` does (RenderCount only), 4. runs the PUBLIC market legs when the segment binds: the recompete clock for the agency and, when naics is present, the incumbent or competitor segment read; each market leg is public and rendered verbatim, 5. adds the price to win HONEST FLOOR line ("labor rate data not yet ingested", until M5), 6. composes a deterministic capture narrative (the bench fit with suppressed counts, the market context, the gap, the price floor), narrates through `o.generate` and `o.finalize` so model prose ships only when faithful, else the deterministic narrative, 7. emits a counts only `aedt.ai.capture_answered` audit: the requirement is identified by intent only, the bench cell and suppressed counts, whether the segment bound, and k. NO candidate id, NO raw count, NO requirement id in the payload. - [ ] **Step 2: Register and route.** Register `capture_picture_for_requirement` (ParamUUID requirement_id) in the Registry. Add it to `graphIntents` and `benchIntents`. In `AnswerGraph`/`answerBench` dispatch, route this name to `answerCapturePicture` (it is a bench intent, so it is already auth gated and raw path rejected). Add conservative classifier keywords that route only on an explicit requirement UUID. Commit. --- ## Phase 3.G6.B: live keystone proof on staging ### Task B1: the loop proven end to end **Files:** - Create: `internal/integration/graph_loop_integration_test.go` - [ ] **Step 1: Seed** a per run org with a requirement (a customer_agency and required_level), a bench that exercises both sides of the structural gap (at least k that meet, at least k that do not, so a published answer is readable), and a market segment: seed a couple of `opportunity_records` and an award under the requirement's agency so the market leg has an incumbent and a recompete to report. Unique per run ids. - [ ] **Step 2: Assert** the composed answer (a) carries route graph, (b) reports the SUPPRESSED bench fit (the sentinel for a sub k cell, a real count when both sides are at or above k), (c) reports the market context (the agency recompete or incumbent line) from the seeded segment, (d) states the price to win honest floor, (e) refuses without a tenant context, (f) the `aedt.ai.capture_answered` audit holds no candidate id, no raw count, and no requirement id. Assert the frozen engine path is never invoked (no score in the answer). - [ ] **Step 3: Run on staging** (DSNs as in Phase 2), then the full graph suite for no regression, then commit. ```bash GOFIPS140=v1.0.0 CGO_ENABLED=0 go test -tags=integration -count=1 -run "TestG6|TestCapture|TestLoop" ./internal/integration/... -v ``` --- ## Self review checklist 1. **Suppression:** every bench count in the composed answer, the model prompt, and the audit comes from `RenderCount`; no raw sub k number appears. `capture_picture_for_requirement` is in `benchIntents` and rejected on the raw `ExecuteIntent` path. 2. **No score:** G6 reports structural counts and public market structure only; `internal/modules/matching` is untouched and byte identical. 3. **RLS + auth gate:** the resolver and the bench legs run under `RunInTenantTx`; the composition refuses without `tenant.OrgFromContext`. 4. **EEO unreachable:** no eeo reference; the safety EEO walk covers graph_loop.go. 5. **Honest floor:** an unbindable segment and the absent price data are stated plainly, never fabricated. 6. **Determinism, decimal, no dashes, FIPS.**