rfp-evaluate

This skill implements the 6-dimension scoring framework for evaluating RFP opportunities, with both logic-based (keyword matching) and AI-based evaluation modes.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rfp-evaluate" with this command: npx skills add atemndobs/nebula-rfp/atemndobs-nebula-rfp-rfp-evaluate

RFP Evaluation Skill

Overview

This skill implements the 6-dimension scoring framework for evaluating RFP opportunities, with both logic-based (keyword matching) and AI-based evaluation modes.

CRITICAL: Execution Order

Eligibility Gate is P0 HIGHEST PRIORITY and runs BEFORE scoring.

┌─────────────────────────────────────────────────────────┐ │ EVALUATION PIPELINE │ ├─────────────────────────────────────────────────────────┤ │ 1. ELIGIBILITY GATE (Hard filters - Phase 2) │ │ ↓ │ │ Output: ELIGIBLE | PARTNER_REQUIRED | REJECTED │ │ ↓ │ │ 2. SCORING ENGINE (Only if eligible - Phase 3) │ │ ↓ │ │ Output: 0-6 score + Good Fit determination │ └─────────────────────────────────────────────────────────┘

Implementation Plan References:

  • Eligibility Gate: docs/implementation-plan/phase-2-eligibility/

  • Scoring Engine: docs/implementation-plan/phase-3-scoring/

6-Dimension Scoring Framework

Dimension Weight Purpose

Technical Relevance 25% Tech stack alignment

Scope Fit 20% Project type match

Category Focus 15% Industry alignment

Client Profile 15% Client type match

Logistics 15% Practical feasibility

Skill Alignment 10% Team capability match

Criterion Configuration

interface Criterion { id: string; name: string; weight: number; // 0-100, sum should = 100 enabled: boolean; keywords: Keyword[]; minMatches: number; // Minimum keywords to meet criterion systemInstruction?: string; // For AI evaluation }

interface Keyword { value: string; enabled: boolean; weight?: number; // Optional keyword importance }

Default Keywords

Technical Relevance (25%)

const TECHNICAL_KEYWORDS = [ "aws", "azure", "gcp", "cloud", "serverless", "lambda", "kubernetes", "docker", "react", "nextjs", "typescript", "node", "api", "rest", "graphql", "microservices", "data platform", "analytics", "etl", "ci/cd", "devsecops" ];

Scope Fit (20%)

const SCOPE_KEYWORDS = [ "website redesign", "web application", "portal development", "cms implementation", "platform modernization", "digital transformation", "cloud migration", "api development", "system integration", "data migration", "taxonomy", "information architecture" ];

Category Focus (15%)

const CATEGORY_KEYWORDS = [ "public sector", "federal", "state", "local government", "it services", "software development", "digital services", "technology", "information technology" ];

Client Profile (15%)

const CLIENT_KEYWORDS = [ "federal agency", "state agency", "municipality", "department of", "office of", "bureau of", "technology-forward", "agile", "modern" ];

Skill Alignment (10%)

const SKILL_KEYWORDS = [ "frontend developer", "backend developer", "full-stack", "cloud architect", "devops engineer", "ux designer", "technical lead", "project manager", "qa engineer" ];

Eligibility Gate

Run BEFORE scoring to reject ineligible opportunities:

interface EligibilityResult { eligible: boolean; status: "ok" | "needs_partner" | "reject"; disqualifiers: string[]; }

const HARD_DISQUALIFIERS = [ { pattern: /security\sclearance\s(required|mandatory)/i, fatal: true }, { pattern: /on-?site\s*(presence\s*)?(required|mandatory)/i, fatal: true }, { pattern: /u.?s.?\s*(citizen|company|organization)\s*only/i, fatal: false }, // Can partner ];

function checkEligibility(text: string): EligibilityResult { const disqualifiers: string[] = []; let canPartner = true;

for (const { pattern, fatal } of HARD_DISQUALIFIERS) { if (pattern.test(text)) { disqualifiers.push(pattern.source); if (fatal) canPartner = false; } }

if (disqualifiers.length === 0) { return { eligible: true, status: "ok", disqualifiers: [] }; }

return { eligible: canPartner, status: canPartner ? "needs_partner" : "reject", disqualifiers, }; }

Logic-Based Evaluation

interface EvaluationResult { score: number; // 0-100 isFit: boolean; // score >= 60 criteriaResults: CriterionResult[]; eligibility: EligibilityResult; reasoning?: string; }

interface CriterionResult { criterionId: string; criterionName: string; weight: number; met: boolean; score: number; matchedKeywords: string[]; details: string; }

function evaluateLogically( rfp: RFP, criteria: Criterion[] ): EvaluationResult { const text = ${rfp.title} ${rfp.description}.toLowerCase(); const results: CriterionResult[] = []; let totalScore = 0; let totalWeight = 0;

for (const criterion of criteria) { if (!criterion.enabled) continue;

const enabledKeywords = criterion.keywords
  .filter(kw => kw.enabled)
  .map(kw => kw.value.toLowerCase());

const matches = enabledKeywords.filter(kw => text.includes(kw));
const met = matches.length >= criterion.minMatches;
const score = met ? criterion.weight : 0;

results.push({
  criterionId: criterion.id,
  criterionName: criterion.name,
  weight: criterion.weight,
  met,
  score,
  matchedKeywords: matches,
  details: met
    ? `Matched ${matches.length} keywords: ${matches.join(", ")}`
    : `Only ${matches.length}/${criterion.minMatches} required matches`,
});

totalScore += score;
totalWeight += criterion.weight;

}

const normalizedScore = totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0;

return { score: Math.round(normalizedScore), isFit: normalizedScore >= 60, criteriaResults: results, eligibility: checkEligibility(text), }; }

AI-Based Evaluation

async function evaluateWithAI( rfp: RFP, criterion: Criterion, aiProvider: AIProvider ): Promise<CriterionResult> { const prompt = ` Analyze this RFP for ${criterion.name}.

RFP Title: ${rfp.title} RFP Description: ${rfp.description}

Keywords to consider: ${criterion.keywords.map(k => k.value).join(", ")}

Evaluate if this RFP aligns with these keywords. Consider both exact matches AND semantic relevance.

Respond with JSON only: { "foundKeywords": ["keyword1", "keyword2"], "isMatch": true/false, "confidence": 0.0-1.0, "reasoning": "One sentence explanation" }`;

const systemInstruction = criterion.systemInstruction ?? "You are an expert RFP analyst for a cloud-native software company.";

const response = await aiProvider.analyze(prompt, systemInstruction); const parsed = JSON.parse(response);

return { criterionId: criterion.id, criterionName: criterion.name, weight: criterion.weight, met: parsed.isMatch, score: parsed.isMatch ? criterion.weight : 0, matchedKeywords: parsed.foundKeywords, details: parsed.reasoning, }; }

Chaseability Score

Final composite score with recommendation:

interface ChaseabilityScore { overall: number; recommendation: "pursue" | "maybe" | "skip"; reasoning: string; breakdown: Record<string, number>; }

function calculateChaseability( evaluation: EvaluationResult ): ChaseabilityScore { // Apply partner penalty if needed const partnerPenalty = evaluation.eligibility.status === "needs_partner" ? 0.85 : 1.0; const adjustedScore = evaluation.score * partnerPenalty;

// Determine recommendation let recommendation: "pursue" | "maybe" | "skip"; if (!evaluation.eligibility.eligible) { recommendation = "skip"; } else if (adjustedScore >= 70) { recommendation = "pursue"; } else if (adjustedScore >= 50) { recommendation = "maybe"; } else { recommendation = "skip"; }

// Build breakdown const breakdown: Record<string, number> = {}; for (const result of evaluation.criteriaResults) { breakdown[result.criterionId] = result.score; }

return { overall: Math.round(adjustedScore), recommendation, reasoning: buildReasoning(evaluation), breakdown, }; }

function buildReasoning(evaluation: EvaluationResult): string { const met = evaluation.criteriaResults.filter(r => r.met); const notMet = evaluation.criteriaResults.filter(r => !r.met);

let reasoning = Score: ${evaluation.score}%. ; reasoning += Met ${met.length}/${evaluation.criteriaResults.length} criteria. ;

if (notMet.length > 0) { reasoning += Missing: ${notMet.map(r => r.criterionName).join(", ")}. ; }

if (evaluation.eligibility.status === "needs_partner") { reasoning += "Note: Requires US partner for eligibility."; } else if (evaluation.eligibility.status === "reject") { reasoning += Disqualified: ${evaluation.eligibility.disqualifiers.join(", ")}; }

return reasoning; }

Convex Implementation

// convex/evaluations.ts import { mutation, query } from "./_generated/server"; import { v } from "convex/values";

export const evaluate = mutation({ args: { rfpId: v.id("rfps"), evaluationType: v.optional(v.string()), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");

const rfp = await ctx.db.get(args.rfpId);
if (!rfp) throw new Error("RFP not found");

// Get criteria configuration
const criteria = await ctx.db.query("criteria").collect();

// Run evaluation
const evaluation = evaluateLogically(rfp, criteria);

// Save result
return await ctx.db.insert("evaluations", {
  rfpId: args.rfpId,
  userId: identity.subject,
  evaluationType: args.evaluationType ?? "logic",
  ...evaluation,
  evaluatedAt: Date.now(),
});

}, });

export const getByRfp = query({ args: { rfpId: v.id("rfps") }, handler: async (ctx, args) => { return await ctx.db .query("evaluations") .withIndex("by_rfp", (q) => q.eq("rfpId", args.rfpId)) .order("desc") .first(); }, });

Score Display Guidelines

Score Range Color Badge Text

≥70% text-success (green) "Strong Fit"

50-69% text-warning (yellow) "Potential Fit"

<50% text-destructive (red) "Weak Fit"

function EvaluationBadge({ score }: { score: number }) { const variant = score >= 70 ? "success" : score >= 50 ? "warning" : "destructive"; const label = score >= 70 ? "Strong Fit" : score >= 50 ? "Potential Fit" : "Weak Fit";

return ( <span className={px-2 py-1 rounded text-sm bg-${variant}/20 text-${variant}}> {score}% - {label} </span> ); }

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

proposal-builder

No summary provided by upstream source.

Repository SourceNeeds Review
General

csv-export

No summary provided by upstream source.

Repository SourceNeeds Review
General

pursuit-brief

No summary provided by upstream source.

Repository SourceNeeds Review