Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / docs/superpowers/specs/2026-07-20-recruitmatch-design.md |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | docs/superpowers/specs/2026-07-20-recruitmatch-design.md |
| Original SHA-256 | f0d1317e48d1d5415b9b53b3318be7642d4e4bbd4ca34635417c049875e30e2e |
| Git blob SHA | de1fa4e1fe5d822ce0d4abb8f058f895f1c89d02 |
| Reading-copy SHA-256 | f0d1317e48d1d5415b9b53b3318be7642d4e4bbd4ca34635417c049875e30e2e |
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 / docs/superpowers/specs/2026-07-20-recruitmatch-design.md | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / docs/superpowers/specs/2026-07-20-recruitmatch-design.md | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓268 lines · 13.8 KB
# recruitMATCH and org branding: design
Status: proposed, awaiting review
Date: 2026 07 20
Author: design session with Joseph
Branch: feature/aioe-upgrade
## 1. Purpose
Add **recruitMATCH**, a fourth product tool alongside missionMATCH, screenMATCH and slateMATCH,
giving recruiters an operational dashboard over work the platform already performs. Add
**per organization branding** so the application white labels for every client that buys it.
FG Talent Partners is the first client of missionMATCH. It is not the only client. Nothing in this
design may be FG specific. FG is a row in `organizations` and nothing more.
## 2. Non goals
These are explicitly out of scope. Each would be its own design.
1. No applicant tracking spine. No client account entity, job requisition entity, hiring manager,
interview, offer, placement, bill rate, pay rate, invoice, recruiter activity log, or task and
reminder tables. Verified absent across all 47 migrations and against the live schema.
2. No change to the scoring engine. `engine.ModelVersion` stays `v1.4.0` and the six weights are
untouched.
3. No per tenant database routing. That work lives unmerged on `per-tenant-routing` and is a
separate project. Tenancy here is the existing shared database plus forced RLS.
4. No orchestration layer. Parked.
5. No change to the EEO boundary. recruitMATCH must never read the `eeo` schema.
## 3. What a tool is in this codebase
Verified from the existing three. A tool is a repeatable structure, not a concept.
Frontend
* `frontend/src/tools/<tool>/brand.ts` exporting `{key, name, tagline, accent, basePath}`
* `frontend/src/tools/<tool>/api.ts` with `const NS = '/<tool>'` and a client object
* `frontend/src/tools/<tool>/queries.ts` with hooks keyed `['<tool>', resource, ...params]`
* `frontend/src/tools/<tool>/*.tsx` screens, named exports, no default export
* one entry in the `TOOLS` array in `frontend/src/components/AppShell.tsx`
* one accent token in the `@theme` block of `frontend/src/index.css`
Backend
* `internal/modules/<tool>/{dto,service,service_queries,handler}.go`
* `r.Route("/<tool>", handler.Routes)` in `cmd/api/main.go`
## 4. How the tools commingle, and how recruitMATCH joins
screenMATCH and slateMATCH are not parallel silos. They share one `*matching.Handler` instance, and
`matching.ReadRoutes` is mounted under every tool prefix, so `/api/screenmatch/candidates` and
`/api/slatematch/candidates` answer identically.
The deeper join is through shared tables:
```
screenMATCH scores a candidate writes candidate_evaluations
slateMATCH builds a slate writes slates and slate_members
missionMATCH Command Center aggregates both into readiness
recruitMATCH aggregates both into recruiting operations
```
recruitMATCH joins the family two ways:
1. It mounts `matching.ReadRoutes` under `/recruitmatch`, exactly as the other three do.
2. It reads the same tables the other tools write. recruitMATCH is the outcome view of work performed
in screenMATCH and slateMATCH. It introduces no private tables.
## 5. Data contract, panel by panel
Every source below was verified against the migrations and the live schema. Panels with no source
render an explicit empty state that names the missing feed. They never render a fabricated number.
### Backed by real data
| Panel | Source |
|---|---|
| Active jobs | `requirements` where parent `opportunities.status = 'open'` or `opportunity_id is null` |
| New candidates | `candidates.created_at` inside the window |
| Submissions | `slate_members.created_at` inside the window |
| Screened | `candidate_evaluations.evaluated_at` inside the window |
| Band mix, green yellow red | `candidate_evaluations.band` |
| Approvals and rejections | `slate_members.decision` with `decided_at` |
| Decision velocity | `slate_members.decided_at` minus `slate_members.created_at` |
| Recruiter attribution | `slate_members.decided_by` and `candidate_evaluations.evaluated_by` joined to `users` |
| Top active jobs | `requirements` left joined to a count of `slate_members` |
| Clearance breakdown | `candidates.clearance_level` joined to `clearance_levels` |
| Alerts feed | existing `GET /api/alerts`, populated by the scheduled `alert-sweep` timer |
| Recommended actions | existing `GET /api/recommendations/proactive`, populated by `staffing-proactive-sync` |
### Not backed, rendered as an explicit empty state
| Panel | Why |
|---|---|
| Interviews | no interview entity exists |
| Offers | no offer entity exists |
| Placements | no placement entity. `vendor_submissions.status = 'placed'` exists but is keyed by vendor, so a recruiter placement cannot be recorded |
| Revenue | no bill rate, pay rate, margin or invoice anywhere in the schema |
| Average time to fill | `requirements` has no fill date. Migration 0039 seeds a flat hazard prior and feeds from nothing |
| Candidate source breakdown | `candidates.source` is `CHECK (seed, external)`. Job board attribution cannot be expressed |
| Activity trend, placement series | the other three series are real. The placement series has no source |
### Two deliberate substitutions
The mockup shows an Upcoming Tasks panel and an AI Insight panel. Neither has a data source. Both
are replaced with feeds that are real and already accumulating behind no interface today:
* Upcoming Tasks becomes the **Alerts** feed from `GET /api/alerts`.
* AI Insight becomes **Recommended actions** from `GET /api/recommendations/proactive`.
This trades two invented panels for two panels with live scheduled data.
## 6. Backend design
New module `internal/modules/recruitmatch`, following the Command Center vertical slice, which is
the proven aggregation pattern here.
```
dto.go View plus nested panel types. Every money and percentage value is a string.
service.go Service{db, audit, clock}. One entry point:
RecruiterDashboard(ctx context.Context, asOf time.Time) (View, error)
service_queries.go buildTemps plus SQL panel builders. SQL returns counts only.
derive.go pure banding, classification and copy. No ctx, no tx, DB free tests.
handler.go Routes(r chi.Router), as_of parsing, error mapping.
*_test.go pure tests against hand built View structs.
```
Rules carried over from Command Center because they are load bearing:
1. **One** `RunInTenantTx` for the whole request.
2. **Keep the GUC assertion.** `buildTemps` refuses to aggregate when `app.current_org_id` is unset.
On a product serving many clients this is the difference between a bug and a cross tenant breach.
3. TEMP tables prefixed `rm_`, all `ON COMMIT DROP`, so they can never collide with the `cc_` temps.
4. SQL returns counts only. All banding happens in Go, importing `engine.BandGreenMin` and
`engine.BandYellowMin` rather than redeclaring thresholds.
5. `as_of` is bound as `$1::date`. No `now()` and no `current_date` anywhere in the module, so a
fixed `as_of` produces byte identical output.
6. All arithmetic uses `shopspring/decimal`. Percentages use a helper that guards a zero denominator.
7. Do **not** copy the `upcomingOpportunities` shape from Command Center. It is a real N plus 1.
### Routes
```go
r.Route("/recruitmatch", recruitH.Routes)
```
| Method | Path | Capability | Notes |
|---|---|---|---|
| GET | `/api/recruitmatch/dashboard` | `CapCandidateRead` | optional `as_of` in `2006-01-02` |
| GET | `/api/recruitmatch/candidates` | `CapCandidateRead` | shared `matching.ReadRoutes` |
| GET | `/api/recruitmatch/requirements` | `CapCandidateRead` | shared `matching.ReadRoutes` |
`CapCandidateRead` includes every role down to `viewer`, so the dashboard is visible to all members
of an org.
Alerts and recommendations stay at their existing flat paths. They are genuinely cross tool and
aliasing them a fourth time would add registrations without adding meaning.
### Error mapping
Follow the Command Center handler: `tenant.ErrNoTenant` maps to 403, `ErrNoDatabase` maps to 503,
anything else maps to 500 with a fixed body that never leaks a wrapped error.
## 7. Frontend design
New directory `frontend/src/tools/recruitmatch/` containing `brand.ts`, `api.ts`, `queries.ts` and
`RecruiterDashboard.tsx`.
* Accent: one new token in the `@theme` block of `src/index.css`, joining screenMATCH teal `#14b8a6`
and slateMATCH violet `#8b5cf6`.
* `TOOLS` gains a fourth entry with `basePath: '/recruit'` and a `match` covering that prefix.
* Query keys follow `['recruitmatch', resource, ...params]`.
* Every decimal arrives as a string and is converted with `num()` from `lib/format.ts`.
* Reuse `Panel`, `Stat`, `Bar`, `Badge`, `Gauge` and `ToolHeader`. Do not invent card markup.
* Dynamic colors are `var(--color-*)` inline. Static colors are Tailwind classes.
* `tabular-nums` on every number.
* The screen owns no outer padding. `AppShell` main already applies it.
**Handle the error state.** `CommandCenter.tsx` destructures without `isError`, so a failed fetch
leaves its loading panel on screen forever. Copy the pattern in `screens/Requirements.tsx` instead.
Charts: recharts is already a dependency but is used in exactly one file. The dominant idiom is hand
built SVG. Use `Bar` and `Gauge` for meters and reach for recharts only for the multi series activity
trend, matching the styling in `components/WorkforceDNA.tsx`.
## 8. Org branding, platform scope
This is platform work, not recruitMATCH work. All four tools inherit it.
**Migration 0046** adds to `organizations`: `display_name text`, `logo bytea`, `logo_mime text`,
`accent_color text`. RLS policy `org_self_select` already scopes reads to the GUC. The existing
column grant is deliberately narrow at `UPDATE (name)`, so widen it to exactly the new columns and
leave `status` locked so a tenant cannot un suspend itself.
**Extend `SessionInfo`** with the org display name and a flag indicating a logo exists. `AuthProvider`
already fetches `/api/auth/session` once on mount and `AppShell` already consumes the principal, so
branding arrives with no extra request and no new fetch plumbing.
**Serve the logo** from `GET /api/org/logo`, session gated, returning the current org bytes with the
stored content type. Writes are gated by `CapOrgSettings`, which is owner only. Upload UI goes in
`screens/Settings.tsx` beside the existing team panel.
**`AppShell` renders** the org mark in the sidebar header, falling back to the missionMATCH mark when
an org has not uploaded one, which is every org on its first day.
Storage decision: keep logo bytes in Postgres for v1. Logos are tens of kilobytes, one row per
tenant, and the bytes inherit RLS isolation automatically. Wiring object storage into the API would
mean building the AWS adapter that does not exist yet, for a few kilobytes per client. The `Storer`
interface in `internal/platform/storage` remains available if this ever outgrows a column.
## 9. Multi tenancy requirements
Non negotiable, and the reason for the GUC assertion above.
1. No org identifier ever appears in a handler signature. Org is ambient in context and reaches
Postgres only through `RunInTenantTx`.
2. No FG string, FG asset, or FG conditional anywhere in the codebase.
3. Every panel must render correctly for an org with zero rows. A new client sees zeros and named
empty states, never a crash, a NaN, or a fabricated figure.
4. Recruiter attribution derives from `users` in the tenant, so it works for a one recruiter shop and
a twenty recruiter shop without configuration.
## 10. Testing
* Pure unit tests for `derive.go` against hand built `View` structs, no database, mirroring
`commandcenter/executive_test.go`.
* A test asserting a zero data org produces a complete `View` with zeros and no panic. This is the
new client case and it is the one most likely to regress.
* An integration test under the `integration` build tag proving two orgs see disjoint dashboards.
Note that the integration suite exits 0 silently when `MM_APP_DSN` and `MM_MIGRATOR_DSN` are unset,
so a green run does not prove it executed. Confirm the DSNs are exported.
* `make build-test-gates` must pass, including gofmt, vet, the backdoor check, the FIPS check and the
frontend lint and build.
## 11. Deployment notes
Production is behind this branch and that must be reconciled before shipping.
* Live goose version is 42. This branch is at 45. Migrations 0043, 0044 and 0045 are not applied, so
`outcome_events`, `aioe_weight_profiles` and `term_expansions` do not exist in production.
* The `mm-api` binary and the frontend bundle in production were both built on July 1.
* Deployment is manual. CI runs gates and never deploys.
Sequence: apply 0043 through 0046, rebuild and deploy the binary, rebuild and deploy the frontend.
## 12. Open questions
1. The tool name is recruitMATCH and the base path is `/recruit`. Confirm both.
2. Accent color for the fourth tool.
3. Whether `accent_color` in org branding should actually theme the shell, or only be stored for
later. Storing it is cheap. Applying it interacts with the fixed tool accents and needs a rule.
4. The activity trend window. The mockup shows seven days.
## 13. Related findings, out of scope but recorded
Surfaced during the scan. Each deserves its own ticket.
1. `docs/MODEL_CARD.md` documents `v1.1.0` while the engine is `v1.4.0`, missing both band caps and
the compartment check. For a product whose central claim is auditability this is the highest
severity item found.
2. `ontology_crosswalk` is queried by a live code path but has no writer anywhere, so `SkillGapBridge`
silently under returns.
3. `internal/platform/federated` and `dp_budget_ledger` have zero importers. Dead code.
4. `/api/opportunities`, `/api/awards` and `/api/wages` have no capability gate at all.
5. `/metrics` is unauthenticated and outside the rate limiter.
6. Several buttons render as working features with no handler attached, in `CommandCenter.tsx` and
`CandidateEvaluation.tsx`.
7. `users.last_login_at` is never written, though `login_attempts` records the successes.