Docs
Vendor Grading Spec
Vendor grading and contract renewal intelligence build spec.
Vendor Grading & Contract Renewal Intelligence — Build Spec (2026-07-08)
Merged verdict from two DRs (ChatGPT: "Vendor Grading and Contract Renewal Intelligence System"; Gemini: "Vendor Grading Intelligence System" — extracts in session scratchpad) + governance repo map. This module ships INSIDE nomoi-governance: same org/RLS/auth/Stripe/import rails.
Verdict on the DRs (what we keep, what we kill for v1)
Both DRs converge on the same two-layer split, and that split IS the product: LLM extracts and cites (behind a human gate); a deterministic engine decides. Nothing scored is free text. Every score component traces to typed events and evidence quotes. This is exactly the governance house philosophy (import-sync engine, date-lock, audit spine) — which is why this is a module, not a new app.
KEEP (v1):
- Canonical model: Vendor, Contract, Obligation, Event, Score, RenewalRecommendation, Document — with evidence spans on every extracted fact.
- Extraction contract: every populated field carries a verbatim quote + location; quote is re-located in source text (exact
indexOffirst, sliding-window fuzzy ≥0.85 fallback; below threshold → verification failure, field goes to review as low-confidence). - Deterministic scoring, fixed default weights (org-editable, must sum to 1): Quality .30, Timeliness .25, Cost .15, Responsiveness .15, Compliance/Risk .15.
- BenefitMetric = 100·clamp((x−L)/(U−L),0,1); CostMetric = 100·clamp((U−x)/(U−L),0,1)
- Grade = Σ w_d · dim_d
- Trend: EWMA over period grades, λ=0.3, stored separately from raw grade. DEGRADING = negative 2–3 period slope.
- Decision engine emits exactly three actions with typed critical flags (never narrative):
- RENEW: grade ≥ 85, no critical flag, trend not materially negative
- RENEGOTIATE: 70 ≤ grade < 85; or ≥85 with one material risk flag; or sustained negative trend; or low grade but IMPROVING
- TERMINATE: grade < 70 with STABLE/DEGRADING trend; or repeated uncured material breach; or any critical flag (security default, insolvency)
- Flags enum: material_sla_breach, uncured_default_notice, security_nonconformance, financial_distress_signal, chronic_delivery_delay, audit_failure
- Audit packet: recommendation → score lineage → event rows → evidence quotes. Test fixture: ChatGPT DR worked example (Q92/T68/C85/R74/K60 → 77.45 → RENEGOTIATE).
KILL for v1 (enterprise fantasy vs a Next.js module with 0 customers; revisit on demand):
- Docling/GPU parse nodes → repo already has pdf-parse/mammoth/xlsx; scanned PDFs are flagged "needs OCR", out of scope.
- Splink/Dedupe ER → org-scoped vendor lists are small;
trade_names text[]+ exact/alias match + manual merge action suffices. - AHP pairwise elicitation & TOPSIS → fixed editable weights + 3-action matrix cover it; consistency by construction.
- pgvector/embeddings, BAML, Python microservices → all TS in-app, LLM via existing import-plan LiteLLM path.
- SMTP ingress → Wave 6 (repo already has /api/email/inbound to extend); v1 events are manually logged or extracted from uploaded docs/pasted email text, always behind the review gate.
Schema (migration 0019, schema governance, all org-scoped, RLS per 0002 pattern)
vendors: id, org_id, canonical_name, trade_names text[], registration_id, category, contact_email, status(active|inactive), notes, created_at, updated_atvendor_documents: id, org_id, vendor_id?, filename, mime, text_content, sha256, uploaded_by, created_at (immutable source-of-truth text; contracts/obligations point here)vendor_contracts: id, org_id, vendor_id, title, contract_type(MSA|SOW|NDA|RFP|AMENDMENT|OTHER), effective_date, expiration_date, is_auto_renew, notice_period_days, renewal_term, value_amount, currency, status(active|expired|pending|terminated), document_id?, metadata jsonbvendor_obligations: id, org_id, contract_id, clause_type(deliverable|milestone|sla|acceptance_criteria|reporting|penalty|termination|renewal|payment), description, due_rule, target_metric, penalty_rule, survives_termination, evidence jsonb {quote,page,char_start,char_end,confidence}, statusvendor_events(append-only; the performance log): id, org_id, vendor_id, contract_id?, obligation_id?, event_type(delivered_on_time|delivered_late|missed|sla_breach|dispute|escalation|resolved|quality_check|response_latency|audit_finding|default_notice|penalty_applied|note), event_date, quant jsonb {days_late,score,latency_hours,amount}, narrative, evidence jsonb, source(manual|document|email), logged_by, created_atvendor_scores: id, org_id, vendor_id, contract_id?, period_start, period_end, quality, timeliness, cost, responsiveness, compliance (0–100 numerics), grade, ewma_grade, trend(IMPROVING|STABLE|DEGRADING), inputs jsonb (event-id lineage per dimension), computed_atvendor_renewal_recommendations: id, org_id, contract_id, score_id, action(RENEW|RENEGOTIATE|TERMINATE), critical_flags text[], rationale, evidence jsonb, status(open|accepted|dismissed), decided_by?, created_atvendor_weights: org_id pk, quality, timeliness, cost, responsiveness, compliance (defaults .30/.25/.15/.15/.15, CHECK sum ≈ 1), updated_by, updated_at
RLS: reuse is_member/role_in_org. Read: all org members. Write: pm + exec (events also writable by owner role). vendor_events: INSERT-only for non-pm (no UPDATE/DELETE policies); corrections are new events. audit_log trigger pattern as elsewhere.
Dimension inputs (deterministic, from events)
- Quality: mean quality_check score (0–100)
- Timeliness: 100·on_time/(on_time+late+missed), penalized by mean days_late (cost-metric on days_late, U=14)
- Cost: cost-metric on penalty/dispute amounts vs contract value (U=5% of value)
- Responsiveness: cost-metric on mean latency_hours (L=4, U=72); manual rating events can override
- Compliance/Risk: 100 − deductions per sla_breach/audit_finding/default_notice event (typed deduction table in engine)
Dimensions with zero events return null and re-normalize remaining weights (never fabricate a score).
Waves
- Migration 0019 (Codex) → dogfood Sonnet review (DDL = high-risk) → apply via Supabase MCP → verify information_schema.
src/lib/vendor-grading.tspure engine +vendor-grading.test.ts(Codex). Fixtures: both DR worked examples.- Contract ingestion: extend import-plan pattern → extraction schema above → tri-state review (reuse sync review UI pattern) → apply. Evidence relocation util
src/lib/evidence-locate.ts. - UI: /vendors (list w/ grade chips), /vendors/[id] (scorecard, trend, contracts, obligations, event timeline, recommendation card w/ evidence), event logging form, /vendors/renewals queue (expiring ≤90d + notice deadlines). nomoi-taste before building.
- Full treatment: Stripe (decision pending steal-scan pricing signal — default: vendor module included in Growth $149 + new standalone "Vendor Intelligence" price), landing section on governance.nomoi.ai, nomoi.ai catalog, Bug Desk registration, audit-packet export (print view v1).
- (Later) email-in events via /api/email/inbound; OCR lane; cron: recompute scores + renewal-window alerts into notifications table (dedupe_key exists).
Renewal queue rule: surface contracts where expiration_date − notice_period_days ≤ today+90d; auto_renew contracts show "silent renewal" warning before notice deadline.
Steal-scan addendum (2026-07-08, see VENDOR_GRADING_STEALSCAN.md)
- Standings gate (ERPNext pattern, adopt in Wave 4): map grade bands to named standings (e.g. Preferred ≥85 / Acceptable 70–85 / Probation 55–70 / Blocked <55) shown as the vendor's primary chip; standings are the human-legible layer over the numeric grade and later gate actions (v2: block new contracts for Blocked vendors). Store band → label mapping alongside vendor_weights.
- Positioning (Wave 5): GCC compliance wedge — "ADHICS Domain 10 vendor oversight evidence pack" export (Abu Dhabi healthcare) and Abu Dhabi Procurement Standards supplier-performance-score output (AD government). The audit packet IS the evidence pack; brand it as such.
- Pricing (Wave 5, confirmed by scan): market floor is Gatekeeper ~$15k/yr; OSS field is empty. Anchor $99–299/mo, priced per vendor count (Gatekeeper's axis), unlimited users. Sirion's "9% revenue drain from missed renewals" stat is usable in copy.
- v2 extraction quorum: CUAD-fine-tuned RoBERTa checkpoints (renewal/expiration/notice-period labels) as a deterministic second engine; disagreement with LLM extraction → review flag.