import type { CommunityRuleEntry, CommunityRuleLabeledBlock, CommunityRuleSection, } from "../../app/components/type/CommunityRule/CommunityRule.types"; import type { PublishedMethodSelections } from "./buildPublishPayload"; import { templateCategoryToGroupKey } from "./templateReviewMapping"; /** Canonical `categoryName` strings for method groups in published documents. */ export const RULE_SECTION_CATEGORY = { values: "Values", communication: "Communication", membership: "Membership", decisionMaking: "Decision-making", conflictManagement: "Conflict management", } as const; const COMM_LABELS: Record = { corePrinciple: "Core Principle & Scope", logisticsAdmin: "Logistics, Admin & Norms", codeOfConduct: "Code of Conduct", }; const MEM_LABELS: Record = { eligibility: "Eligibility & Philosophy", joiningProcess: "Joining Process", expectations: "Expectations & Removal", }; const DEC_LABELS: Record = { corePrinciple: "Core Principle", applicableScope: "Applicable Scope", stepByStepInstructions: "Step-by-Step Instructions", consensusLevel: "Consensus Level", objectionsDeadlocks: "Objections & Deadlocks", }; const CM_LABELS: Record = { corePrinciple: "Core Principle", applicableScope: "Applicable Scope", processProtocol: "Process Protocol", restorationFallbacks: "Restoration & Fallbacks", }; export function nonEmptyTrimmed(s: unknown): string | null { if (typeof s !== "string") return null; const t = s.trim(); return t.length > 0 ? t : null; } export function formatScopePayload(val: unknown): string | null { if (typeof val === "string") return nonEmptyTrimmed(val); if (!Array.isArray(val)) return null; const lines = val.filter((x): x is string => typeof x === "string" && x.trim().length > 0); if (lines.length === 0) return null; return lines.join("\n"); } /** * Conflict-management applicable scope is a single textarea; preset JSON often * splits one sentence across multiple strings (legacy chip fragments). Join * with ", " for normal sentence display. Prefer non-empty `selectedApplicableScope` * when present, otherwise `applicableScope`. */ export function formatConflictApplicableScopeForTextarea( selectedApplicableScope: readonly string[], applicableScope: readonly string[], ): string { const sel = selectedApplicableScope.filter( (x): x is string => typeof x === "string" && x.trim().length > 0, ); const app = applicableScope.filter( (x): x is string => typeof x === "string" && x.trim().length > 0, ); const parts = sel.length > 0 ? sel : app; if (parts.length === 0) return ""; return parts.join(", "); } export function blocksFromKeyedRecord( sections: Record, labelByKey: Record, options?: { consensusLevelKey?: string; }, ): CommunityRuleLabeledBlock[] { const blocks: CommunityRuleLabeledBlock[] = []; for (const [key, label] of Object.entries(labelByKey)) { if (options?.consensusLevelKey === key) { const n = sections[key]; if (typeof n === "number" && !Number.isNaN(n)) { blocks.push({ label, body: `${n}%` }); } continue; } const raw = sections[key]; const text = key === "applicableScope" || key === "selectedApplicableScope" ? formatScopePayload(raw) : nonEmptyTrimmed(raw); if (text) blocks.push({ label, body: text }); } return blocks; } export function communityRuleEntryFromMethodChip( title: string, sections: Record, labelByKey: Record, options?: { consensusLevelKey?: string }, ): CommunityRuleEntry | null { const blocks = blocksFromKeyedRecord(sections, labelByKey, options); if (blocks.length === 0) return null; return { title, body: "", blocks }; } export function sectionFromCommunication( ms: NonNullable, ): CommunityRuleSection | null { if (ms.length === 0) return null; const entries: CommunityRuleEntry[] = []; for (const m of ms) { const sec = m.sections as unknown as Record; const e = communityRuleEntryFromMethodChip(m.label, sec, COMM_LABELS); if (e) entries.push(e); } return entries.length > 0 ? { categoryName: RULE_SECTION_CATEGORY.communication, entries } : null; } export function sectionFromMembership( ms: NonNullable, ): CommunityRuleSection | null { if (ms.length === 0) return null; const entries: CommunityRuleEntry[] = []; for (const m of ms) { const sec = m.sections as unknown as Record; const e = communityRuleEntryFromMethodChip(m.label, sec, MEM_LABELS); if (e) entries.push(e); } return entries.length > 0 ? { categoryName: RULE_SECTION_CATEGORY.membership, entries } : null; } export function sectionFromDecision( ms: NonNullable, ): CommunityRuleSection | null { if (ms.length === 0) return null; const entries: CommunityRuleEntry[] = []; for (const m of ms) { const sec = m.sections as unknown as Record; const merged: Record = { ...sec }; const scope = formatScopePayload(sec.selectedApplicableScope) ?? formatScopePayload(sec.applicableScope); if (scope) merged.applicableScope = scope; delete merged.selectedApplicableScope; const e = communityRuleEntryFromMethodChip(m.label, merged, DEC_LABELS, { consensusLevelKey: "consensusLevel", }); if (e) entries.push(e); } return entries.length > 0 ? { categoryName: RULE_SECTION_CATEGORY.decisionMaking, entries } : null; } export function sectionFromConflict( ms: NonNullable, ): CommunityRuleSection | null { if (ms.length === 0) return null; const entries: CommunityRuleEntry[] = []; for (const m of ms) { const sec = m.sections as unknown as Record; const merged: Record = { ...sec }; const scope = formatScopePayload(sec.selectedApplicableScope) ?? formatScopePayload(sec.applicableScope); if (scope) merged.applicableScope = scope; delete merged.selectedApplicableScope; const e = communityRuleEntryFromMethodChip(m.label, merged, CM_LABELS); if (e) entries.push(e); } return entries.length > 0 ? { categoryName: RULE_SECTION_CATEGORY.conflictManagement, entries } : null; } /** * Swap template `sections` method rows for fully-resolved entries built from * `methodSelections` (preset + overrides). */ export function replaceMethodSectionsWithMethodSelections( sections: CommunityRuleSection[], ms: PublishedMethodSelections, ): CommunityRuleSection[] { return sections.map((s) => { const gk = templateCategoryToGroupKey(s.categoryName); if (gk === "communication" && ms.communication?.length) { return sectionFromCommunication(ms.communication) ?? s; } if (gk === "membership" && ms.membership?.length) { return sectionFromMembership(ms.membership) ?? s; } if (gk === "decisionApproaches" && ms.decisionApproaches?.length) { return sectionFromDecision(ms.decisionApproaches) ?? s; } if (gk === "conflictManagement" && ms.conflictManagement?.length) { return sectionFromConflict(ms.conflictManagement) ?? s; } return s; }); }