///
import { type Cell, cell, Default, handler, lift, recipe } from "commontools";
type RiskTier = "low" | "medium" | "high";
interface VendorResponseInput {
topic?: string;
rating?: number;
weight?: number;
}
interface VendorInput {
id?: string;
name?: string;
category?: string;
responses?: VendorResponseInput[];
}
interface ResponseRecord {
topic: string;
rating: number;
weight: number;
}
interface VendorRecord {
id: string;
name: string;
category: string;
responses: ResponseRecord[];
}
interface ThresholdState {
medium: number;
high: number;
}
interface ResponseBreakdown extends ResponseRecord {
contribution: number;
}
interface VendorRiskSummary {
id: string;
name: string;
category: string;
total: number;
tier: RiskTier;
breakdown: ResponseBreakdown[];
}
interface TierCounts {
high: number;
medium: number;
low: number;
}
interface TierEntry {
tier: RiskTier;
vendors: {
id: string;
name: string;
score: number;
}[];
}
interface ResponseAdjustmentEvent {
vendorId?: string;
topic?: string;
rating?: number;
weight?: number;
category?: string;
name?: string;
}
interface VendorRiskAssessmentArgs {
vendors: Default;
}
const baselineThresholds: ThresholdState = {
medium: 50,
high: 80,
};
const defaultVendors: VendorRecord[] = [
{
id: "vendor-apex-cloud",
name: "Apex Cloud",
category: "Infrastructure",
responses: [
{ topic: "compliance", rating: 15, weight: 1 },
{ topic: "financial", rating: 12, weight: 1 },
{ topic: "security", rating: 30, weight: 2 },
],
},
{
id: "vendor-data-harbor",
name: "Data Harbor",
category: "Analytics",
responses: [
{ topic: "compliance", rating: 22, weight: 1 },
{ topic: "financial", rating: 10, weight: 1 },
{ topic: "security", rating: 18, weight: 1 },
],
},
{
id: "vendor-orbita-supplies",
name: "Orbita Supplies",
category: "Hardware",
responses: [
{ topic: "compliance", rating: 8, weight: 1 },
{ topic: "financial", rating: 6, weight: 1 },
{ topic: "security", rating: 10, weight: 1 },
],
},
];
const tierOrder: readonly RiskTier[] = ["high", "medium", "low"];
const tierWeight: Record = {
high: 0,
medium: 1,
low: 2,
};
const roundOne = (value: number): number => Math.round(value * 10) / 10;
const slugify = (value: string): string =>
value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
const formatNumber = (value: number): string => {
const rounded = roundOne(value);
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1);
};
const sanitizeVendorId = (value: unknown, fallback: string): string => {
if (typeof value !== "string") {
return fallback;
}
const trimmed = value.trim();
if (trimmed.length === 0) {
return fallback;
}
const slug = slugify(trimmed);
return slug.length > 0 ? slug : fallback;
};
const sanitizeVendorName = (value: unknown, fallbackId: string): string => {
if (typeof value !== "string") {
return `Vendor ${fallbackId}`;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : `Vendor ${fallbackId}`;
};
const sanitizeCategory = (value: unknown): string => {
if (typeof value !== "string") {
return "General";
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : "General";
};
const sanitizeTopic = (value: unknown, fallback: string): string => {
if (typeof value !== "string") {
return fallback;
}
const trimmed = value.trim();
if (trimmed.length === 0) {
return fallback;
}
const slug = slugify(trimmed);
return slug.length > 0 ? slug : fallback;
};
const sanitizeRating = (value: unknown): number => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return 0;
}
const clamped = Math.min(Math.max(value, 0), 100);
return roundOne(clamped);
};
const sanitizeWeight = (value: unknown): number => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return 1;
}
const clamped = Math.min(Math.max(value, 0.1), 5);
return roundOne(clamped);
};
const cloneResponses = (
responses: readonly ResponseRecord[],
): ResponseRecord[] => responses.map((response) => ({ ...response }));
const cloneDefaultVendors = (): VendorRecord[] =>
defaultVendors.map((vendor) => ({
id: vendor.id,
name: vendor.name,
category: vendor.category,
responses: cloneResponses(vendor.responses),
}));
const ensureUniqueId = (id: string, used: Set): string => {
let candidate = id;
let suffix = 2;
while (used.has(candidate)) {
candidate = `${id}-${suffix}`;
suffix += 1;
}
used.add(candidate);
return candidate;
};
const ensureUniqueTopic = (
topic: string,
used: Set,
): string => {
let candidate = topic;
let suffix = 2;
while (used.has(candidate)) {
candidate = `${topic}-${suffix}`;
suffix += 1;
}
used.add(candidate);
return candidate;
};
const sanitizeResponseList = (
value: unknown,
vendorId: string,
): ResponseRecord[] => {
if (!Array.isArray(value)) {
return [];
}
const used = new Set();
const sanitized: ResponseRecord[] = [];
let fallbackIndex = 1;
for (const entry of value) {
const raw = typeof entry === "object" && entry !== null
? entry as Record
: {};
const fallbackTopic = `${vendorId}-topic-${fallbackIndex}`;
fallbackIndex += 1;
const topic = ensureUniqueTopic(
sanitizeTopic(raw["topic"], fallbackTopic),
used,
);
sanitized.push({
topic,
rating: sanitizeRating(raw["rating"]),
weight: sanitizeWeight(raw["weight"]),
});
}
sanitized.sort((left, right) => left.topic.localeCompare(right.topic));
return sanitized;
};
const sanitizeVendorList = (value: unknown): VendorRecord[] => {
const list = Array.isArray(value) ? value : cloneDefaultVendors();
const sanitized: VendorRecord[] = [];
const used = new Set();
let fallbackIndex = 1;
for (const entry of list) {
const raw = typeof entry === "object" && entry !== null
? entry as Record
: {};
const fallbackId = `vendor-${fallbackIndex}`;
fallbackIndex += 1;
const id = ensureUniqueId(
sanitizeVendorId(raw["id"], fallbackId),
used,
);
const name = sanitizeVendorName(raw["name"], id);
const category = sanitizeCategory(raw["category"]);
const responses = sanitizeResponseList(raw["responses"], id);
sanitized.push({ id, name, category, responses });
}
sanitized.sort((left, right) => {
const nameCompare = left.name.localeCompare(right.name);
if (nameCompare !== 0) {
return nameCompare;
}
return left.id.localeCompare(right.id);
});
return sanitized;
};
const calculateRiskScore = (
responses: readonly ResponseRecord[],
): number => {
let total = 0;
for (const response of responses) {
total += response.rating * response.weight;
}
return roundOne(total);
};
const assignTier = (
score: number,
thresholds: ThresholdState,
): RiskTier => {
if (score >= thresholds.high) {
return "high";
}
if (score >= thresholds.medium) {
return "medium";
}
return "low";
};
const buildBreakdown = (
responses: readonly ResponseRecord[],
): ResponseBreakdown[] =>
responses.map((response) => ({
topic: response.topic,
rating: response.rating,
weight: response.weight,
contribution: roundOne(response.rating * response.weight),
}));
const buildSummaries = (
vendors: readonly VendorRecord[],
thresholds: ThresholdState,
): VendorRiskSummary[] => {
const summaries: VendorRiskSummary[] = [];
for (const vendor of vendors) {
const breakdown = buildBreakdown(vendor.responses);
const total = roundOne(
breakdown.reduce((sum, item) => sum + item.contribution, 0),
);
const tier = assignTier(total, thresholds);
summaries.push({
id: vendor.id,
name: vendor.name,
category: vendor.category,
total,
tier,
breakdown,
});
}
summaries.sort((left, right) => {
const tierDiff = tierWeight[left.tier] - tierWeight[right.tier];
if (tierDiff !== 0) {
return tierDiff;
}
if (right.total !== left.total) {
return right.total - left.total;
}
return left.name.localeCompare(right.name);
});
return summaries;
};
const buildTierCounts = (
summaries: readonly VendorRiskSummary[],
): TierCounts => {
const counts: TierCounts = { high: 0, medium: 0, low: 0 };
for (const summary of summaries) {
counts[summary.tier] += 1;
}
return counts;
};
const buildTierBreakdown = (
summaries: readonly VendorRiskSummary[],
): TierEntry[] =>
tierOrder.map((tier) => ({
tier,
vendors: summaries
.filter((summary) => summary.tier === tier)
.map((summary) => ({
id: summary.id,
name: summary.name,
score: summary.total,
})),
}));
const formatTopVendor = (
summary: VendorRiskSummary | null,
): string => {
if (!summary) {
return "No vendors";
}
return `${summary.name} (${formatNumber(summary.total)})`;
};
const formatAuditMessage = (
vendor: VendorRecord,
topic: string,
response: ResponseRecord,
): string => {
const total = calculateRiskScore(vendor.responses);
return `Adjusted ${topic} for ${vendor.id} to ${
formatNumber(response.rating)
} ` +
`@ ${formatNumber(response.weight)} (total ${formatNumber(total)})`;
};
const readAuditLog = (value: unknown): string[] => {
if (!Array.isArray(value)) {
return [];
}
const entries: string[] = [];
for (const item of value) {
if (typeof item === "string") {
entries.push(item);
}
}
return entries;
};
const adjustVendorResponse = handler(
(
event: ResponseAdjustmentEvent | undefined,
context: {
vendors: Cell;
audit: Cell;
},
) => {
const vendorId = sanitizeVendorId(event?.vendorId, "");
if (vendorId.length === 0) {
return;
}
const topic = sanitizeTopic(event?.topic, "");
if (topic.length === 0) {
return;
}
const rawSource = context.vendors as unknown as {
getRaw?: () => unknown;
};
const currentValue = typeof rawSource.getRaw === "function"
? rawSource.getRaw()
: context.vendors.get();
const current = sanitizeVendorList(currentValue);
let mutated = false;
let trackedTopic: string | null = null;
const updated = current.map((vendor) => {
if (vendor.id !== vendorId) {
return vendor;
}
const existingResponses = cloneResponses(vendor.responses);
const index = existingResponses.findIndex((entry) =>
entry.topic === topic
);
let responseChanged = false;
if (index >= 0) {
const currentResponse = existingResponses[index];
const nextRating = event?.rating === undefined
? currentResponse.rating
: sanitizeRating(event.rating);
const nextWeight = event?.weight === undefined
? currentResponse.weight
: sanitizeWeight(event.weight);
if (
nextRating !== currentResponse.rating ||
nextWeight !== currentResponse.weight
) {
existingResponses[index] = {
topic,
rating: nextRating,
weight: nextWeight,
};
responseChanged = true;
}
} else {
if (event?.rating === undefined && event?.weight === undefined) {
return vendor;
}
existingResponses.push({
topic,
rating: sanitizeRating(event?.rating),
weight: sanitizeWeight(event?.weight),
});
responseChanged = true;
}
const sanitizedResponses = sanitizeResponseList(
existingResponses,
vendor.id,
);
const nextCategory = event?.category === undefined
? vendor.category
: sanitizeCategory(event.category);
const nextName = event?.name === undefined
? vendor.name
: sanitizeVendorName(event.name, vendor.id);
if (
!responseChanged &&
nextCategory === vendor.category &&
nextName === vendor.name
) {
return vendor;
}
mutated = true;
if (responseChanged) {
trackedTopic = topic;
}
return {
id: vendor.id,
name: nextName,
category: nextCategory,
responses: sanitizedResponses,
};
});
if (!mutated) {
return;
}
const normalized = sanitizeVendorList(updated);
context.vendors.set(normalized);
if (!trackedTopic) {
return;
}
const targetVendor = normalized.find((vendor) => vendor.id === vendorId);
if (!targetVendor) {
return;
}
const response = targetVendor.responses.find((entry) =>
entry.topic === trackedTopic
);
if (!response) {
return;
}
const currentLog = readAuditLog(context.audit.get());
const nextLog = [
...currentLog,
formatAuditMessage(
targetVendor,
trackedTopic,
response,
),
].slice(-10);
context.audit.set(nextLog);
},
);
export const vendorRiskAssessment = recipe(
"Vendor Risk Assessment",
({ vendors }) => {
const auditLog = cell([]);
const vendorsView = lift(sanitizeVendorList)(vendors);
const vendorRiskSummaries = lift((records: VendorRecord[]) =>
buildSummaries(records, baselineThresholds)
)(vendorsView);
const tierCounts = lift(buildTierCounts)(vendorRiskSummaries);
const tierBreakdown = lift(buildTierBreakdown)(vendorRiskSummaries);
const highRiskCount = lift((counts: TierCounts) => counts.high)(
tierCounts,
);
const mediumRiskCount = lift((counts: TierCounts) => counts.medium)(
tierCounts,
);
const lowRiskCount = lift((counts: TierCounts) => counts.low)(
tierCounts,
);
const riskOverview = lift((counts: TierCounts) =>
`High: ${counts.high}, Medium: ${counts.medium}, Low: ${counts.low}`
)(tierCounts);
const highestRiskVendor = lift((summaries: VendorRiskSummary[]) =>
summaries.length > 0 ? summaries[0] : null
)(vendorRiskSummaries);
const highestRiskLabel = lift(formatTopVendor)(highestRiskVendor);
const auditTrail = lift((entries: string[]) => [...entries])(auditLog);
return {
vendors,
vendorsView,
vendorRiskSummaries,
tierBreakdown,
tierCounts,
highRiskCount,
mediumRiskCount,
lowRiskCount,
riskOverview,
highestRiskVendor,
highestRiskLabel,
auditTrail,
adjustResponse: adjustVendorResponse({
vendors,
audit: auditLog,
}),
};
},
);
export type {
ResponseAdjustmentEvent,
TierCounts,
TierEntry,
VendorInput,
VendorRecord,
VendorResponseInput,
VendorRiskAssessmentArgs,
VendorRiskSummary,
};
export type { RiskTier };