///
import { type Cell, cell, Default, handler, lift, recipe } from "commontools";
type ClauseStatus = "approved" | "draft" | "deprecated";
type RegionCode = "global" | "na" | "eu" | "apac";
interface ClauseInput {
id?: string;
title?: string;
topic?: string;
region?: string;
status?: string;
text?: string;
lastReviewed?: string;
}
interface ClauseRecord {
id: string;
title: string;
topicKey: string;
topicLabel: string;
region: RegionCode;
status: ClauseStatus;
text: string;
excerpt: string;
lastReviewed: string;
}
interface ClausePreview {
id: string;
title: string;
topic: string;
region: string;
status: ClauseStatus;
lastReviewed: string;
excerpt: string;
}
interface TopicBreakdown {
key: string;
label: string;
count: number;
}
interface TopicOption {
key: string;
label: string;
count: number;
active: boolean;
regions: {
region: RegionCode;
label: string;
count: number;
}[];
}
interface RegionOption {
key: RegionCode;
label: string;
count: number;
active: boolean;
topics: TopicBreakdown[];
}
interface StatusSummary {
approved: number;
draft: number;
deprecated: number;
}
interface LegalClauseLibraryArgs {
clauses: Default;
}
interface SelectTopicEvent {
topic?: string;
}
interface SelectRegionEvent {
region?: string;
}
interface UpdateStatusEvent {
id?: string;
status?: string;
reviewedOn?: string;
}
const regionLabels: Record = {
global: "Global",
na: "North America",
eu: "Europe",
apac: "Asia Pacific",
};
const regionOrder: RegionCode[] = ["global", "na", "eu", "apac"];
const statusCatalog: ClauseStatus[] = [
"approved",
"draft",
"deprecated",
];
const fallbackTopicKey = "compliance";
const defaultClauses: ClauseInput[] = [
{
id: "NDA Standard",
title: "Standard NDA",
topic: "Confidentiality",
region: "NA",
status: "approved",
text: "Outlines confidentiality duties for mutual disclosures.",
lastReviewed: "2023-09-12",
},
{
id: "GDPR Data Addendum",
title: "GDPR Data Addendum",
topic: "DATA PROTECTION",
region: "eu",
status: "draft",
text: "Captures controller-processor duties under EU GDPR.",
lastReviewed: "2023-08-01",
},
{
id: "CCPA Supplement",
title: "California Privacy Supplement",
topic: "data protection",
region: "NA",
status: "approved",
text: "Extends privacy rights alignment for California residents.",
lastReviewed: "2023-07-22",
},
{
id: "Supplier Governance",
title: "Supplier Governance Clause",
topic: "Supplier Risk",
region: "APAC",
status: "deprecated",
text: "Sets supplier reporting cadence and remediation timelines.",
lastReviewed: "2023-05-10",
},
];
const toSlug = (value: string): string =>
value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const capitalizeWord = (value: string): string => {
if (value.length === 0) return value;
return value[0].toUpperCase() + value.slice(1);
};
const topicLabelFromKey = (key: string): string => {
if (!key) return "General";
return key
.split("-")
.filter((part) => part.length > 0)
.map(capitalizeWord)
.join(" ") || "General";
};
const sanitizeClauseId = (value: unknown): string | null => {
if (typeof value !== "string") return null;
const slug = toSlug(value);
return slug.length > 0 ? slug : null;
};
const sanitizeTitle = (value: unknown, id: string): string => {
if (typeof value !== "string") {
return topicLabelFromKey(id);
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : topicLabelFromKey(id);
};
const sanitizeTopicKey = (value: unknown): string | null => {
if (typeof value !== "string") return null;
const slug = toSlug(value);
return slug.length > 0 ? slug : null;
};
const sanitizeRegionKey = (value: unknown): RegionCode | null => {
if (typeof value !== "string") return null;
const slug = toSlug(value);
if (slug.length === 0) return null;
for (const code of regionOrder) {
if (code === slug) return code;
if (toSlug(regionLabels[code]) === slug) return code;
}
return null;
};
const sanitizeRegion = (value: unknown): RegionCode => {
return sanitizeRegionKey(value) ?? "global";
};
const sanitizeStatus = (
value: unknown,
fallback: ClauseStatus,
): ClauseStatus => {
if (typeof value !== "string") return fallback;
const normalized = value.trim().toLowerCase();
for (const status of statusCatalog) {
if (status === normalized) return status;
}
return fallback;
};
const parseStatus = (value: unknown): ClauseStatus | null => {
if (typeof value !== "string") return null;
const normalized = value.trim().toLowerCase();
for (const status of statusCatalog) {
if (status === normalized) return status;
}
return null;
};
const sanitizeText = (value: unknown): string => {
if (typeof value !== "string") return "Content pending review.";
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : "Content pending review.";
};
const sanitizeReviewDate = (value: unknown, fallback: string): string => {
if (typeof value !== "string") return fallback;
const trimmed = value.trim();
return /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? trimmed : fallback;
};
const buildExcerpt = (text: string): string => {
const normalized = text.replace(/\s+/g, " ").trim();
if (normalized.length <= 72) return normalized;
return `${normalized.slice(0, 69)}...`;
};
const cloneClause = (entry: ClauseRecord): ClauseRecord => ({ ...entry });
const compareClauses = (left: ClauseRecord, right: ClauseRecord): number => {
const topicCompare = left.topicLabel.localeCompare(right.topicLabel);
if (topicCompare !== 0) return topicCompare;
return left.title.localeCompare(right.title);
};
const sanitizeClause = (value: unknown): ClauseRecord | null => {
if (typeof value !== "object" || value === null) return null;
const input = value as ClauseInput;
const id = sanitizeClauseId(input.id ?? input.title);
if (!id) return null;
const topicKey = sanitizeTopicKey(input.topic) ?? fallbackTopicKey;
const topicLabel = topicLabelFromKey(topicKey);
const region = sanitizeRegion(input.region);
const status = sanitizeStatus(input.status, "draft");
const text = sanitizeText(input.text);
const lastReviewed = sanitizeReviewDate(
input.lastReviewed,
"2023-01-01",
);
return {
id,
title: sanitizeTitle(input.title, id),
topicKey,
topicLabel,
region,
status,
text,
excerpt: buildExcerpt(text),
lastReviewed,
};
};
const defaultClauseRecords: ClauseRecord[] = (() => {
const dedup = new Map();
for (const entry of defaultClauses) {
const clause = sanitizeClause(entry);
if (!clause) continue;
if (!dedup.has(clause.id)) {
dedup.set(clause.id, clause);
}
}
const list = Array.from(dedup.values());
list.sort(compareClauses);
return list;
})();
const cloneDefaultClauses = (): ClauseRecord[] =>
defaultClauseRecords.map(cloneClause);
const sanitizeClauseList = (value: unknown): ClauseRecord[] => {
if (!Array.isArray(value)) return cloneDefaultClauses();
const dedup = new Map();
for (const raw of value) {
const clause = sanitizeClause(raw);
if (!clause) continue;
if (!dedup.has(clause.id)) {
dedup.set(clause.id, clause);
}
}
if (dedup.size === 0) return cloneDefaultClauses();
const list = Array.from(dedup.values());
list.sort(compareClauses);
return list.map(cloneClause);
};
const toClauseInputs = (entries: readonly ClauseRecord[]): ClauseInput[] =>
entries.map((entry) => ({
id: entry.id,
title: entry.title,
topic: entry.topicKey,
region: entry.region,
status: entry.status,
text: entry.text,
lastReviewed: entry.lastReviewed,
}));
const buildTopicOptions = (
clauses: readonly ClauseRecord[],
activeKey: string | null,
): TopicOption[] => {
const map = new Map;
}>();
for (const clause of clauses) {
const bucket = map.get(clause.topicKey) ?? {
label: clause.topicLabel,
count: 0,
regions: new Map(),
};
bucket.count += 1;
bucket.regions.set(
clause.region,
(bucket.regions.get(clause.region) ?? 0) + 1,
);
map.set(clause.topicKey, bucket);
}
const options: TopicOption[] = [];
for (const [key, value] of map.entries()) {
const regions = Array.from(value.regions.entries()).map((
[region, count],
) => ({
region,
label: regionLabels[region],
count,
}));
regions.sort((left, right) => left.label.localeCompare(right.label));
options.push({
key,
label: value.label,
count: value.count,
active: activeKey === key,
regions,
});
}
options.sort((left, right) => left.label.localeCompare(right.label));
return options;
};
const buildRegionOptions = (
clauses: readonly ClauseRecord[],
activeKey: RegionCode | null,
): RegionOption[] => {
const buckets = new Map;
}>();
for (const region of regionOrder) {
buckets.set(region, { count: 0, topics: new Map() });
}
for (const clause of clauses) {
const bucket = buckets.get(clause.region);
if (!bucket) continue;
bucket.count += 1;
const topic = bucket.topics.get(clause.topicKey) ?? {
key: clause.topicKey,
label: clause.topicLabel,
count: 0,
};
topic.count += 1;
bucket.topics.set(clause.topicKey, topic);
}
const options: RegionOption[] = [];
for (const region of regionOrder) {
const bucket = buckets.get(region);
if (!bucket) continue;
const topics = Array.from(bucket.topics.values());
topics.sort((left, right) => left.label.localeCompare(right.label));
options.push({
key: region,
label: regionLabels[region],
count: bucket.count,
active: activeKey === region,
topics,
});
}
return options;
};
const resolveTopicLabel = (
active: string | null,
options: readonly TopicOption[],
): string => {
if (!active) return "All Topics";
const match = options.find((option) => option.key === active);
return match ? match.label : "All Topics";
};
const resolveRegionLabel = (
active: RegionCode | null,
options: readonly RegionOption[],
): string => {
if (!active) return "All Regions";
const match = options.find((option) => option.key === active);
return match ? match.label : "All Regions";
};
const buildFilteredClauses = (
clauses: readonly ClauseRecord[],
topic: string | null,
region: RegionCode | null,
): ClausePreview[] => {
const list: ClausePreview[] = [];
for (const clause of clauses) {
if (topic && clause.topicKey !== topic) continue;
if (region && clause.region !== region) continue;
list.push({
id: clause.id,
title: clause.title,
topic: clause.topicLabel,
region: regionLabels[clause.region],
status: clause.status,
lastReviewed: clause.lastReviewed,
excerpt: clause.excerpt,
});
}
return list;
};
const summarizeStatus = (clauses: readonly ClauseRecord[]): StatusSummary => {
const summary: StatusSummary = { approved: 0, draft: 0, deprecated: 0 };
for (const clause of clauses) {
summary[clause.status] += 1;
}
return summary;
};
const buildSummaryLine = (input: {
filtered: number;
total: number;
topic: string;
region: string;
}): string => {
return `Showing ${input.filtered} of ${input.total} clauses for ${input.topic} in ${input.region}`;
};
const selectTopic = handler(
(
event: SelectTopicEvent | undefined,
context: {
clauses: Cell;
topicFilter: Cell;
},
) => {
const available = sanitizeClauseList(context.clauses.get());
if (available.length === 0) {
context.topicFilter.set(null);
return;
}
const requested = sanitizeTopicKey(event?.topic);
if (!requested) {
context.topicFilter.set(null);
return;
}
for (const clause of available) {
if (clause.topicKey === requested) {
context.topicFilter.set(requested);
return;
}
}
context.topicFilter.set(null);
},
);
const selectRegion = handler(
(
event: SelectRegionEvent | undefined,
context: {
regionFilter: Cell;
},
) => {
const resolved = sanitizeRegionKey(event?.region);
context.regionFilter.set(resolved);
},
);
const clearFilters = handler(
(
_event: unknown,
context: {
topicFilter: Cell;
regionFilter: Cell;
},
) => {
context.topicFilter.set(null);
context.regionFilter.set(null);
},
);
const updateClauseStatus = handler(
(
event: UpdateStatusEvent | undefined,
context: { clauses: Cell },
) => {
const id = sanitizeClauseId(event?.id);
const status = parseStatus(event?.status);
if (!id || !status) return;
const current = sanitizeClauseList(context.clauses.get());
let changed = false;
const updated = current.map((clause) => {
if (clause.id !== id) return clause;
changed = true;
const nextDate = sanitizeReviewDate(
event?.reviewedOn,
clause.lastReviewed,
);
return {
...clause,
status,
lastReviewed: nextDate,
};
});
if (!changed) return;
updated.sort(compareClauses);
context.clauses.set(toClauseInputs(updated));
},
);
export const legalClauseLibrary = recipe(
"Legal Clause Library",
({ clauses }) => {
const topicFilter = cell(null);
const regionFilter = cell(null);
const clauseCatalog = lift(sanitizeClauseList)(clauses);
const topicOptions = lift((input: {
clauses: ClauseRecord[];
active: string | null;
}) => buildTopicOptions(input.clauses, input.active))({
clauses: clauseCatalog,
active: topicFilter,
});
const regionOptions = lift((input: {
clauses: ClauseRecord[];
active: RegionCode | null;
}) => buildRegionOptions(input.clauses, input.active))({
clauses: clauseCatalog,
active: regionFilter,
});
const totalCount = lift((entries: ClauseRecord[]) => entries.length)(
clauseCatalog,
);
const filteredClauses = lift((input: {
clauses: ClauseRecord[];
topic: string | null;
region: RegionCode | null;
}) => buildFilteredClauses(input.clauses, input.topic, input.region))({
clauses: clauseCatalog,
topic: topicFilter,
region: regionFilter,
});
const filteredCount = lift((entries: ClausePreview[]) => entries.length)(
filteredClauses,
);
const activeTopicLabel = lift((input: {
active: string | null;
options: TopicOption[];
}) => resolveTopicLabel(input.active, input.options))({
active: topicFilter,
options: topicOptions,
});
const activeRegionLabel = lift((input: {
active: RegionCode | null;
options: RegionOption[];
}) => resolveRegionLabel(input.active, input.options))({
active: regionFilter,
options: regionOptions,
});
const statusSummary = lift((entries: ClauseRecord[]) =>
summarizeStatus(entries)
)(clauseCatalog);
const selectedTopic = lift((value: string | null) => value ?? "all")(
topicFilter,
);
const selectedRegion = lift((value: RegionCode | null) => value ?? "all")(
regionFilter,
);
const summaryLine = lift(buildSummaryLine)({
filtered: filteredCount,
total: totalCount,
topic: activeTopicLabel,
region: activeRegionLabel,
});
return {
clauseCatalog,
topicOptions,
regionOptions,
filteredClauses,
selectedTopic,
selectedRegion,
statusSummary,
summaryLine,
handlers: {
selectTopic: selectTopic({ clauses, topicFilter }),
selectRegion: selectRegion({ regionFilter }),
clearFilters: clearFilters({ topicFilter, regionFilter }),
updateClauseStatus: updateClauseStatus({ clauses }),
},
};
},
);
export type {
ClauseInput,
ClausePreview,
RegionCode,
RegionOption,
TopicOption,
};
export type { ClauseStatus };