Stop seeding 75% consensus on custom decision cards so wizard copy shows on the published rule.

Wire Back on the final-review chip editor to the same dismiss path as close, instead of a no-op.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-31 11:28:32 -06:00
co-authored by Cursor
parent 7f67cc6271
commit 511c3efb4c
20 changed files with 531 additions and 36 deletions
@@ -1078,6 +1078,7 @@ export function FinalReviewChipEditModal({
<Create <Create
isOpen={isOpen && !addCustomWizardOpen} isOpen={isOpen && !addCustomWizardOpen}
onClose={handleModalClose} onClose={handleModalClose}
onBack={handleModalClose}
backdropVariant="blurredYellow" backdropVariant="blurredYellow"
headerContent={headerContent} headerContent={headerContent}
showNextButton={true} showNextButton={true}
@@ -80,19 +80,21 @@ function DecisionApproachEditFieldsComponent({
onChange={(v) => patch("stepByStepInstructions", v)} onChange={(v) => patch("stepByStepInstructions", v)}
disabled={readOnly} disabled={readOnly}
/> />
<IncrementerBlock {value.consensusLevel !== undefined ? (
label={t.sectionHeadings.consensusLevel} <IncrementerBlock
helpIcon={false} label={t.sectionHeadings.consensusLevel}
value={value.consensusLevel} helpIcon={false}
min={CONSENSUS_LEVEL_MIN} value={value.consensusLevel}
max={CONSENSUS_LEVEL_MAX} min={CONSENSUS_LEVEL_MIN}
step={CONSENSUS_LEVEL_STEP} max={CONSENSUS_LEVEL_MAX}
onChange={(next) => patch("consensusLevel", next)} step={CONSENSUS_LEVEL_STEP}
formatValue={(v) => `${v}%`} onChange={(next) => patch("consensusLevel", next)}
decrementAriaLabel="Decrease consensus level" formatValue={(v) => `${v}%`}
incrementAriaLabel="Increase consensus level" decrementAriaLabel="Decrease consensus level"
disabled={readOnly} incrementAriaLabel="Increase consensus level"
/> disabled={readOnly}
/>
) : null}
<ModalTextAreaField <ModalTextAreaField
label={t.sectionHeadings.objectionsDeadlocks} label={t.sectionHeadings.objectionsDeadlocks}
value={value.objectionsDeadlocks} value={value.objectionsDeadlocks}
+5 -1
View File
@@ -85,7 +85,11 @@ export type DecisionApproachDetailEntry = {
applicableScope: string[]; applicableScope: string[];
selectedApplicableScope: string[]; selectedApplicableScope: string[];
stepByStepInstructions: string; stepByStepInstructions: string;
consensusLevel: number; /**
* Catalog presets always set this. User-authored custom cards omit it until
* the author adds a proportion field or edits consensus in the facet form.
*/
consensusLevel?: number;
objectionsDeadlocks: string; objectionsDeadlocks: string;
}; };
@@ -14,8 +14,8 @@ export interface CommunityRuleEntry {
/** Plain text; split on blank lines into paragraphs when rendering. */ /** Plain text; split on blank lines into paragraphs when rendering. */
body: string; body: string;
/** /**
* When set, rendered as Figma-style label + body stacks. If non-empty, takes * Figma-style label + body stacks (facet sections, wizard fields). Shown
* precedence over {@link body} for main content (body may be empty). * after {@link body} when both are present.
*/ */
blocks?: CommunityRuleLabeledBlock[]; blocks?: CommunityRuleLabeledBlock[];
} }
@@ -45,7 +45,7 @@ function CommunityRuleView({
<TextBlock <TextBlock
key={entryIndex} key={entryIndex}
title={entry.title} title={entry.title}
body={hasBlocks ? undefined : entry.body} body={entry.body}
rows={hasBlocks ? entry.blocks : undefined} rows={hasBlocks ? entry.blocks : undefined}
/> />
); );
@@ -94,9 +94,10 @@ function TextBlockView({
> >
<p className={`${ENTRY_TITLE_CLASS} w-full min-w-0`}>{title}</p> <p className={`${ENTRY_TITLE_CLASS} w-full min-w-0`}>{title}</p>
<div className="flex min-w-0 flex-col gap-3"> <div className="flex min-w-0 flex-col gap-3">
{body.trim().length > 0 ? <ParagraphGroup text={body} /> : null}
{hasRows {hasRows
? rows!.map((row, i) => <LabeledRowView key={i} row={row} />) ? rows!.map((row, i) => <LabeledRowView key={i} row={row} />)
: body.trim().length > 0 && <ParagraphGroup text={body} />} : null}
</div> </div>
</div> </div>
); );
+41 -2
View File
@@ -16,7 +16,10 @@ import {
publishedMethodDisplayLabel, publishedMethodDisplayLabel,
} from "./finalReviewChipPresets"; } from "./finalReviewChipPresets";
import { isDocumentEntry } from "./documentEntryGuards"; import { isDocumentEntry } from "./documentEntryGuards";
import { replaceMethodSectionsWithMethodSelections } from "./ruleSectionsFromMethodSelections"; import {
replaceMethodSectionsWithMethodSelections,
withoutUnpublishedDecisionConsensus,
} from "./ruleSectionsFromMethodSelections";
import { templateCategoryToGroupKey } from "./templateReviewMapping"; import { templateCategoryToGroupKey } from "./templateReviewMapping";
export { isDocumentEntry } from "./documentEntryGuards"; export { isDocumentEntry } from "./documentEntryGuards";
@@ -78,21 +81,25 @@ export type PublishedMethodSelections = {
id: string; id: string;
label: string; label: string;
sections: CommunicationMethodDetailEntry; sections: CommunicationMethodDetailEntry;
supportText?: string;
}>; }>;
membership?: Array<{ membership?: Array<{
id: string; id: string;
label: string; label: string;
sections: MembershipMethodDetailEntry; sections: MembershipMethodDetailEntry;
supportText?: string;
}>; }>;
decisionApproaches?: Array<{ decisionApproaches?: Array<{
id: string; id: string;
label: string; label: string;
sections: DecisionApproachDetailEntry; sections: DecisionApproachDetailEntry;
supportText?: string;
}>; }>;
conflictManagement?: Array<{ conflictManagement?: Array<{
id: string; id: string;
label: string; label: string;
sections: ConflictManagementDetailEntry; sections: ConflictManagementDetailEntry;
supportText?: string;
}>; }>;
}; };
@@ -247,6 +254,14 @@ function pickMethodIds(
return derived; return derived;
} }
function publishedRowSupportText(
id: string,
meta: CreateFlowState["customMethodCardMetaById"],
): string | undefined {
const t = meta?.[id]?.supportText?.trim();
return t && t.length > 0 ? t : undefined;
}
/** /**
* Merge `selected*MethodIds` with any saved `{group}MethodDetailsById` * Merge `selected*MethodIds` with any saved `{group}MethodDetailsById`
* overrides authored on the final-review screen. Preset defaults from the * overrides authored on the final-review screen. Preset defaults from the
@@ -270,6 +285,10 @@ export function buildMethodSelectionsForDocument(
out.communication = commIds.map((id) => { out.communication = commIds.map((id) => {
const preset = communicationPresetFor(id); const preset = communicationPresetFor(id);
const override = state.communicationMethodDetailsById?.[id]; const override = state.communicationMethodDetailsById?.[id];
const supportText = publishedRowSupportText(
id,
state.customMethodCardMetaById,
);
return { return {
id, id,
label: publishedMethodDisplayLabel( label: publishedMethodDisplayLabel(
@@ -278,6 +297,7 @@ export function buildMethodSelectionsForDocument(
state.customMethodCardMetaById, state.customMethodCardMetaById,
), ),
sections: override ? { ...preset, ...override } : preset, sections: override ? { ...preset, ...override } : preset,
...(supportText ? { supportText } : {}),
}; };
}); });
} }
@@ -290,6 +310,10 @@ export function buildMethodSelectionsForDocument(
out.membership = memIds.map((id) => { out.membership = memIds.map((id) => {
const preset = membershipPresetFor(id); const preset = membershipPresetFor(id);
const override = state.membershipMethodDetailsById?.[id]; const override = state.membershipMethodDetailsById?.[id];
const supportText = publishedRowSupportText(
id,
state.customMethodCardMetaById,
);
return { return {
id, id,
label: publishedMethodDisplayLabel( label: publishedMethodDisplayLabel(
@@ -298,6 +322,7 @@ export function buildMethodSelectionsForDocument(
state.customMethodCardMetaById, state.customMethodCardMetaById,
), ),
sections: override ? { ...preset, ...override } : preset, sections: override ? { ...preset, ...override } : preset,
...(supportText ? { supportText } : {}),
}; };
}); });
} }
@@ -310,6 +335,11 @@ export function buildMethodSelectionsForDocument(
out.decisionApproaches = daIds.map((id) => { out.decisionApproaches = daIds.map((id) => {
const preset = decisionApproachPresetFor(id); const preset = decisionApproachPresetFor(id);
const override = state.decisionApproachDetailsById?.[id]; const override = state.decisionApproachDetailsById?.[id];
const supportText = publishedRowSupportText(
id,
state.customMethodCardMetaById,
);
const merged = override ? { ...preset, ...override } : preset;
return { return {
id, id,
label: publishedMethodDisplayLabel( label: publishedMethodDisplayLabel(
@@ -317,7 +347,11 @@ export function buildMethodSelectionsForDocument(
id, id,
state.customMethodCardMetaById, state.customMethodCardMetaById,
), ),
sections: override ? { ...preset, ...override } : preset, sections: withoutUnpublishedDecisionConsensus(
{ ...merged },
state.customMethodCardFieldBlocksById?.[id],
) as DecisionApproachDetailEntry,
...(supportText ? { supportText } : {}),
}; };
}); });
} }
@@ -330,6 +364,10 @@ export function buildMethodSelectionsForDocument(
out.conflictManagement = cmIds.map((id) => { out.conflictManagement = cmIds.map((id) => {
const preset = conflictManagementPresetFor(id); const preset = conflictManagementPresetFor(id);
const override = state.conflictManagementDetailsById?.[id]; const override = state.conflictManagementDetailsById?.[id];
const supportText = publishedRowSupportText(
id,
state.customMethodCardMetaById,
);
return { return {
id, id,
label: publishedMethodDisplayLabel( label: publishedMethodDisplayLabel(
@@ -338,6 +376,7 @@ export function buildMethodSelectionsForDocument(
state.customMethodCardMetaById, state.customMethodCardMetaById,
), ),
sections: override ? { ...preset, ...override } : preset, sections: override ? { ...preset, ...override } : preset,
...(supportText ? { supportText } : {}),
}; };
}); });
} }
+9 -6
View File
@@ -106,7 +106,7 @@ export function membershipPresetFor(id: string): MembershipMethodDetailEntry {
}; };
} }
/** Default consensus level used when presets omit a value (see DecisionApproachesScreen). */ /** Default consensus level used when a **catalog** preset omits a value. */
export const DECISION_CONSENSUS_LEVEL_DEFAULT = 75; export const DECISION_CONSENSUS_LEVEL_DEFAULT = 75;
export function decisionApproachPresetFor( export function decisionApproachPresetFor(
@@ -114,19 +114,22 @@ export function decisionApproachPresetFor(
): DecisionApproachDetailEntry { ): DecisionApproachDetailEntry {
const method = findMethod(decisionApproachesMessages, id); const method = findMethod(decisionApproachesMessages, id);
const s = method?.sections ?? {}; const s = method?.sections ?? {};
return { const entry: DecisionApproachDetailEntry = {
corePrinciple: asString(s.corePrinciple), corePrinciple: asString(s.corePrinciple),
applicableScope: asStringArray(s.applicableScope), applicableScope: asStringArray(s.applicableScope),
selectedApplicableScope: [], selectedApplicableScope: [],
stepByStepInstructions: asString(s.stepByStepInstructions), stepByStepInstructions: asString(s.stepByStepInstructions),
consensusLevel: asNumberClamped( objectionsDeadlocks: asString(s.objectionsDeadlocks),
};
if (method) {
entry.consensusLevel = asNumberClamped(
s.consensusLevel, s.consensusLevel,
0, 0,
100, 100,
DECISION_CONSENSUS_LEVEL_DEFAULT, DECISION_CONSENSUS_LEVEL_DEFAULT,
), );
objectionsDeadlocks: asString(s.objectionsDeadlocks), }
}; return entry;
} }
export function conflictManagementPresetFor( export function conflictManagementPresetFor(
+9 -2
View File
@@ -195,12 +195,19 @@ function mapFacetPrefillToWizardFieldBlocks(
prefill.headings.stepByStepInstructions, prefill.headings.stepByStepInstructions,
prefill.draft.stepByStepInstructions, prefill.draft.stepByStepInstructions,
), ),
{ );
if (
typeof prefill.draft.consensusLevel === "number" &&
facetPrefillHasContent(prefill)
) {
blocks.push({
kind: "proportion", kind: "proportion",
id: "facet-consensusLevel", id: "facet-consensusLevel",
blockTitle: prefill.headings.consensusLevel, blockTitle: prefill.headings.consensusLevel,
defaultPercent: clampPercent(prefill.draft.consensusLevel), defaultPercent: clampPercent(prefill.draft.consensusLevel),
}, });
}
blocks.push(
textBlock( textBlock(
"facet-objectionsDeadlocks", "facet-objectionsDeadlocks",
prefill.headings.objectionsDeadlocks, prefill.headings.objectionsDeadlocks,
@@ -22,6 +22,7 @@ function customMethodCardMetaFromPublishedSelections(
| Array<{ | Array<{
id: string; id: string;
label: string; label: string;
supportText?: string;
}> }>
| undefined, | undefined,
) => { ) => {
@@ -32,7 +33,9 @@ function customMethodCardMetaFromPublishedSelections(
if (methodLabelFor(groupKey, id).length > 0) continue; if (methodLabelFor(groupKey, id).length > 0) continue;
const label = typeof row.label === "string" ? row.label.trim() : ""; const label = typeof row.label === "string" ? row.label.trim() : "";
if (!label) continue; if (!label) continue;
meta[id] = { label, supportText: "" }; const supportText =
typeof row.supportText === "string" ? row.supportText : "";
meta[id] = { label, supportText };
} }
}; };
absorb("communication", ms.communication); absorb("communication", ms.communication);
+76 -6
View File
@@ -78,6 +78,8 @@ export function labeledBlocksFromCustomMethodCardFieldBlocks(
export type CommunityRuleEntryFromChipOptions = { export type CommunityRuleEntryFromChipOptions = {
consensusLevelKey?: string; consensusLevelKey?: string;
customFieldBlocks?: CustomMethodCardFieldBlock[]; customFieldBlocks?: CustomMethodCardFieldBlock[];
/** Wizard step-2 policy description (`customMethodCardMetaById.supportText`). */
supportText?: string;
}; };
/** Canonical `categoryName` strings for method groups in published documents. */ /** Canonical `categoryName` strings for method groups in published documents. */
@@ -195,8 +197,64 @@ export function communityRuleEntryFromMethodChip(
? labeledBlocksFromCustomMethodCardFieldBlocks(options.customFieldBlocks) ? labeledBlocksFromCustomMethodCardFieldBlocks(options.customFieldBlocks)
: []; : [];
const blocks = [...presetBlocks, ...wizardBlocks]; const blocks = [...presetBlocks, ...wizardBlocks];
if (blocks.length === 0) return null; const description = nonEmptyTrimmed(options?.supportText);
return { title, body: "", blocks }; if (blocks.length === 0) {
if (!description) return null;
return { title, body: description };
}
return {
title,
body: description ?? "",
blocks,
};
}
function decisionApproachHasPublishableFacetCopy(
sections: Record<string, unknown>,
): boolean {
return Boolean(
nonEmptyTrimmed(sections.corePrinciple) ||
nonEmptyTrimmed(sections.stepByStepInstructions) ||
nonEmptyTrimmed(sections.objectionsDeadlocks) ||
formatScopePayload(sections.applicableScope) ||
formatScopePayload(sections.selectedApplicableScope),
);
}
/**
* Catalog methods publish their consensus figure. User-authored custom cards
* often seed `75` with empty facet copy — skip that unless the author actually
* filled decision sections. Wizard field blocks (including proportion) are the
* source of truth when present.
*/
function shouldPublishDecisionConsensusLevel(
sections: Record<string, unknown>,
customFieldBlocks?: CustomMethodCardFieldBlock[],
): boolean {
if (
typeof sections.consensusLevel !== "number" ||
Number.isNaN(sections.consensusLevel)
) {
return false;
}
if (customFieldBlocks && customFieldBlocks.length > 0) {
return false;
}
return decisionApproachHasPublishableFacetCopy(sections);
}
/** Drop seeded / wizard-superseded `consensusLevel` before publish or hydrate. */
export function withoutUnpublishedDecisionConsensus(
sections: Record<string, unknown>,
customFieldBlocks?: CustomMethodCardFieldBlock[],
): Record<string, unknown> {
if (shouldPublishDecisionConsensusLevel(sections, customFieldBlocks)) {
return sections;
}
if (!("consensusLevel" in sections)) return sections;
const next = { ...sections };
delete next.consensusLevel;
return next;
} }
export function sectionFromCommunication( export function sectionFromCommunication(
@@ -209,6 +267,7 @@ export function sectionFromCommunication(
const sec = m.sections as unknown as Record<string, unknown>; const sec = m.sections as unknown as Record<string, unknown>;
const e = communityRuleEntryFromMethodChip(m.label, sec, COMM_LABELS, { const e = communityRuleEntryFromMethodChip(m.label, sec, COMM_LABELS, {
customFieldBlocks: customFieldBlocksById?.[m.id], customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText,
}); });
if (e) entries.push(e); if (e) entries.push(e);
} }
@@ -227,6 +286,7 @@ export function sectionFromMembership(
const sec = m.sections as unknown as Record<string, unknown>; const sec = m.sections as unknown as Record<string, unknown>;
const e = communityRuleEntryFromMethodChip(m.label, sec, MEM_LABELS, { const e = communityRuleEntryFromMethodChip(m.label, sec, MEM_LABELS, {
customFieldBlocks: customFieldBlocksById?.[m.id], customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText,
}); });
if (e) entries.push(e); if (e) entries.push(e);
} }
@@ -254,10 +314,19 @@ export function sectionFromDecision(
); );
if (scope) merged.applicableScope = scope; if (scope) merged.applicableScope = scope;
delete merged.selectedApplicableScope; delete merged.selectedApplicableScope;
const e = communityRuleEntryFromMethodChip(m.label, merged, DEC_LABELS, { const e = communityRuleEntryFromMethodChip(
consensusLevelKey: "consensusLevel", m.label,
customFieldBlocks: customFieldBlocksById?.[m.id], withoutUnpublishedDecisionConsensus(
}); merged,
customFieldBlocksById?.[m.id],
),
DEC_LABELS,
{
consensusLevelKey: "consensusLevel",
customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText,
},
);
if (e) entries.push(e); if (e) entries.push(e);
} }
return entries.length > 0 return entries.length > 0
@@ -281,6 +350,7 @@ export function sectionFromConflict(
delete merged.selectedApplicableScope; delete merged.selectedApplicableScope;
const e = communityRuleEntryFromMethodChip(m.label, merged, CM_LABELS, { const e = communityRuleEntryFromMethodChip(m.label, merged, CM_LABELS, {
customFieldBlocks: customFieldBlocksById?.[m.id], customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText,
}); });
if (e) entries.push(e); if (e) entries.push(e);
} }
+1 -1
View File
@@ -54,7 +54,7 @@ const decisionApproachDetailEntrySchema = z.object({
applicableScope: z.array(z.string().max(2000)).max(50), applicableScope: z.array(z.string().max(2000)).max(50),
selectedApplicableScope: z.array(z.string().max(2000)).max(50), selectedApplicableScope: z.array(z.string().max(2000)).max(50),
stepByStepInstructions: z.string().max(8000), stepByStepInstructions: z.string().max(8000),
consensusLevel: z.number().int().min(0).max(100), consensusLevel: z.number().int().min(0).max(100).optional(),
objectionsDeadlocks: z.string().max(8000), objectionsDeadlocks: z.string().max(8000),
}); });
+22
View File
@@ -50,4 +50,26 @@ describe("CommunityRule", () => {
); );
expect(screen.getByText("How proposals pass")).toBeInTheDocument(); expect(screen.getByText("How proposals pass")).toBeInTheDocument();
}); });
it("renders entry body together with labeled blocks", () => {
render(
<CommunityRule
sections={[
{
categoryName: "Decision-making",
entries: [
{
title: "Forum proposals",
body: "Anyone can start a thread.",
blocks: [{ label: "Quorum", body: "60%" }],
},
],
},
]}
/>,
);
expect(screen.getByText("Anyone can start a thread.")).toBeInTheDocument();
expect(screen.getByText("Quorum")).toBeInTheDocument();
expect(screen.getByText("60%")).toBeInTheDocument();
});
}); });
+19
View File
@@ -519,6 +519,25 @@ describe("FinalReviewScreen — chip detail modal", () => {
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("closes the chip edit modal when Back is pressed", async () => {
render(
<FinalReviewWithStateProbe
onState={() => {}}
initial={{
title: "Oak Park Commons",
selectedCommunicationMethodIds: ["signal"],
}}
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "Back" }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
}); });
/** /**
+13
View File
@@ -45,4 +45,17 @@ describe("TextBlock", () => {
"/api/uploads/aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee", "/api/uploads/aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee",
); );
}); });
it("renders body paragraphs above labeled rows when both are set", () => {
render(
<TextBlock
title="Forum proposals"
body="Anyone can start a thread."
rows={[{ label: "Quorum", body: "60%" }]}
/>,
);
expect(screen.getByText("Anyone can start a thread.")).toBeInTheDocument();
expect(screen.getByText("Quorum")).toBeInTheDocument();
expect(screen.getByText("60%")).toBeInTheDocument();
});
}); });
+49
View File
@@ -249,6 +249,55 @@ describe("buildPublishPayload — methodSelections", () => {
expect(entry.sections.corePrinciple.length).toBeGreaterThan(0); expect(entry.sections.corePrinciple.length).toBeGreaterThan(0);
}); });
it("does not seed 75% consensus on a custom decision-making card", () => {
const customId = "00000000-0000-4000-8000-000000000170";
const r = buildPublishPayload({
title: "T",
selectedDecisionApproachIds: [customId],
customMethodCardMetaById: {
[customId]: {
label: "Forum proposals",
supportText: "Anyone can start a thread.",
},
},
decisionApproachDetailsById: {
[customId]: {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "",
consensusLevel: 75,
objectionsDeadlocks: "",
},
},
});
expect(r.ok).toBe(true);
if (!r.ok) return;
const ms = r.document.methodSelections as {
decisionApproaches?: Array<{
supportText?: string;
sections: { consensusLevel?: number };
}>;
};
expect(ms.decisionApproaches?.[0]?.supportText).toBe(
"Anyone can start a thread.",
);
expect(ms.decisionApproaches?.[0]?.sections.consensusLevel).toBeUndefined();
});
it("keeps catalog decision consensus in methodSelections", () => {
const r = buildPublishPayload({
title: "T",
selectedDecisionApproachIds: ["lazy-consensus"],
});
expect(r.ok).toBe(true);
if (!r.ok) return;
const ms = r.document.methodSelections as {
decisionApproaches?: Array<{ sections: { consensusLevel?: number } }>;
};
expect(ms.decisionApproaches?.[0]?.sections.consensusLevel).toBe(100);
});
it("merges override on top of preset for the selected method", () => { it("merges override on top of preset for the selected method", () => {
const r = buildPublishPayload({ const r = buildPublishPayload({
title: "T", title: "T",
@@ -3,6 +3,7 @@ import {
buildMethodCardWizardInitialValues, buildMethodCardWizardInitialValues,
coreValueDetailsFromWizardFieldBlocks, coreValueDetailsFromWizardFieldBlocks,
facetDetailsToWizardFieldBlocks, facetDetailsToWizardFieldBlocks,
overlayFacetPrefillValues,
} from "../../lib/create/methodCardWizardPrefill"; } from "../../lib/create/methodCardWizardPrefill";
import type { CustomMethodCardFieldBlock } from "../../lib/create/customMethodCardFieldBlocks"; import type { CustomMethodCardFieldBlock } from "../../lib/create/customMethodCardFieldBlocks";
@@ -219,6 +220,49 @@ describe("facetDetailsToWizardFieldBlocks", () => {
}); });
}); });
it("omits a consensus proportion when the draft has no consensus level", () => {
const blocksOut = facetDetailsToWizardFieldBlocks({
group: "decisionApproaches",
draft: {
corePrinciple: "Momentum.",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "Post a deadline.",
objectionsDeadlocks: "",
},
headings: {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
stepByStepInstructions: "Step-by-Step Instructions",
consensusLevel: "Consensus Level",
objectionsDeadlocks: "Objections & Deadlocks",
},
});
expect(blocksOut.find((b) => b.kind === "proportion")).toBeUndefined();
});
it("does not inject a seeded 75% proportion onto empty custom drafts", () => {
const next = overlayFacetPrefillValues([], {
group: "decisionApproaches",
draft: {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "",
consensusLevel: 75,
objectionsDeadlocks: "",
},
headings: {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
stepByStepInstructions: "Step-by-Step Instructions",
consensusLevel: "Consensus Level",
objectionsDeadlocks: "Objections & Deadlocks",
},
});
expect(next).toEqual([]);
});
it("maps core value meaning and signals onto text blocks", () => { it("maps core value meaning and signals onto text blocks", () => {
expect( expect(
facetDetailsToWizardFieldBlocks({ facetDetailsToWizardFieldBlocks({
@@ -314,6 +314,37 @@ describe("createFlowStateFromPublishedRule", () => {
}); });
}); });
it("hydrates wizard supportText from published methodSelections", () => {
const customId = "b7c0a9f3-0000-4000-8000-000000000002";
const partial = createFlowStateFromPublishedRule({
id: "rule-custom-desc",
title: "C",
summary: "",
document: {
methodSelections: {
decisionApproaches: [
{
id: customId,
label: "Forum proposals",
supportText: "Anyone can start a thread.",
sections: {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "",
objectionsDeadlocks: "",
},
},
],
},
},
});
expect(partial.customMethodCardMetaById?.[customId]).toEqual({
label: "Forum proposals",
supportText: "Anyone can start a thread.",
});
});
it("sets sections to [] even when methodSelections is missing (edit hydrate)", () => { it("sets sections to [] even when methodSelections is missing (edit hydrate)", () => {
const partial = createFlowStateFromPublishedRule({ const partial = createFlowStateFromPublishedRule({
id: "rule-2", id: "rule-2",
@@ -248,4 +248,35 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
}, },
]); ]);
}); });
it("shows custom decision description without a seeded 75% consensus row", () => {
const customId = "00000000-0000-4000-8000-000000000170";
const out = parsePublishedDocumentForCommunityRuleDisplay({
sections: [],
methodSelections: {
decisionApproaches: [
{
id: customId,
label: "Forum proposals",
supportText: "Anyone can start a thread.",
sections: {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "",
consensusLevel: 75,
objectionsDeadlocks: "",
},
},
],
},
});
const decision = out.find((s) => s.categoryName === "Decision-making");
expect(decision?.entries).toEqual([
{
title: "Forum proposals",
body: "Anyone can start a thread.",
},
]);
});
}); });
@@ -0,0 +1,156 @@
import { describe, expect, it } from "vitest";
import {
communityRuleEntryFromMethodChip,
sectionFromDecision,
withoutUnpublishedDecisionConsensus,
} from "../../lib/create/ruleSectionsFromMethodSelections";
const emptyDecisionSections = {
corePrinciple: "",
applicableScope: [] as string[],
selectedApplicableScope: [] as string[],
stepByStepInstructions: "",
consensusLevel: 75,
objectionsDeadlocks: "",
};
describe("withoutUnpublishedDecisionConsensus", () => {
it("keeps catalog consensus when facet copy is present", () => {
const next = withoutUnpublishedDecisionConsensus({
corePrinciple: "Momentum.",
consensusLevel: 100,
});
expect(next.consensusLevel).toBe(100);
});
it("drops seeded consensus on empty custom facet copy", () => {
const next = withoutUnpublishedDecisionConsensus({ ...emptyDecisionSections });
expect(next.consensusLevel).toBeUndefined();
});
it("drops keyed consensus when wizard field blocks exist", () => {
const next = withoutUnpublishedDecisionConsensus(
{ corePrinciple: "Momentum.", consensusLevel: 75 },
[
{
kind: "proportion",
id: "p1",
blockTitle: "Quorum",
defaultPercent: 60,
},
],
);
expect(next.consensusLevel).toBeUndefined();
});
});
describe("communityRuleEntryFromMethodChip", () => {
it("publishes supportText as body when there are no labeled blocks", () => {
expect(
communityRuleEntryFromMethodChip(
"Our process",
{ corePrinciple: "" },
{ corePrinciple: "Core Principle" },
{ supportText: " Members propose in the forum. " },
),
).toEqual({
title: "Our process",
body: "Members propose in the forum.",
});
});
it("keeps supportText alongside wizard blocks", () => {
const entry = communityRuleEntryFromMethodChip(
"Our process",
{ corePrinciple: "" },
{ corePrinciple: "Core Principle" },
{
supportText: "How we decide.",
customFieldBlocks: [
{
kind: "text",
id: "b1",
blockTitle: "Notes",
placeholderText: "Post in #governance.",
},
],
},
);
expect(entry).toEqual({
title: "Our process",
body: "How we decide.",
blocks: [{ label: "Notes", body: "Post in #governance." }],
});
});
});
describe("sectionFromDecision", () => {
it("omits seeded 75% and shows the policy description on a custom card", () => {
const section = sectionFromDecision([
{
id: "00000000-0000-4000-8000-000000000001",
label: "Forum proposals",
supportText: "Anyone can start a thread; silence after a week is consent.",
sections: { ...emptyDecisionSections },
},
]);
expect(section?.categoryName).toBe("Decision-making");
expect(section?.entries).toEqual([
{
title: "Forum proposals",
body: "Anyone can start a thread; silence after a week is consent.",
},
]);
});
it("still emits catalog consensus when facet copy is present", () => {
const section = sectionFromDecision([
{
id: "lazy-consensus",
label: "Lazy Consensus",
sections: {
corePrinciple: "Silence is consent.",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "Post a deadline.",
consensusLevel: 100,
objectionsDeadlocks: "Any member can block.",
},
},
]);
const blocks = section?.entries[0]?.blocks ?? [];
expect(blocks).toContainEqual({
label: "Consensus Level",
body: "100%",
});
});
it("does not duplicate consensus when a wizard proportion block exists", () => {
const section = sectionFromDecision(
[
{
id: "00000000-0000-4000-8000-000000000002",
label: "Custom vote",
supportText: "We vote in person.",
sections: {
...emptyDecisionSections,
consensusLevel: 75,
},
},
],
{
"00000000-0000-4000-8000-000000000002": [
{
kind: "proportion",
id: "q",
blockTitle: "Quorum",
defaultPercent: 60,
},
],
},
);
const blocks = section?.entries[0]?.blocks ?? [];
expect(blocks).toEqual([{ label: "Quorum", body: "60%" }]);
expect(section?.entries[0]?.body).toBe("We vote in person.");
});
});