import type { CommunityRuleSection } from "../../app/components/type/CommunityRule/CommunityRule.types"; import type { CommunityRuleLabeledBlock } from "../../app/components/type/CommunityRule/CommunityRule.types"; import type { PublishedMethodSelections } from "./buildPublishPayload"; import { parseDocumentSectionsForDisplay } from "./buildPublishPayload"; import { resolveMethodPresetIdFromLabel } from "./buildFinalReviewCategories"; import { resolveCoreValueChipIdFromLabel } from "./finalReviewChipPresets"; import { communicationPresetFor, conflictManagementPresetFor, decisionApproachPresetFor, membershipPresetFor, mergeCoreValueDetailWithPresets, } from "./finalReviewChipPresets"; import { templateCategoryToGroupKey } from "./templateReviewMapping"; import type { TemplateFacetGroupKey } from "./templateReviewMapping"; import { RULE_SECTION_CATEGORY } from "./ruleSectionsFromMethodSelections"; 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", }; const LABELS_BY_GROUP: Record< Exclude, Record > = { communication: COMM_LABELS, membership: MEM_LABELS, decisionApproaches: DEC_LABELS, conflictManagement: CM_LABELS, }; function slugifyId(label: string): string { const base = label .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return base.length > 0 ? base : "custom-method"; } function keyForLabel( label: string, labelByKey: Record, ): string | null { const trimmed = label.trim(); for (const [key, displayLabel] of Object.entries(labelByKey)) { if (displayLabel === trimmed) return key; } return null; } function parseConsensusPercent(body: string): number | null { const m = body.trim().match(/^(\d+)\s*%?$/); if (!m) return null; const n = Number(m[1]); return Number.isFinite(n) ? n : null; } function sectionsRecordFromBlocks( blocks: CommunityRuleLabeledBlock[], labelByKey: Record, options?: { consensusLevelKey?: string }, ): Record { const out: Record = {}; for (const block of blocks) { const key = keyForLabel(block.label, labelByKey); if (!key) continue; const body = block.body.trim(); if (options?.consensusLevelKey === key) { const pct = parseConsensusPercent(body); if (pct !== null) out[key] = pct; continue; } if (key === "applicableScope" || key === "selectedApplicableScope") { const parts = body .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); if (parts.length > 0) { out.selectedApplicableScope = parts; out.applicableScope = parts; } continue; } if (body.length > 0) out[key] = body; } return out; } function presetForMethod( groupKey: Exclude, id: string, ): Record { switch (groupKey) { case "communication": return { ...communicationPresetFor(id) } as Record; case "membership": return { ...membershipPresetFor(id) } as Record; case "decisionApproaches": return { ...decisionApproachPresetFor(id) } as Record; case "conflictManagement": return { ...conflictManagementPresetFor(id) } as Record; } } function sectionsRecordFromEntry( entry: CommunityRuleSection["entries"][number], groupKey: Exclude, presetId: string, ): Record { const labelByKey = LABELS_BY_GROUP[groupKey]; const consensusKey = groupKey === "decisionApproaches" ? "consensusLevel" : undefined; if (entry.blocks && entry.blocks.length > 0) { const fromBlocks = sectionsRecordFromBlocks(entry.blocks, labelByKey, { consensusLevelKey: consensusKey, }); if (Object.keys(fromBlocks).length > 0) { return { ...presetForMethod(groupKey, presetId), ...fromBlocks }; } } const body = (entry.body ?? "").trim(); if (body.length === 0) { return presetForMethod(groupKey, presetId); } return { ...presetForMethod(groupKey, presetId), corePrinciple: body }; } function coreValuesFromValuesSection( section: CommunityRuleSection, ): Array<{ chipId: string; label: string; meaning: string; signals: string }> { const out: Array<{ chipId: string; label: string; meaning: string; signals: string; }> = []; for (const entry of section.entries) { const label = entry.title.trim(); if (!label) continue; const body = (entry.body ?? "").trim(); const parts = body.length > 0 ? body.split(/\n\n+/) : []; const meaning = (parts[0] ?? "").trim(); const signals = parts.slice(1).join("\n\n").trim(); const merged = mergeCoreValueDetailWithPresets("", label, { meaning, signals, }); const chipId = resolveCoreValueChipIdFromLabel(label) ?? `hydrated-${label.toLowerCase()}`; out.push({ chipId, label, meaning: merged.meaning, signals: merged.signals, }); } return out; } type PublishedMethodRow = { id: string; label: string; sections: Record; }; function methodSelectionsFromDisplaySections( sections: CommunityRuleSection[], ): PublishedMethodSelections { const out: PublishedMethodSelections = {}; const pushGroup = ( key: keyof PublishedMethodSelections, groupKey: Exclude, section: CommunityRuleSection, ) => { const rows: PublishedMethodRow[] = []; for (const entry of section.entries) { const label = entry.title.trim(); if (!label) continue; const id = resolveMethodPresetIdFromLabel(label, groupKey) ?? `custom-${slugifyId(label)}`; rows.push({ id, label, sections: sectionsRecordFromEntry(entry, groupKey, id), }); } if (rows.length > 0) { switch (key) { case "communication": out.communication = rows as NonNullable< PublishedMethodSelections["communication"] >; break; case "membership": out.membership = rows as NonNullable< PublishedMethodSelections["membership"] >; break; case "decisionApproaches": out.decisionApproaches = rows as NonNullable< PublishedMethodSelections["decisionApproaches"] >; break; case "conflictManagement": out.conflictManagement = rows as NonNullable< PublishedMethodSelections["conflictManagement"] >; break; default: break; } } }; for (const section of sections) { const groupKey = templateCategoryToGroupKey(section.categoryName); if (!groupKey || groupKey === "coreValues") continue; switch (groupKey) { case "communication": pushGroup("communication", groupKey, section); break; case "membership": pushGroup("membership", groupKey, section); break; case "decisionApproaches": pushGroup("decisionApproaches", groupKey, section); break; case "conflictManagement": pushGroup("conflictManagement", groupKey, section); break; default: break; } } return out; } function hasMethodSelections(ms: PublishedMethodSelections): boolean { return Boolean( ms.communication?.length || ms.membership?.length || ms.decisionApproaches?.length || ms.conflictManagement?.length, ); } /** * Ensures a stored published `document` includes `methodSelections` and * `coreValues` derived from display `sections` when missing (e.g. use-case * template duplicates). Idempotent when the document is already normalized. */ export function normalizePublishedDocumentForEdit( document: unknown, ): Record { if (!document || typeof document !== "object" || Array.isArray(document)) { return {}; } const doc = { ...(document as Record) }; const sections = parseDocumentSectionsForDisplay(doc); const existingMs = doc.methodSelections; const hasMs = existingMs && typeof existingMs === "object" && !Array.isArray(existingMs) && hasMethodSelections(existingMs as PublishedMethodSelections); const existingCv = doc.coreValues; const hasCv = Array.isArray(existingCv) && existingCv.length > 0; if (!hasCv) { const valuesSection = sections.find( (s) => s.categoryName === RULE_SECTION_CATEGORY.values || templateCategoryToGroupKey(s.categoryName) === "coreValues", ); if (valuesSection) { const coreValues = coreValuesFromValuesSection(valuesSection); if (coreValues.length > 0) { doc.coreValues = coreValues; } } } if (!hasMs && sections.length > 0) { const methodSelections = methodSelectionsFromDisplaySections(sections); if (hasMethodSelections(methodSelections)) { doc.methodSelections = methodSelections; } } if (!Array.isArray(doc.sections) || doc.sections.length === 0) { doc.sections = sections; } return doc; }