skills/atemndobs/nebula-rfp/compliance-matrix

compliance-matrix

SKILL.md

Compliance Matrix Skill

Overview

This skill generates compliance matrices to track RFP requirements against proposal responses, ensuring complete coverage of all mandatory elements.

Data Structures

interface ComplianceMatrix {
  rfpId: string;
  rfpTitle: string;
  createdAt: Date;
  updatedAt: Date;
  status: "draft" | "in_review" | "complete";
  sections: ComplianceSection[];
  summary: MatrixSummary;
}

interface ComplianceSection {
  id: string;
  name: string;
  requirements: ComplianceRequirement[];
}

interface ComplianceRequirement {
  id: string;
  reference: string;           // RFP section reference
  requirement: string;          // Original requirement text
  type: "mandatory" | "desirable" | "informational";
  category: RequirementCategory;
  responseSection: string;      // Proposal section
  responseText: string;         // Draft response
  evidence: string;             // Supporting evidence
  owner: string;                // Team member responsible
  status: "pending" | "draft" | "review" | "complete" | "n/a";
  notes: string;
}

type RequirementCategory =
  | "technical"
  | "management"
  | "past_performance"
  | "pricing"
  | "certifications"
  | "staffing"
  | "security"
  | "compliance"
  | "other";

interface MatrixSummary {
  totalRequirements: number;
  mandatory: number;
  desirable: number;
  addressed: number;
  pending: number;
  notApplicable: number;
}

Template Output

# Compliance Matrix: {{RFP_TITLE}}

**RFP ID:** {{EXTERNAL_ID}}
**Agency:** {{AGENCY}}
**Deadline:** {{DEADLINE}}
**Last Updated:** {{UPDATED_AT}}

## Summary
| Status | Mandatory | Desirable | Total |
|--------|-----------|-----------|-------|
| Complete | {{M_COMPLETE}} | {{D_COMPLETE}} | {{TOTAL_COMPLETE}} |
| Draft | {{M_DRAFT}} | {{D_DRAFT}} | {{TOTAL_DRAFT}} |
| Pending | {{M_PENDING}} | {{D_PENDING}} | {{TOTAL_PENDING}} |
| N/A | {{M_NA}} | {{D_NA}} | {{TOTAL_NA}} |

---

## Section 1: Technical Requirements

| Ref | Requirement | Type | Response | Status | Owner |
|-----|-------------|------|----------|--------|-------|
| 3.1.1 | {{REQ}} | M | {{RESPONSE_SECTION}} || {{OWNER}} |

---

## Submission Checklist
- [ ] All mandatory requirements addressed
- [ ] Technical volume complete
- [ ] Past performance complete
- [ ] Pricing complete
- [ ] All forms signed
- [ ] Format requirements met
- [ ] Red team review complete

Requirement Extraction

Convex Action

// convex/compliance.ts
import { action, mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const generateMatrix = action({
  args: { rfpId: v.id("rfps") },
  handler: async (ctx, args) => {
    const rfp = await ctx.runQuery(internal.rfps.get, { id: args.rfpId });
    if (!rfp) throw new Error("RFP not found");

    // Extract requirements using AI
    const requirements = await extractRequirements(rfp);

    // Group into sections
    const sections = groupRequirements(requirements);

    // Create matrix
    const matrix: ComplianceMatrix = {
      rfpId: args.rfpId,
      rfpTitle: rfp.title,
      createdAt: new Date(),
      updatedAt: new Date(),
      status: "draft",
      sections,
      summary: calculateSummary(sections),
    };

    // Save to pursuit
    await ctx.runMutation(internal.pursuits.saveComplianceMatrix, {
      rfpId: args.rfpId,
      matrix: JSON.stringify(matrix),
    });

    return matrix;
  },
});

AI Extraction

async function extractRequirements(rfp: RFP): Promise<ComplianceRequirement[]> {
  const prompt = `
Analyze this RFP and extract ALL requirements.

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

For each requirement, identify:
1. Section reference (use sequential numbering if not available)
2. Exact requirement text
3. Type: mandatory (must/shall/required), desirable (should/may), or informational
4. Category: technical, management, past_performance, pricing, certifications, staffing, security, compliance, other

Respond with JSON array:
[
  {
    "reference": "3.1.1",
    "requirement": "System must support 1000 concurrent users",
    "type": "mandatory",
    "category": "technical"
  }
]`;

  const response = await callAIProvider(prompt);
  const parsed = JSON.parse(response);

  return parsed.map((req: any, index: number) => ({
    id: `req-${index + 1}`,
    reference: req.reference || `${index + 1}`,
    requirement: req.requirement,
    type: req.type,
    category: req.category,
    responseSection: "",
    responseText: "",
    evidence: "",
    owner: "",
    status: "pending" as const,
    notes: "",
  }));
}

Grouping Logic

function groupRequirements(
  requirements: ComplianceRequirement[]
): ComplianceSection[] {
  const groups = new Map<string, ComplianceRequirement[]>();

  for (const req of requirements) {
    const existing = groups.get(req.category) || [];
    existing.push(req);
    groups.set(req.category, existing);
  }

  const categoryNames: Record<string, string> = {
    technical: "Technical Requirements",
    management: "Management Requirements",
    past_performance: "Past Performance",
    pricing: "Pricing Requirements",
    certifications: "Certifications & Compliance",
    staffing: "Staffing Requirements",
    security: "Security Requirements",
    compliance: "Regulatory Compliance",
    other: "Other Requirements",
  };

  const order = [
    "technical",
    "management",
    "staffing",
    "past_performance",
    "security",
    "compliance",
    "pricing",
    "certifications",
    "other",
  ];

  return order
    .filter((cat) => groups.has(cat))
    .map((category) => ({
      id: category,
      name: categoryNames[category] || category,
      requirements: groups.get(category)!,
    }));
}

Update Functions

// convex/compliance.ts
export const updateRequirement = mutation({
  args: {
    rfpId: v.id("rfps"),
    requirementId: v.string(),
    updates: v.object({
      responseSection: v.optional(v.string()),
      responseText: v.optional(v.string()),
      evidence: v.optional(v.string()),
      owner: v.optional(v.string()),
      status: v.optional(v.string()),
      notes: v.optional(v.string()),
    }),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");

    const pursuit = await ctx.db
      .query("pursuits")
      .withIndex("by_rfp", (q) => q.eq("rfpId", args.rfpId))
      .first();

    if (!pursuit?.complianceMatrix) {
      throw new Error("Compliance matrix not found");
    }

    const matrix: ComplianceMatrix = JSON.parse(pursuit.complianceMatrix);

    // Find and update requirement
    for (const section of matrix.sections) {
      const req = section.requirements.find((r) => r.id === args.requirementId);
      if (req) {
        Object.assign(req, args.updates);
        break;
      }
    }

    // Recalculate summary
    matrix.summary = calculateSummary(matrix.sections);
    matrix.updatedAt = new Date();

    await ctx.db.patch(pursuit._id, {
      complianceMatrix: JSON.stringify(matrix),
      updatedAt: Date.now(),
    });

    return matrix;
  },
});

export const getProgress = query({
  args: { rfpId: v.id("rfps") },
  handler: async (ctx, args) => {
    const pursuit = await ctx.db
      .query("pursuits")
      .withIndex("by_rfp", (q) => q.eq("rfpId", args.rfpId))
      .first();

    if (!pursuit?.complianceMatrix) return null;

    const matrix: ComplianceMatrix = JSON.parse(pursuit.complianceMatrix);

    return {
      summary: matrix.summary,
      byCategory: matrix.sections.map((s) => ({
        category: s.name,
        total: s.requirements.length,
        complete: s.requirements.filter((r) => r.status === "complete").length,
        mandatory: s.requirements.filter((r) => r.type === "mandatory").length,
      })),
      lastUpdated: matrix.updatedAt,
    };
  },
});

UI Components

// components/ComplianceMatrixView.tsx
export function ComplianceMatrixView({ rfpId }: { rfpId: Id<"rfps"> }) {
  const progress = useQuery(api.compliance.getProgress, { rfpId });
  const generateMatrix = useMutation(api.compliance.generateMatrix);

  if (progress === undefined) return <LoadingSpinner />;

  if (!progress) {
    return (
      <div className="p-6 text-center">
        <p className="text-muted-foreground mb-4">
          No compliance matrix generated yet.
        </p>
        <button
          onClick={() => generateMatrix({ rfpId })}
          className="px-4 py-2 bg-primary text-primary-foreground rounded"
        >
          Generate Compliance Matrix
        </button>
      </div>
    );
  }

  return (
    <div className="p-6 space-y-6">
      {/* Summary Cards */}
      <div className="grid grid-cols-4 gap-4">
        <SummaryCard
          label="Total"
          value={progress.summary.totalRequirements}
          color="blue"
        />
        <SummaryCard
          label="Complete"
          value={progress.summary.addressed}
          color="green"
        />
        <SummaryCard
          label="Pending"
          value={progress.summary.pending}
          color="yellow"
        />
        <SummaryCard
          label="Mandatory"
          value={progress.summary.mandatory}
          color="red"
        />
      </div>

      {/* Progress by Category */}
      <div className="space-y-2">
        {progress.byCategory.map((cat) => (
          <ProgressBar
            key={cat.category}
            label={cat.category}
            current={cat.complete}
            total={cat.total}
          />
        ))}
      </div>
    </div>
  );
}

Export Formats

export function exportToMarkdown(matrix: ComplianceMatrix): string {
  // See template above
}

export function exportToCsv(matrix: ComplianceMatrix): string {
  const headers = [
    "Section",
    "Reference",
    "Requirement",
    "Type",
    "Response Section",
    "Status",
    "Owner",
    "Notes",
  ];

  const rows = matrix.sections.flatMap((section) =>
    section.requirements.map((req) => [
      section.name,
      req.reference,
      `"${req.requirement.replace(/"/g, '""')}"`,
      req.type,
      req.responseSection,
      req.status,
      req.owner,
      `"${req.notes.replace(/"/g, '""')}"`,
    ])
  );

  return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
}

Status Legend

Icon Status Meaning
Complete Fully addressed with evidence
🔶 Draft Response written, needs review
Pending Not yet started
N/A Not applicable to this proposal
Weekly Installs
3
First Seen
Feb 26, 2026
Installed on
gemini-cli3
github-copilot3
codex3
amp3
kimi-cli3
openclaw3