Import the remade decks on stable ids and recommend one card per job, so a saved draft still loads after a chip is renamed.

The real-group profiles stay as a test, so a change that moves a pick fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilalloandCursor committed 2026-11-04 14:22:54 -07:00
1 parent 96ca15d8e3
commit bcbc6f30d9
89 files changed
+36555 -2828

No files matched your search

@@ -92,7 +92,15 @@ function DecisionApproachEditFieldsComponent({
generateDraftLabel={draftField.generateDraft} generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint} generateDraftHint={draftField.generateDraftHint}
/> />
{value.consensusLevel !== undefined ? ( {value.consensusLevel === undefined ? (
<ModalTextAreaField
label={t.sectionHeadings.consensusLevel}
value={t.consensusLevelDisplay.supermajorityShare}
onChange={() => {}}
disabled
rows={1}
/>
) : (
<IncrementerBlock <IncrementerBlock
label={t.sectionHeadings.consensusLevel} label={t.sectionHeadings.consensusLevel}
helpIcon={false} helpIcon={false}
@@ -101,12 +109,14 @@ function DecisionApproachEditFieldsComponent({
max={CONSENSUS_LEVEL_MAX} max={CONSENSUS_LEVEL_MAX}
step={CONSENSUS_LEVEL_STEP} step={CONSENSUS_LEVEL_STEP}
onChange={(next) => patch("consensusLevel", next)} onChange={(next) => patch("consensusLevel", next)}
formatValue={(v) => `${v}%`} formatValue={(v) =>
v === 0 ? t.consensusLevelDisplay.noSetShare : `${v}%`
}
decrementAriaLabel="Decrease consensus level" decrementAriaLabel="Decrease consensus level"
incrementAriaLabel="Increase consensus level" incrementAriaLabel="Increase consensus level"
disabled={readOnly} disabled={readOnly}
/> />
) : null} )}
<ModalTextAreaField <ModalTextAreaField
label={t.sectionHeadings.objectionsDeadlocks} label={t.sectionHeadings.objectionsDeadlocks}
value={value.objectionsDeadlocks} value={value.objectionsDeadlocks}
@@ -9,6 +9,12 @@ import {
loadFacetScores, loadFacetScores,
} from "../../../../lib/create/facetRecommendationsLoad"; } from "../../../../lib/create/facetRecommendationsLoad";
import { useCreateFlow } from "../context/CreateFlowContext"; import { useCreateFlow } from "../context/CreateFlowContext";
import { groupAnswersFromCreateFlowState } from "../../../../lib/create/groupAnswers";
import {
deckData,
recommendDeck,
} from "../../../../lib/create/deckRecommend";
import type { GroupAnswers } from "../../../../lib/create/deckTypes";
/** /**
* Card-deck section ids served by `/api/create-flow/methods` (CR-88 §9.2). * Card-deck section ids served by `/api/create-flow/methods` (CR-88 §9.2).
@@ -125,63 +131,46 @@ export function useFacetRecommendations(
} }
/** /**
* Stable comparator for re-ranking a messages-file `methods[]` array. Higher * One card per job, then the display rule for the top `limit` cards.
* `scoresBySlug[id]` first; ties fall back to authoring index, so a * Cards marked every-rule carry no Recommended tag. With no facets, the
* zero-facet user sees the original ordering verbatim. * first `limit` cards in deck order show and nothing is tagged.
*/ */
export function rankMethodsByScore<T extends { id: string }>( export function deriveCompactCards(
methods: readonly T[], section: RecommendationSection,
scoresBySlug: Record<string, number>, answers: GroupAnswers,
): T[] {
const indexById = new Map<string, number>();
methods.forEach((m, i) => indexById.set(m.id, i));
return [...methods].sort((a, b) => {
const sa = scoresBySlug[a.id] ?? 0;
const sb = scoresBySlug[b.id] ?? 0;
if (sa !== sb) return sb - sa;
return (indexById.get(a.id) ?? 0) - (indexById.get(b.id) ?? 0);
});
}
/**
* Picks (a) which method ids fill the compact card stack and (b) which of
* those should render with the "Recommended" tag. The messages JSON no
* longer carries a static `recommended` flag — both selections come
* entirely from facet scores (CR-88 §10).
*
* Behavior:
* - Facets selected & at least one method scored > 0 →
* `compactCardIds` = up to `limit` top-scored methods (1..limit cards;
* never padded with unrecommended fillers). All shown cards get the
* "Recommended" badge.
* - No facets selected, or every method scored 0 → `compactCardIds` =
* first `limit` in ranked/authoring order, `recommendedIds` empty (no
* badges shown — honest "no signal yet" fallback).
*
* `CardStack.view` is responsible for laying out variable-length compact
* arrays gracefully (uses `.map`/`.slice` and length-guarded indexing).
*/
export function deriveCompactCards<T extends { id: string }>(
rankedMethods: readonly T[],
scoresBySlug: Record<string, number>,
hasAnyFacets: boolean,
limit: number, limit: number,
): { compactCardIds: string[]; recommendedIds: Set<string> } { ): {
const fallback = () => ({ compactCardIds: string[];
compactCardIds: rankedMethods.slice(0, limit).map((m) => m.id), recommendedIds: Set<string>;
recommendedIds: new Set<string>(), orderedIds: string[];
}); } {
const recommendation = recommendDeck(deckData(section), answers, { limit });
if (!hasAnyFacets) return fallback(); const orderedIds = recommendation.fallback
? recommendation.seeAll
const matched = rankedMethods.filter( : [
(m) => (scoresBySlug[m.id] ?? 0) > 0, ...recommendation.shown.map((card) => card.id),
); ...recommendation.seeAll,
if (matched.length === 0) return fallback(); ];
if (recommendation.fallback) {
const top = matched.slice(0, limit); return {
compactCardIds: orderedIds.slice(0, limit),
recommendedIds: new Set<string>(),
orderedIds,
};
}
return { return {
compactCardIds: top.map((m) => m.id), compactCardIds: recommendation.shown.map((card) => card.id),
recommendedIds: new Set(top.map((m) => m.id)), recommendedIds: new Set(
recommendation.recommended
.filter((card) => card.tag === "recommended")
.map((card) => card.id),
),
orderedIds,
}; };
} }
export function groupAnswersForRecommendations(
state: Parameters<typeof groupAnswersFromCreateFlowState>[0],
): GroupAnswers {
return groupAnswersFromCreateFlowState(state);
}
@@ -4,68 +4,80 @@ import { useMemo } from "react";
import { mergeCompactCardIdsWithPinnedSelected } from "../../../../lib/create/methodCardDisplayOrder"; import { mergeCompactCardIdsWithPinnedSelected } from "../../../../lib/create/methodCardDisplayOrder";
import { import {
deriveCompactCards, deriveCompactCards,
rankMethodsByScore, groupAnswersForRecommendations,
useFacetRecommendations,
type RecommendationSection, type RecommendationSection,
} from "./useFacetRecommendations"; } from "./useFacetRecommendations";
import { useCreateFlow } from "../context/CreateFlowContext";
type MethodEntry = { id: string; label: string; supportText: string }; type MethodEntry = { id: string; label: string; supportText: string };
function orderByIds<T extends { id: string }>(
methods: readonly T[],
orderedIds: readonly string[],
): T[] {
const byId = new Map(methods.map((method) => [method.id, method]));
const ordered = orderedIds.flatMap((id) => {
const method = byId.get(id);
return method ? [method] : [];
});
const seen = new Set(ordered.map((method) => method.id));
return [...ordered, ...methods.filter((method) => !seen.has(method.id))];
}
/** /**
* Applies score ranking and compact-slot rules. Expanded CardStack order stays * Deck order and the compact stack come from the job rule. Current screens
* the ranked catalog (selected cards keep their place). Compact slots still * show five cards; a deck screen passes seven. Selected cards still pin
* pin selected ids first so a pick outside the unpinned top-N remains visible * into the compact stack.
* when the stack is collapsed.
*/ */
export function useMethodCardDeckOrdering( export function useMethodCardDeckOrdering(
section: RecommendationSection, section: RecommendationSection,
methods: readonly MethodEntry[], methods: readonly MethodEntry[],
selectedIds: readonly string[], selectedIds: readonly string[],
) { ) {
const { scoresBySlug, hasAnyFacets, isReady } = const { state } = useCreateFlow();
useFacetRecommendations(section); const answers = useMemo(
const recommendationsReady = !hasAnyFacets || isReady; () => groupAnswersForRecommendations(state),
[state],
);
const deck = useMemo(
() => deriveCompactCards(section, answers, 5),
[section, answers],
);
const rankedMethods = useMemo( const rankedMethods = useMemo(
() => rankMethodsByScore(methods, scoresBySlug), () => orderByIds(methods, deck.orderedIds),
[methods, scoresBySlug], [methods, deck.orderedIds],
); );
const selectionShowcaseActive = selectedIds.length > 0; const selectionShowcaseActive = selectedIds.length > 0;
const displayMethods = rankedMethods; const { recommendedIds } = deck;
const { compactCardIds: baseCompactCardIds, recommendedIds } = useMemo(
() =>
deriveCompactCards(
rankedMethods,
scoresBySlug,
hasAnyFacets,
/* limit */ 5,
),
[rankedMethods, scoresBySlug, hasAnyFacets],
);
const compactCardIds = useMemo( const compactCardIds = useMemo(
() => () =>
mergeCompactCardIdsWithPinnedSelected( mergeCompactCardIdsWithPinnedSelected(
rankedMethods.map((m) => m.id), rankedMethods.map((method) => method.id),
baseCompactCardIds, deck.compactCardIds,
selectedIds, selectedIds,
selectionShowcaseActive, selectionShowcaseActive,
5, 5,
), ),
[rankedMethods, baseCompactCardIds, selectedIds, selectionShowcaseActive], [
rankedMethods,
deck.compactCardIds,
selectedIds,
selectionShowcaseActive,
],
); );
const sampleCards = useMemo( const sampleCards = useMemo(
() => () =>
displayMethods.map((entry) => ({ rankedMethods.map((entry) => ({
id: entry.id, id: entry.id,
label: entry.label, label: entry.label,
supportText: entry.supportText, supportText: entry.supportText,
recommended: recommendedIds.has(entry.id), recommended: recommendedIds.has(entry.id),
})), })),
[displayMethods, recommendedIds], [rankedMethods, recommendedIds],
); );
const methodById = useMemo( const methodById = useMemo(
@@ -75,11 +87,11 @@ export function useMethodCardDeckOrdering(
return { return {
rankedMethods, rankedMethods,
displayMethods, displayMethods: rankedMethods,
compactCardIds, compactCardIds,
recommendedIds, recommendedIds,
sampleCards, sampleCards,
methodById, methodById,
recommendationsReady, recommendationsReady: true,
}; };
} }
@@ -113,7 +113,10 @@ export function useTemplateReviewActions({
setTemplateReviewApplyError(loaded.message); setTemplateReviewApplyError(loaded.message);
return; return;
} }
const prefill = buildTemplateCustomizePrefill(loaded.template.body); const prefill = buildTemplateCustomizePrefill(
loaded.template.body,
templateReviewSlug,
);
const pinPatch = methodSectionsPinsForHydratedSelections(prefill); const pinPatch = methodSectionsPinsForHydratedSelections(prefill);
const hasCommunityName = const hasCommunityName =
typeof state.title === "string" && state.title.trim().length > 0; typeof state.title === "string" && state.title.trim().length > 0;
@@ -182,7 +185,10 @@ export function useTemplateReviewActions({
// (reset + update separately could leave selections undefined in Strict Mode). // (reset + update separately could leave selections undefined in Strict Mode).
replaceState((prev) => { replaceState((prev) => {
const base = stripCustomRuleSelectionFields(prev); const base = stripCustomRuleSelectionFields(prev);
const customizePrefill = buildTemplateCustomizePrefill(doc); const customizePrefill = buildTemplateCustomizePrefill(
doc,
templateReviewSlug ?? undefined,
);
const hasValuesSeed = const hasValuesSeed =
customizePrefill.selectedCoreValueIds !== undefined; customizePrefill.selectedCoreValueIds !== undefined;
@@ -2,9 +2,11 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import Rule, { type Category } from "../../../../components/cards/Rule"; import Rule, { type Category } from "../../../../components/cards/Rule";
import Alert from "../../../../components/modals/Alert";
import { TemplateChipDetailModal } from "../../../../components/cards/TemplateReviewCard/TemplateChipDetailModal"; import { TemplateChipDetailModal } from "../../../../components/cards/TemplateReviewCard/TemplateChipDetailModal";
import { useMessages, useTranslation } from "../../../../contexts/MessagesContext"; import { useMessages, useTranslation } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlow } from "../../context/CreateFlowContext";
import type { CreateFlowState } from "../../types";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { import {
CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS, CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS,
@@ -39,6 +41,15 @@ import {
readDraftPreviewSurfaceColor, readDraftPreviewSurfaceColor,
} from "../../utils/resolvePreviewSurfaceFallback"; } from "../../utils/resolvePreviewSurfaceFallback";
function bindingArbitrationNeedsSeparateYes(state: CreateFlowState): boolean {
const membership = state.selectedMembershipMethodIds ?? [];
const conflict = state.selectedConflictManagementIds ?? [];
const joinsWithoutAYes =
membership.includes("open-admission") ||
membership.includes("membership-by-token");
return joinsWithoutAYes && conflict.includes("binding-arbitration");
}
const FACET_FALLBACK_ORDER: readonly TemplateFacetGroupKey[] = [ const FACET_FALLBACK_ORDER: readonly TemplateFacetGroupKey[] = [
"coreValues", "coreValues",
"communication", "communication",
@@ -284,6 +295,16 @@ export function FinalReviewScreen({
: t("description") : t("description")
} }
> >
{bindingArbitrationNeedsSeparateYes(state) ? (
<Alert
type="banner"
status="warning"
size="m"
title={t("bindingArbitrationWarning.title")}
description={t("bindingArbitrationWarning.description")}
hasTrailingIcon={false}
/>
) : null}
<Rule <Rule
title={ruleCardTitle} title={ruleCardTitle}
description={ruleCardDescription} description={ruleCardDescription}
@@ -9,10 +9,10 @@ import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell"; import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
function chipRowsFromLabels( function chipRowsFromLabels(
rows: readonly { label: string }[], rows: readonly { id?: string; label: string }[],
): ChipOption[] { ): ChipOption[] {
return rows.map((row, i) => ({ return rows.map((row, i) => ({
id: String(i + 1), id: row.id ?? String(i + 1),
label: row.label, label: row.label,
state: "unselected" as const, state: "unselected" as const,
})); }));
@@ -47,10 +47,10 @@ function createListCustomHandlers(
} }
function chipRowsFromLabels( function chipRowsFromLabels(
rows: readonly { label: string }[], rows: readonly { id?: string; label: string }[],
): ChipOption[] { ): ChipOption[] {
return rows.map((row, i) => ({ return rows.map((row, i) => ({
id: String(i + 1), id: row.id ?? String(i + 1),
label: row.label, label: row.label,
state: "unselected" as const, state: "unselected" as const,
})); }));
@@ -54,12 +54,16 @@ const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
*/ */
type ModalSession = "pending" | "customPending" | "editing"; type ModalSession = "pending" | "customPending" | "editing";
/** Row in `coreValues.json` `values` — string (legacy) or `{ label, meaning, signals }`. */ /** Row in `coreValues.json` `values`. */
type CoreValuePresetJson = type CoreValuePresetJson = {
| string id: string;
| { label: string; meaning?: string; signals?: string }; label: string;
meaning?: string;
signals?: string;
};
type CoreValuePreset = { type CoreValuePreset = {
id: string;
label: string; label: string;
meaning: string; meaning: string;
signals: string; signals: string;
@@ -68,16 +72,12 @@ type CoreValuePreset = {
function normalizeCoreValuePresets( function normalizeCoreValuePresets(
values: readonly CoreValuePresetJson[], values: readonly CoreValuePresetJson[],
): CoreValuePreset[] { ): CoreValuePreset[] {
return values.map((v) => { return values.map((v) => ({
if (typeof v === "string") { id: v.id,
return { label: v, meaning: "", signals: "" }; label: v.label,
} meaning: typeof v.meaning === "string" ? v.meaning : "",
return { signals: typeof v.signals === "string" ? v.signals : "",
label: v.label, }));
meaning: typeof v.meaning === "string" ? v.meaning : "",
signals: typeof v.signals === "string" ? v.signals : "",
};
});
} }
function selectedIdsFromOptions(options: ChipOption[]): string[] { function selectedIdsFromOptions(options: ChipOption[]): string[] {
@@ -98,7 +98,7 @@ function chipOptionsToSnapshotRows(
const EMPTY_DETAIL: CoreValueDetailEntry = { meaning: "", signals: "" }; const EMPTY_DETAIL: CoreValueDetailEntry = { meaning: "", signals: "" };
/** Create Custom — Core Values (Figma `20264:68378`). Up to five selections; preset list + custom chips. */ /** Create Custom — Core Values (Figma `20264:68378`). Up to six selections; preset list + custom chips. */
export function CoreValuesSelectScreen() { export function CoreValuesSelectScreen() {
const m = useMessages(); const m = useMessages();
const cv = m.create.customRule.coreValues; const cv = m.create.customRule.coreValues;
+12 -2
View File
@@ -129,8 +129,18 @@ export interface CreateFlowState {
selectedOrganizationTypeIds?: string[]; selectedOrganizationTypeIds?: string[];
/** Selected chip ids from `community-structure` (scale). */ /** Selected chip ids from `community-structure` (scale). */
selectedScaleIds?: string[]; selectedScaleIds?: string[];
/** Selected chip ids from `community-structure` (maturity). */ /** Selected chip ids from `community-structure` (maturity). One answer scores. */
selectedMaturityIds?: string[]; selectedMaturityIds?: string[];
/**
* Setup answers that add or open cards. The setup screen writes these.
* Stored with the draft and never published.
*/
setupTraits?: Partial<
Record<
"network" | "nonvoters" | "employer" | "sponsor" | "records" | "risky",
boolean
>
>;
/** /**
* Full chip lists for `community-structure` (needed so custom chips round-trip in drafts). * Full chip lists for `community-structure` (needed so custom chips round-trip in drafts).
* IDs alone are insufficient because custom rows are not reconstructible from copy JSON. * IDs alone are insufficient because custom rows are not reconstructible from copy JSON.
@@ -140,7 +150,7 @@ export interface CreateFlowState {
scale?: CommunityStructureChipSnapshotRow[]; scale?: CommunityStructureChipSnapshotRow[];
maturity?: CommunityStructureChipSnapshotRow[]; maturity?: CommunityStructureChipSnapshotRow[];
}; };
/** Create Custom — core values step (max five `selectedCoreValueIds`). */ /** Create Custom — core values step (max six `selectedCoreValueIds`). */
selectedCoreValueIds?: string[]; selectedCoreValueIds?: string[];
/** Full chip rows for core values (custom labels). */ /** Full chip rows for core values (custom labels). */
coreValuesChipsSnapshot?: CommunityStructureChipSnapshotRow[]; coreValuesChipsSnapshot?: CommunityStructureChipSnapshotRow[];
@@ -206,7 +206,13 @@ function resolveChipContent(
/> />
<ReadOnlyValueField <ReadOnlyValueField
label={da.sectionHeadings.consensusLevel} label={da.sectionHeadings.consensusLevel}
value={`${preset.sections.consensusLevel}%`} value={
preset.sections.consensusLevel === undefined
? da.consensusLevelDisplay.supermajorityShare
: preset.sections.consensusLevel === 0
? da.consensusLevelDisplay.noSetShare
: `${preset.sections.consensusLevel}%`
}
/> />
<ModalTextAreaField <ModalTextAreaField
label={da.sectionHeadings.objectionsDeadlocks} label={da.sectionHeadings.objectionsDeadlocks}
+1 -1
View File
@@ -17,7 +17,7 @@ const NOT_FOUND_TEMPLATE_SLUGS = [
"consensus", "consensus",
"do-ocracy", "do-ocracy",
"devolution", "devolution",
"quadratic-governance", "solidarity-network",
] as const; ] as const;
/** /**
+143
View File
@@ -0,0 +1,143 @@
{
"about": "Pairs whose text contradicts when both are picked. The product can warn and ask the group to edit one. Since 2 November 2026 cards state their own policy instead of naming a shared term, so a card that spells out a default can clash with the card a group picked for that job. Launch accepts these and lists each pair here for the warnings after launch (answer 69).",
"pairs": [
{
"deck": "membership",
"a": "equal-membership",
"b": "volunteers-and-core-members",
"why": "Equal membership gives every member the same rights and one kind of member. Volunteers and core members sets two kinds."
},
{
"deck": "membership",
"a": "equal-membership",
"b": "member-classes",
"why": "Equal membership has one kind of member. Member classes has several, each with its own seats."
},
{
"deck": "membership",
"a": "equal-membership",
"b": "contributors-and-maintainers",
"why": "Equal membership gives everyone the same rights. Maintainers hold rights contributors do not."
},
{
"deck": "membership",
"a": "equal-membership",
"b": "owners-and-staff",
"why": "Equal membership gives everyone the same say. Owners have the final say over staff."
},
{
"deck": "membership",
"a": "equal-membership",
"b": "membership-by-token",
"why": "Equal membership gives one vote each. Votes by token follow holdings."
},
{
"deck": "membership",
"a": "volunteers-and-core-members",
"b": "member-classes",
"why": "Both define kinds of members, in different ways."
},
{
"deck": "membership",
"a": "starting-with-one-person",
"b": "owners-and-staff",
"why": "Starting with one person gives the founder no extra say once others join. Owners and staff keeps the final say with the owners."
},
{
"deck": "membership",
"a": "members-approve-new-members",
"b": "worker-owner-candidacy",
"why": "Both decide who becomes a member. A worker co-op admits worker-owners through candidacy and their own vote."
},
{
"deck": "membership",
"a": "trial-period",
"b": "worker-owner-candidacy",
"why": "Both set a trial before full membership. Candidacy is a worker co-op's trial."
},
{
"deck": "conflictManagement",
"a": "conflict-ladder",
"b": "conflict-working-group",
"why": "Conflict ladder sends step three to two people close to neither person. Conflict working group names two to five members to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "conflict-ladder",
"b": "hearing-panel",
"why": "Conflict ladder sends step three to two people close to neither person. Hearing panel draws three panelists from an elected pool to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "conflict-ladder",
"b": "code-of-conduct-team",
"why": "Conflict ladder sends step three to two people close to neither person. Code of conduct team names two to seven people to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "conflict-ladder",
"b": "managerial-decision",
"why": "Conflict ladder sends step three to two people close to neither person. Managerial decision has the manager hear conflicts about the work of the people they manage."
},
{
"deck": "conflictManagement",
"a": "graduated-responses",
"b": "conflict-working-group",
"why": "Graduated responses has two people close to neither person propose a response. Conflict working group names two to five members to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "graduated-responses",
"b": "hearing-panel",
"why": "Graduated responses has two people close to neither person propose a response. Hearing panel draws three panelists from an elected pool to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "graduated-responses",
"b": "code-of-conduct-team",
"why": "Graduated responses has two people close to neither person propose a response. Code of conduct team names two to seven people to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "mediation",
"b": "conflict-working-group",
"why": "When mediation fails, Mediation sends a case to two people named each year to hear conflicts. Conflict working group names two to five members to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "mediation",
"b": "hearing-panel",
"why": "When mediation fails, Mediation sends a case to two people named each year to hear conflicts. Hearing panel draws three panelists from an elected pool to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "mediation",
"b": "code-of-conduct-team",
"why": "When mediation fails, Mediation sends a case to two people named each year to hear conflicts. Code of conduct team names two to seven people to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "mediation",
"b": "managerial-decision",
"why": "When mediation fails, Mediation sends a case to two people named each year to hear conflicts. Managerial decision has the manager hear conflicts about the work of the people they manage."
},
{
"deck": "conflictManagement",
"a": "precautionary-suspension",
"b": "conflict-working-group",
"why": "Precautionary suspension names two members each year who can pause someone. Conflict working group names two to five members to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "precautionary-suspension",
"b": "hearing-panel",
"why": "Precautionary suspension names two members each year who can pause someone. Hearing panel draws three panelists from an elected pool to hear the same cases."
},
{
"deck": "conflictManagement",
"a": "precautionary-suspension",
"b": "code-of-conduct-team",
"why": "Precautionary suspension names two members each year who can pause someone. Code of conduct team names two to seven people to hear the same cases."
}
]
}
+19 -57
View File
@@ -2,78 +2,40 @@
"size": { "size": {
"source": "messages/en/create/community/communitySize.json#/communitySizes", "source": "messages/en/create/community/communitySize.json#/communitySizes",
"values": { "values": {
"oneMember": { "oneMember": { "chipId": "oneMember" },
"chipId": "1" "twoToFive": { "chipId": "twoToFive" },
}, "sixToTwelve": { "chipId": "sixToTwelve" },
"twoToFive": { "thirteenToOneHundred": { "chipId": "thirteenToOneHundred" },
"chipId": "2" "oneHundredToOneHundredK": { "chipId": "oneHundredToOneHundredK" }
},
"sixToTwelve": {
"chipId": "3"
},
"thirteenToOneHundred": {
"chipId": "4"
},
"oneHundredToOneHundredK": {
"chipId": "5"
}
} }
}, },
"orgType": { "orgType": {
"source": "messages/en/create/community/communityStructure.json#/organizationTypes", "source": "messages/en/create/community/communityStructure.json#/organizationTypes",
"values": { "values": {
"workersCoop": { "workersCoop": { "chipId": "workersCoop" },
"chipId": "1" "mutualAid": { "chipId": "mutualAid" },
}, "openSource": { "chipId": "openSource" },
"mutualAid": { "nonprofit": { "chipId": "nonprofit" },
"chipId": "2" "forProfit": { "chipId": "forProfit" },
}, "dao": { "chipId": "dao" }
"openSource": {
"chipId": "3"
},
"nonprofit": {
"chipId": "4"
},
"forProfit": {
"chipId": "5"
},
"dao": {
"chipId": "6"
}
} }
}, },
"scale": { "scale": {
"source": "messages/en/create/community/communityStructure.json#/scaleOptions", "source": "messages/en/create/community/communityStructure.json#/scaleOptions",
"values": { "values": {
"local": { "local": { "chipId": "local" },
"chipId": "1" "regional": { "chipId": "regional" },
}, "national": { "chipId": "national" },
"regional": { "global": { "chipId": "global" }
"chipId": "2"
},
"national": {
"chipId": "3"
},
"global": {
"chipId": "4"
}
} }
}, },
"maturity": { "maturity": {
"source": "messages/en/create/community/communityStructure.json#/maturityOptions", "source": "messages/en/create/community/communityStructure.json#/maturityOptions",
"values": { "values": {
"earlyStage": { "earlyStage": { "chipId": "earlyStage" },
"chipId": "1" "growthStage": { "chipId": "growthStage" },
}, "established": { "chipId": "established" },
"growthStage": { "enterprise": { "chipId": "enterprise" }
"chipId": "2"
},
"established": {
"chipId": "3"
},
"enterprise": {
"chipId": "4"
}
} }
} }
} }
+209
View File
@@ -0,0 +1,209 @@
{
"about": "Old card id to new card id, for drafts and published Rules saved before the remake. A null target means the card was cut and its content moved to another deck or card listed in movedTo. movedTo names each card the content moved to by deck and id, so a loader can tell a group where it went. A value mapped to null was cut, as Holocracy was.",
"methods": {
"decisionApproaches": {
"algorithm-driven-decisions": "decide-by-formula",
"autocratic-decision-making": "one-person-decides",
"collaborative-platforms": {
"target": null,
"movedTo": [
{
"deck": "communication",
"id": "loomio"
}
],
"reason": "A tool, not a way of deciding. Any decision card can run on Loomio or a forum, and the tools keep their own cards in the communication deck, so a group picks a method here and a tool there."
},
"deliberative-polling": "panel-drawn-by-lot",
"executive-committees": "coordinating-committee",
"first-past-the-post": "majority-rule",
"holacracy": "consent-sociocracy",
"investor-filled-board-seats": "appointed-board",
"lottery-sortition": "panel-drawn-by-lot",
"proof-of-work": {
"target": null,
"movedTo": [
{
"deck": "decisionApproaches",
"id": "weighted-voting"
}
],
"reason": "How a blockchain orders transactions, not how members decide. Its one useful idea, say that follows recorded work, is now an option on Weighted voting, and its fork becomes the right to leave."
},
"range-voting": "score-voting",
"rotational-leadership": "rotating-roles",
"sociocracy": "consent-sociocracy"
},
"membership": {
"application-review": "application-and-review",
"collective-interviews": "members-approve-new-members",
"consensus-or-vote-based-approval": "members-approve-new-members",
"contribution-based": "join-by-contributing",
"hybrid-approval-process": {
"target": null,
"movedTo": [
{
"deck": "membership",
"id": "application-and-review"
}
],
"reason": "It described a stack of other cards rather than a practice, and the tool now combines cards by design. Its application, screening, trial and ratification steps come from picking Application and review, Trial period and Members approve new members together. The Federated Clusters template lists it and should list Member groups instead."
},
"identity-verification": "one-person-one-account",
"invitation-only": "vouching",
"lottery-sortition": "lottery-for-limited-places",
"membership-agreement-or-pledge": "membership-agreement",
"mentorship": "companions",
"open-access": "open-admission",
"orientation-required": "orientation",
"pay-to-join": "member-share-or-dues",
"peer-sponsorship": "vouching",
"referral-system-with-screening": "application-and-review",
"skill-based-contribution": "join-by-contributing",
"skill-based-evaluation": "requirements-for-some-roles",
"trial-period-provisional-membership": "trial-period",
"weighted-or-tiered-membership": "volunteers-and-core-members"
},
"conflictManagement": {
"ad-hoc-arbitration": "non-binding-arbitration",
"binding-contracts": {
"target": null,
"movedTo": [
{
"deck": "conflictManagement",
"id": "disputes-with-partners"
}
],
"note": "disputes-with-partners, which writes dispute steps into agreements with partners. Template Benevolent Dictator lists it.",
"reason": "A legal instrument, not a way of managing conflict, and it was checked for every kind of group, including one-member founders."
},
"conflict-resolution-council": "conflict-working-group",
"consensus-building": "mediation",
"facilitated-negotiation": "mediation",
"interest-based-bargaining": "mediation",
"internal-tribunal": "hearing-panel",
"judicial-committees": "hearing-panel",
"lottery-sortition": {
"target": null,
"movedTo": [
{
"deck": "decisionApproaches",
"id": "random-choice"
},
{
"deck": "conflictManagement",
"id": "hearing-panel"
}
],
"note": "random-choice (decision deck) for ties and small choices. hearing-panel for drawing panelists by lot. Template Sortition (Jury) lists it.",
"reason": "A way to decide, not a conflict practice, and it duplicates the decision deck's Random choice. Refusing the result was called bad faith."
},
"peer-mediation": "conflict-ladder",
"rotational-judging": "hearing-panel",
"supermajority-vote": {
"target": null,
"movedTo": [
{
"deck": "membership",
"id": "ending-a-membership"
},
{
"deck": "decisionApproaches",
"id": "supermajority-rule"
}
],
"note": "ending-a-membership (membership deck) for removal votes. supermajority-rule (decision deck) for policy deadlocks. Templates Quadratic Governance and Petition list it.",
"reason": "Two jobs on one card. Removing a member is the membership deck's removal job, and a vote to break a deadlock over policy is a decision, not a conflict practice. Its fixed 75 percent also broke the supermajority ruling."
}
},
"communication": {}
},
"coreValues": {
"about": "Today a core value chip id is its 1-based position in coreValues.json. oldPositionToNewId maps each old position to the new stable id. Ship stable ids (the id field) so this never happens again.",
"oldPositionToNewId": {
"1": "accessibility",
"2": "accountability",
"3": "adaptability",
"4": "agency",
"5": "generosity",
"6": "anti-oppression",
"7": "autonomy",
"8": "capacity-building",
"9": "cooperation",
"10": "common-ownership",
"11": "community-care",
"12": "managing-conflict",
"13": "consent",
"14": "consensus",
"15": "constructive-feedback",
"16": "cooperation",
"17": "copyleft",
"18": "decentralization",
"19": "direct-action",
"20": "diversity",
"21": "documentation",
"22": "education",
"23": "empathy",
"24": "agency",
"25": "equity",
"26": "innovation",
"27": "fairness",
"28": "fair-pay",
"29": "flexibility",
"30": "forkability",
"31": "freedom",
"32": "freedom-of-information",
"33": "generosity",
"34": "harm-reduction",
"35": "interdependence",
"36": null,
"37": "honesty",
"38": "horizontalism",
"39": "humility",
"40": "inclusion",
"41": "inclusion",
"42": "independence",
"43": "innovation",
"44": "integrity",
"45": "interdependence",
"46": "interoperability",
"47": "intersectionality",
"48": "justice",
"49": "documentation",
"50": "labor-rights",
"51": "leadership",
"52": "education",
"53": "freedom",
"54": "localism",
"55": "maintenance",
"56": "capacity-building",
"57": "contribution",
"58": "mutual-aid",
"59": "non-violence",
"60": "open-source",
"61": "openness",
"62": "participation",
"63": "reliability",
"64": "resilience",
"65": "solidarity",
"66": "stewardship",
"67": "transparency",
"68": "trust",
"69": "voluntarism"
},
"labelMerges": {
"Holocracy": null,
"Altruism": "Generosity",
"Collaboration": "Cooperation",
"Empowerment": "Agency",
"Experimentation": "Innovation",
"Holism": "Interdependence",
"Inclusivity": "Inclusion",
"Knowledge Sharing": "Documentation",
"Learning": "Education",
"Liberty": "Freedom",
"Mentorship": "Capacity building",
"Reciprocity": "Mutual aid"
}
}
}
+869
View File
@@ -0,0 +1,869 @@
{
"about": "The engine’s structure for each deck, for the rule agreed on 24 and 27 October. Jobs run in deck order. displayTier and rank set which recommended cards show first, and addedOrder ranks the cards setup answers add. Each card names its job, its priority inside that job, which is the row order that settles the last ties, whether it carries the Every Rule has label and its marks for the six setup answers. The rule that reads this file is in the package README and engine/recommend.ts.",
"version": "2026-11-02",
"displayLimit": 7,
"maxAddedInSeven": 2,
"tierOrder": [
"frame",
"safety",
"answers",
"fitted",
"floor"
],
"traitKeys": [
"network",
"nonvoters",
"employer",
"sponsor",
"records",
"risky"
],
"sizeAnswers": {
"oneMember": "oneMember",
"twoToFive": "twoToFive",
"sixToTwelve": "sixToTwelve",
"thirteenToOneHundred": "thirteenToOneHundred",
"oneHundredToOneHundredK": "oneHundredToOneHundredK"
},
"decks": {
"decisionApproaches": {
"jobs": [
{
"id": "everyday",
"label": "Everyday decisions",
"displayTier": "frame",
"rank": 1,
"why": "Most decisions are small and easy to undo, so every Rule needs to say who can act alone and when they must tell or ask first. Decision speeds also routes every decision to the group’s other cards."
},
{
"id": "big",
"label": "Big decisions",
"displayTier": "fitted",
"rank": 5,
"why": "Decisions that are hard to undo and touch everyone need one named method, which every other card calls a decision of the whole group."
},
{
"id": "rule-change",
"label": "Changing this Rule",
"displayTier": "floor",
"rank": 7,
"why": "A Rule that does not say how it changes either freezes or gets rewritten by whoever holds the pen."
},
{
"id": "emergency",
"label": "Emergencies",
"displayTier": "safety",
"rank": 2,
"why": "Every group meets a day with no time, so it needs a fast path that is reported at once and lapses unless the group confirms it."
},
{
"id": "money",
"label": "Money",
"displayTier": "floor",
"rank": 8,
"why": "Money and keys start much of the trouble, so a Rule must say where shared money is kept, who signs and who checks."
},
{
"id": "spending",
"label": "Spending",
"displayTier": "floor",
"rank": 9,
"why": "Most money trouble starts with who spent what, so a Rule must say what one member can spend alone, what needs a second member and what goes to the whole group."
},
{
"id": "roles",
"label": "Roles and delegation",
"displayTier": "fitted",
"rank": 6,
"why": "Roles are fine and bosses are not, so every role and working group needs a written mandate, a term, a report-back, a check and a recall."
},
{
"id": "other",
"label": "Other tools",
"displayTier": "none",
"why": "Voting systems, tie-breakers and methods for particular kinds of groups that a group adds when its main methods need help."
}
],
"addedOrder": [
"on-site-lead",
"how-we-are-covered",
"pay-and-hiring"
],
"cards": {
"decision-speeds": {
"job": "everyday",
"priority": 1,
"everyRuleHas": true
},
"lazy-consensus": {
"job": "big",
"priority": 1
},
"modified-consensus": {
"job": "big",
"priority": 2
},
"weighted-voting": {
"job": "big",
"priority": 3
},
"general-meeting": {
"job": "big",
"priority": 4
},
"hierarchical-decision-making": {
"job": "big",
"priority": 5
},
"one-person-decides": {
"job": "big",
"priority": 6
},
"two-person-sign-off": {
"job": "everyday",
"priority": 2
},
"shared-money": {
"job": "money",
"priority": 1,
"everyRuleHas": true
},
"spending-limits": {
"job": "spending",
"priority": 1
},
"emergency-decisions": {
"job": "emergency",
"priority": 1,
"everyRuleHas": true
},
"elected-board-of-directors": {
"job": "roles",
"priority": 1
},
"delegated-decision-making": {
"job": "roles",
"priority": 2
},
"changing-this-rule": {
"job": "rule-change",
"priority": 1,
"everyRuleHas": true
},
"do-ocracy": {
"job": "everyday",
"priority": 3
},
"advice-process": {
"job": "everyday",
"priority": 4
},
"consensus-decision-making": {
"job": "big",
"priority": 7
},
"consent-sociocracy": {
"job": "big",
"priority": 8
},
"rotating-roles": {
"job": "roles",
"priority": 3
},
"decisions-about-one-person": {
"job": "roles",
"priority": 4
},
"follow-through": {
"job": "roles",
"priority": 5
},
"majority-rule": {
"job": "big",
"priority": 9
},
"supermajority-rule": {
"job": "big",
"priority": 10
},
"coordinating-committee": {
"job": "roles",
"priority": 6
},
"appointed-board": {
"job": "roles",
"priority": 7
},
"consensus-seeking-with-delegates": {
"job": "big",
"priority": 11
},
"petition-and-referendum": {
"job": "rule-change",
"priority": 2
},
"annual-budget": {
"job": "money",
"priority": 2
},
"pay-and-hiring": {
"job": "money",
"priority": 3,
"traits": {
"beside": [
"employer"
],
"besideJob": "roles"
}
},
"on-site-lead": {
"job": "emergency",
"priority": 2,
"traits": {
"beside": [
"risky"
],
"besideJob": "emergency"
}
},
"conflicts-of-interest": {
"job": "roles",
"priority": 8
},
"closing-the-group": {
"job": "rule-change",
"priority": 3
},
"decide-by-formula": {
"job": "money",
"priority": 4
},
"liquid-democracy": {
"job": "other",
"priority": 1
},
"participatory-budgeting": {
"job": "money",
"priority": 5
},
"ranked-choice-voting": {
"job": "roles",
"priority": 9
},
"approval-voting": {
"job": "other",
"priority": 2
},
"random-choice": {
"job": "other",
"priority": 3
},
"negotiated-decisions": {
"job": "other",
"priority": 4
},
"advisory-committees": {
"job": "other",
"priority": 5
},
"panel-drawn-by-lot": {
"job": "roles",
"priority": 10
},
"score-voting": {
"job": "other",
"priority": 6
},
"cumulative-voting": {
"job": "other",
"priority": 7
},
"quadratic-voting": {
"job": "other",
"priority": 8
},
"continuous-voting": {
"job": "other",
"priority": 9
},
"formal-motions": {
"job": "other",
"priority": 10
},
"how-we-are-covered": {
"job": "money",
"priority": 6,
"traits": {
"beside": [
"sponsor"
],
"besideJob": "money"
}
}
}
},
"membership": {
"jobs": [
{
"id": "joining",
"label": "Joining",
"displayTier": "frame",
"rank": 1,
"why": "Every Rule must say who can become a member and how, so the group knows who counts as everyone when it decides."
},
{
"id": "levels",
"label": "Levels of membership",
"displayTier": "fitted",
"rank": 5,
"why": "What members can do at each stage and how people move between stages, which also sets who the active members are for every threshold."
},
{
"id": "onboarding",
"label": "Learning how we work",
"displayTier": "fitted",
"rank": 6,
"why": "How newcomers learn what we do and why, at a pace that lets the culture take in new people without drifting."
},
{
"id": "staying-and-leaving",
"label": "Staying and leaving",
"displayTier": "fitted",
"rank": 7,
"why": "Activity, rest, leave, stepping down, handing over and coming back, so the work never rests on whoever cannot say no."
},
{
"id": "removal",
"label": "Removal",
"displayTier": "safety",
"rank": 2,
"why": "How a membership ends against someone’s will, with the same steps whichever conflict cards lead there."
}
],
"addedOrder": [
"a-voice-for-people-without-a-vote",
"member-groups"
],
"cards": {
"open-admission": {
"job": "joining",
"priority": 1
},
"worker-owner-candidacy": {
"job": "joining",
"priority": 2
},
"join-by-contributing": {
"job": "joining",
"priority": 3
},
"membership-by-token": {
"job": "joining",
"priority": 4
},
"volunteers-and-core-members": {
"job": "levels",
"priority": 1,
"traits": {
"fits": [
"nonvoters"
]
}
},
"contributors-and-maintainers": {
"job": "levels",
"priority": 2,
"traits": {
"fits": [
"nonvoters"
]
}
},
"companions": {
"job": "onboarding",
"priority": 1
},
"joining-in-crews": {
"job": "onboarding",
"priority": 2
},
"orientation": {
"job": "onboarding",
"priority": 3
},
"rest-and-workload-limits": {
"job": "staying-and-leaving",
"priority": 1
},
"leave-and-return": {
"job": "staying-and-leaving",
"priority": 2
},
"leaving-the-group": {
"job": "staying-and-leaving",
"priority": 3
},
"handing-over-roles": {
"job": "staying-and-leaving",
"priority": 4
},
"ending-a-membership": {
"job": "removal",
"priority": 1,
"everyRuleHas": true
},
"equal-membership": {
"job": "levels",
"priority": 3
},
"starting-with-one-person": {
"job": "levels",
"priority": 4
},
"owners-and-staff": {
"job": "levels",
"priority": 5,
"traits": {
"fits": [
"nonvoters",
"employer"
]
}
},
"paid-staff": {
"job": "levels",
"priority": 6,
"traits": {
"fits": [
"nonvoters",
"employer"
]
}
},
"one-person-one-account": {
"job": "joining",
"priority": 5
},
"trial-period": {
"job": "levels",
"priority": 7
},
"members-approve-new-members": {
"job": "joining",
"priority": 6
},
"application-and-review": {
"job": "joining",
"priority": 7
},
"vouching": {
"job": "joining",
"priority": 8
},
"membership-agreement": {
"job": "joining",
"priority": 9
},
"member-share-or-dues": {
"job": "joining",
"priority": 10
},
"roster-upkeep": {
"job": "staying-and-leaving",
"priority": 5
},
"requirements-for-some-roles": {
"job": "levels",
"priority": 8
},
"member-groups": {
"job": "joining",
"priority": 11,
"traits": {
"beside": [
"network"
],
"besideJob": "levels"
}
},
"common-bond": {
"job": "joining",
"priority": 12
},
"lottery-for-limited-places": {
"job": "joining",
"priority": 13
},
"member-classes": {
"job": "levels",
"priority": 9
},
"a-voice-for-people-without-a-vote": {
"job": "levels",
"priority": 10,
"traits": {
"beside": [
"nonvoters"
],
"besideJob": "levels"
}
}
}
},
"conflictManagement": {
"jobs": [
{
"id": "ladder",
"label": "First steps",
"displayTier": "frame",
"rank": 1,
"why": "Most conflicts can be managed between the two people with a little help, so every Rule needs the order of the first steps and the sorting that sends safety and money cases straight to the last step and lets a power gap skip the one-on-one talk."
},
{
"id": "safety",
"label": "Safety first",
"displayTier": "safety",
"rank": 2,
"why": "When someone may be unsafe, the group acts at once and a fair process follows, so every Rule needs a pause that is not a finding."
},
{
"id": "formal",
"label": "Who hears serious cases",
"displayTier": "fitted",
"rank": 5,
"why": "When the first steps fail, someone named in advance has to hear the case, and the right body depends on the group’s size and kind, so a group picks one."
},
{
"id": "repair-and-removal",
"label": "Repair and removal",
"displayTier": "floor",
"rank": 7,
"why": "What can follow a finding, from repair to removal, in an order everyone knows in advance. Removal itself needs our supermajority share after the person has been heard."
},
{
"id": "other",
"label": "Outside help and legal duties",
"displayTier": "safety",
"rank": 3,
"why": "Who the group calls from outside, and the written procedures the law expects of groups that employ people or hold money. Refined from the brief’s other so groups can see what it holds."
},
{
"id": "legal",
"label": "Legal matters",
"displayTier": "floor",
"rank": 8,
"why": "Every group of two or more can be sued, subpoenaed, cited or handed legal papers, so every Rule says who receives them, who decides to go to court and how disputes that need a final answer are settled. Split from Outside help and legal duties on 21 Oct, because one card per job left Legal action out of every Rule."
},
{
"id": "police",
"label": "Police and arrests",
"displayTier": "safety",
"rank": 4,
"why": "Groups that work in public meet police, immigration officers and arrests, so their Rule says who speaks to officers, what never gets handed over and how we back a member who is held. Added on 21 Oct for groups that share food, give aid or protest in public. Groups that meet no officers, such as DAOs and open source projects, have no card here and skip it."
},
{
"id": "prevention",
"label": "Prevention",
"displayTier": "fitted",
"rank": 6,
"why": "Skills and habits that keep conflict small, early and cheap, and that fix the gaps conflicts show."
}
],
"addedOrder": [],
"cards": {
"conflict-ladder": {
"job": "ladder",
"priority": 1,
"everyRuleHas": true
},
"precautionary-suspension": {
"job": "safety",
"priority": 1,
"everyRuleHas": true
},
"graduated-responses": {
"job": "repair-and-removal",
"priority": 1,
"everyRuleHas": true
},
"conflict-working-group": {
"job": "formal",
"priority": 1
},
"mediation": {
"job": "formal",
"priority": 2
},
"harassment-and-discrimination-complaints": {
"job": "other",
"priority": 1,
"traits": {
"fits": [
"employer"
]
}
},
"code-of-conduct-team": {
"job": "formal",
"priority": 3
},
"hearing-panel": {
"job": "formal",
"priority": 4
},
"managerial-decision": {
"job": "formal",
"priority": 5
},
"outside-help": {
"job": "other",
"priority": 2
},
"conflict-workshops": {
"job": "prevention",
"priority": 2
},
"regular-debriefs": {
"job": "prevention",
"priority": 1
},
"safety-lead": {
"job": "safety",
"priority": 2
},
"legal-action": {
"job": "legal",
"priority": 1,
"everyRuleHas": true
},
"police-and-immigration-visits": {
"job": "police",
"priority": 1
},
"legal-support-for-members": {
"job": "police",
"priority": 2
},
"disputes-with-partners": {
"job": "legal",
"priority": 2
},
"working-with-children": {
"job": "safety",
"priority": 3
},
"restorative-practices": {
"job": "repair-and-removal",
"priority": 2
},
"accountability-process": {
"job": "repair-and-removal",
"priority": 3
},
"circle-processes": {
"job": "prevention",
"priority": 3
},
"whistleblower-protection": {
"job": "other",
"priority": 3
},
"non-binding-arbitration": {
"job": "legal",
"priority": 3
},
"binding-arbitration": {
"job": "legal",
"priority": 4
}
}
},
"communication": {
"jobs": [
{
"id": "chat",
"label": "Day-to-day chat",
"displayTier": "frame",
"rank": 1,
"why": "Most coordination happens in a chat app, so a Rule names one and the settings and norms that keep members safe in it."
},
{
"id": "records",
"label": "Our records",
"displayTier": "floor",
"rank": 7,
"why": "The one place for the Rule, decisions, minutes and how-tos that the shared term our records points to."
},
{
"id": "people",
"label": "Records about people",
"displayTier": "safety",
"rank": 2,
"why": "What we collect about members and the people we serve, who holds it and when it is deleted. Split from Our records, because for mutual aid groups and nonprofits a leak of these records is the most serious harm this deck can prevent, and one card per job left the card out of every Rule."
},
{
"id": "reach-everyone",
"label": "Reaching every member",
"displayTier": "floor",
"rank": 6,
"why": "Announcements and urgent news must reach every member, including those not on the app, because silence counts as consent in several of our methods."
},
{
"id": "conduct",
"label": "How we treat each other",
"displayTier": "safety",
"rank": 3,
"why": "One code for every channel, meeting and space, so tool cards keep only their own settings and norms."
},
{
"id": "access",
"label": "Accounts, keys and security",
"displayTier": "safety",
"rank": 4,
"why": "Money and keys start much of the trouble, so a Rule says who holds every account, login, domain and key and how we keep them and our devices safe."
},
{
"id": "meetings",
"label": "Meetings and discussion",
"displayTier": "fitted",
"rank": 5,
"why": "Where the group talks things through and decides together, in a room, on a call or in a written thread with a deadline. Refined to include writing, because spread-out groups and DAOs deliberate in forums and Loomio."
},
{
"id": "public",
"label": "Reaching the public",
"displayTier": "none",
"why": "New job. Front doors decay, so a group keeps one way in that outlives its founders, speaks through named members and keeps members’ own names and numbers private. Facebook groups, newsletters and the press had nowhere else to go."
}
],
"addedOrder": [],
"cards": {
"how-we-run-meetings": {
"job": "meetings",
"priority": 1
},
"announcements-channel": {
"job": "reach-everyone",
"priority": 1,
"everyRuleHas": true
},
"discourse-forum": {
"job": "meetings",
"priority": 2
},
"signal": {
"job": "chat",
"priority": 1
},
"slack": {
"job": "chat",
"priority": 2
},
"matrix-element": {
"job": "chat",
"priority": 3
},
"discord": {
"job": "chat",
"priority": 4
},
"shared-records": {
"job": "records",
"priority": 1,
"everyRuleHas": true
},
"code-of-conduct": {
"job": "conduct",
"priority": 1,
"everyRuleHas": true
},
"records-about-people": {
"job": "people",
"priority": 1,
"traits": {
"opens": [
"records"
]
}
},
"accounts-and-keys": {
"job": "access",
"priority": 1,
"everyRuleHas": true
},
"email-distribution-list": {
"job": "reach-everyone",
"priority": 2
},
"github-gitlab": {
"job": "records",
"priority": 2
},
"front-door": {
"job": "public",
"priority": 1
},
"in-person-meetings": {
"job": "meetings",
"priority": 3
},
"video-meetings": {
"job": "meetings",
"priority": 4
},
"phone-and-text-tree": {
"job": "reach-everyone",
"priority": 3
},
"deciding-in-threads": {
"job": "meetings",
"priority": 5
},
"loomio": {
"job": "meetings",
"priority": 6
},
"whatsapp": {
"job": "chat",
"priority": 5
},
"chat-channels": {
"job": "chat",
"priority": 6
},
"field-communication": {
"job": "chat",
"priority": 7
},
"password-manager": {
"job": "access",
"priority": 2
},
"security-plan": {
"job": "access",
"priority": 3
},
"speaking-for-the-group": {
"job": "public",
"priority": 2
},
"newsletter": {
"job": "public",
"priority": 3
},
"facebook-and-instagram": {
"job": "public",
"priority": 4
},
"telegram": {
"job": "chat",
"priority": 8
},
"zulip": {
"job": "chat",
"priority": 9
}
}
}
}
}
@@ -0,0 +1,21 @@
{
"about": "Proposed and not applied. Mark changes the engine run suggested for the marks sign-off. M2 left this file on 2 November 2026. It was decided on 30 October (D7) and is now in the conflict facets and _jobs.json, with Regular debriefs first in the prevention job. M1 stays here unapplied, since it is not applied at launch (D6, 30 October). engine/results/worked-examples.md shows the picks with and without it.",
"fixes": [
{
"id": "M1",
"deck": "membership",
"card": "owners-and-staff",
"set": {
"orgType": {
"workersCoop": true
}
},
"traits": {
"fits": [
"nonvoters"
]
},
"why": "The card says worker-owners are the owners in a worker co-op, but only for-profits are marked, so under one main kind no co-op gets it. The review's worked examples gave it to co-ops whose workers do not all own. This mark does that only if the pay answer also comes off the card, because its help line tells co-ops to tick it for worker-owners on payroll, so this revisits one cell of the 24 Oct table. It then moves 40 of the 47 co-ops in the profiles, 26 because some who work there do not vote and 14 only because they also ticked for-profit, which breaks the tie. Without it, co-ops keep Equal membership and get A voice for people without a vote beside it when some who work there do not vote."
}
]
}
+632 -110
View File
@@ -1,5 +1,5 @@
{ {
"discord": { "accounts-and-keys": {
"size": { "size": {
"oneMember": true, "oneMember": true,
"twoToFive": true, "twoToFive": true,
@@ -9,7 +9,36 @@
}, },
"orgType": { "orgType": {
"dao": true, "dao": true,
"forProfit": false, "forProfit": true,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"announcements-channel": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": true, "nonprofit": true,
"openSource": true, "openSource": true,
"mutualAid": true, "mutualAid": true,
@@ -25,10 +54,39 @@
"earlyStage": true, "earlyStage": true,
"growthStage": true, "growthStage": true,
"established": true, "established": true,
"enterprise": false "enterprise": true
} }
}, },
"discourse-forum": { "chat-channels": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": false,
"openSource": false,
"mutualAid": true,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"code-of-conduct": {
"size": { "size": {
"oneMember": true, "oneMember": true,
"twoToFive": true, "twoToFive": true,
@@ -57,7 +115,181 @@
"enterprise": true "enterprise": true
} }
}, },
"deciding-in-threads": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": false,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"discord": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": false,
"nonprofit": false,
"openSource": false,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"discourse-forum": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": false,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": false,
"nonprofit": false,
"openSource": false,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"email-distribution-list": { "email-distribution-list": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"facebook-and-instagram": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": false,
"openSource": false,
"mutualAid": true,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"field-communication": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": false,
"openSource": false,
"mutualAid": true,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"front-door": {
"size": { "size": {
"oneMember": true, "oneMember": true,
"twoToFive": true, "twoToFive": true,
@@ -95,11 +327,40 @@
"oneHundredToOneHundredK": true "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": true, "dao": false,
"forProfit": true, "forProfit": false,
"nonprofit": false, "nonprofit": false,
"openSource": true, "openSource": true,
"mutualAid": false, "mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"how-we-run-meetings": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true "workersCoop": true
}, },
"scale": { "scale": {
@@ -121,13 +382,13 @@
"twoToFive": true, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": false "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": true, "dao": false,
"forProfit": true, "forProfit": false,
"nonprofit": true, "nonprofit": true,
"openSource": true, "openSource": false,
"mutualAid": true, "mutualAid": true,
"workersCoop": true "workersCoop": true
}, },
@@ -141,23 +402,23 @@
"earlyStage": true, "earlyStage": true,
"growthStage": true, "growthStage": true,
"established": true, "established": true,
"enterprise": false "enterprise": true
} }
}, },
"loomio": { "loomio": {
"size": { "size": {
"oneMember": false, "oneMember": false,
"twoToFive": true, "twoToFive": false,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": true "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": true, "dao": false,
"forProfit": true, "forProfit": false,
"nonprofit": true, "nonprofit": true,
"openSource": true, "openSource": false,
"mutualAid": true, "mutualAid": false,
"workersCoop": true "workersCoop": true
}, },
"scale": { "scale": {
@@ -174,6 +435,35 @@
} }
}, },
"matrix-element": { "matrix-element": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": false,
"openSource": true,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"newsletter": {
"size": { "size": {
"oneMember": true, "oneMember": true,
"twoToFive": true, "twoToFive": true,
@@ -181,6 +471,296 @@
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": true "oneHundredToOneHundredK": true
}, },
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"password-manager": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": true,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"phone-and-text-tree": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": false
},
"scale": {
"global": false,
"national": false,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"records-about-people": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"security-plan": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"shared-records": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"signal": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": false,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"slack": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": true,
"nonprofit": true,
"openSource": false,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"speaking-for-the-group": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"telegram": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": false,
"nonprofit": false,
"openSource": false,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"video-meetings": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": { "orgType": {
"dao": true, "dao": true,
"forProfit": false, "forProfit": false,
@@ -202,120 +782,62 @@
"enterprise": true "enterprise": true
} }
}, },
"signal": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": false
}
},
"slack": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": false,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"video-meetings": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"whatsapp": { "whatsapp": {
"size": { "size": {
"oneMember": true, "oneMember": false,
"twoToFive": true, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": false "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": false, "dao": false,
"forProfit": false, "forProfit": false,
"nonprofit": true, "nonprofit": false,
"openSource": false, "openSource": false,
"mutualAid": true, "mutualAid": true,
"workersCoop": true "workersCoop": true
}, },
"scale": { "scale": {
"global": false, "global": true,
"national": false, "national": true,
"regional": true, "regional": true,
"local": true "local": true
}, },
"maturity": { "maturity": {
"earlyStage": true, "earlyStage": true,
"growthStage": true, "growthStage": true,
"established": false, "established": true,
"enterprise": false "enterprise": true
}
},
"zulip": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": false,
"openSource": true,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
} }
} }
} }
+389 -244
View File
@@ -1,48 +1,19 @@
{ {
"ad-hoc-arbitration": { "accountability-process": {
"size": { "size": {
"oneMember": false, "oneMember": false,
"twoToFive": true, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": false
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": false,
"enterprise": false
}
},
"binding-arbitration": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": false, "dao": false,
"forProfit": true, "forProfit": false,
"nonprofit": true, "nonprofit": false,
"openSource": false, "openSource": false,
"mutualAid": false, "mutualAid": true,
"workersCoop": true "workersCoop": false
}, },
"scale": { "scale": {
"global": true, "global": true,
@@ -57,21 +28,21 @@
"enterprise": true "enterprise": true
} }
}, },
"binding-contracts": { "binding-arbitration": {
"size": { "size": {
"oneMember": true, "oneMember": false,
"twoToFive": true, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": true "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": true, "dao": false,
"forProfit": true, "forProfit": false,
"nonprofit": true, "nonprofit": false,
"openSource": true, "openSource": false,
"mutualAid": true, "mutualAid": false,
"workersCoop": true "workersCoop": false
}, },
"scale": { "scale": {
"global": true, "global": true,
@@ -100,7 +71,36 @@
"nonprofit": true, "nonprofit": true,
"openSource": false, "openSource": false,
"mutualAid": true, "mutualAid": true,
"workersCoop": true "workersCoop": false
},
"scale": {
"global": false,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"code-of-conduct-team": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": false,
"nonprofit": false,
"openSource": true,
"mutualAid": false,
"workersCoop": false
}, },
"scale": { "scale": {
"global": true, "global": true,
@@ -111,42 +111,13 @@
"maturity": { "maturity": {
"earlyStage": true, "earlyStage": true,
"growthStage": true, "growthStage": true,
"established": false,
"enterprise": false
}
},
"conflict-resolution-council": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": false,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": false,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": false,
"growthStage": true,
"established": true, "established": true,
"enterprise": true "enterprise": true
} }
}, },
"conflict-workshops": { "conflict-ladder": {
"size": { "size": {
"oneMember": false, "oneMember": true,
"twoToFive": true, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
@@ -173,16 +144,16 @@
"enterprise": true "enterprise": true
} }
}, },
"consensus-building": { "conflict-working-group": {
"size": { "size": {
"oneMember": false, "oneMember": false,
"twoToFive": true, "twoToFive": false,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": false "oneHundredToOneHundredK": false
}, },
"orgType": { "orgType": {
"dao": true, "dao": false,
"forProfit": false, "forProfit": false,
"nonprofit": true, "nonprofit": true,
"openSource": false, "openSource": false,
@@ -199,10 +170,10 @@
"earlyStage": true, "earlyStage": true,
"growthStage": true, "growthStage": true,
"established": true, "established": true,
"enterprise": false "enterprise": true
} }
}, },
"facilitated-negotiation": { "conflict-workshops": {
"size": { "size": {
"oneMember": false, "oneMember": false,
"twoToFive": true, "twoToFive": true,
@@ -211,126 +182,10 @@
"oneHundredToOneHundredK": true "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": true, "dao": false,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"interest-based-bargaining": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"internal-tribunal": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": false,
"thirteenToOneHundred": false,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": false,
"openSource": true,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": false,
"growthStage": false,
"established": true,
"enterprise": true
}
},
"judicial-committees": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": false,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": false,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": false,
"growthStage": false,
"established": true,
"enterprise": true
}
},
"lottery-sortition": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": false, "forProfit": false,
"nonprofit": false, "nonprofit": true,
"openSource": true, "openSource": false,
"mutualAid": true, "mutualAid": true,
"workersCoop": true "workersCoop": true
}, },
@@ -347,7 +202,7 @@
"enterprise": true "enterprise": true
} }
}, },
"managerial-decision": { "disputes-with-partners": {
"size": { "size": {
"oneMember": true, "oneMember": true,
"twoToFive": true, "twoToFive": true,
@@ -376,9 +231,9 @@
"enterprise": true "enterprise": true
} }
}, },
"mediation": { "graduated-responses": {
"size": { "size": {
"oneMember": false, "oneMember": true,
"twoToFive": true, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
@@ -405,9 +260,9 @@
"enterprise": true "enterprise": true
} }
}, },
"non-binding-arbitration": { "harassment-and-discrimination-complaints": {
"size": { "size": {
"oneMember": false, "oneMember": true,
"twoToFive": true, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
@@ -434,16 +289,16 @@
"enterprise": true "enterprise": true
} }
}, },
"peer-mediation": { "hearing-panel": {
"size": { "size": {
"oneMember": false, "oneMember": false,
"twoToFive": false, "twoToFive": false,
"sixToTwelve": true, "sixToTwelve": false,
"thirteenToOneHundred": true, "thirteenToOneHundred": false,
"oneHundredToOneHundredK": false "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": true, "dao": false,
"forProfit": false, "forProfit": false,
"nonprofit": true, "nonprofit": true,
"openSource": false, "openSource": false,
@@ -463,48 +318,280 @@
"enterprise": true "enterprise": true
} }
}, },
"legal-action": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"legal-support-for-members": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": false
},
"scale": {
"global": false,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"managerial-decision": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": true,
"nonprofit": false,
"openSource": false,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"mediation": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": false,
"thirteenToOneHundred": false,
"oneHundredToOneHundredK": false
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"non-binding-arbitration": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": false,
"openSource": true,
"mutualAid": false,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"outside-help": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"police-and-immigration-visits": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": true,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": false,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"precautionary-suspension": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"regular-debriefs": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": true,
"nonprofit": true,
"openSource": true,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"restorative-practices": { "restorative-practices": {
"size": { "size": {
"oneMember": false, "oneMember": false,
"twoToFive": true, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": false "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": false, "dao": false,
"forProfit": false, "forProfit": false,
"nonprofit": true, "nonprofit": true,
"openSource": true, "openSource": false,
"mutualAid": true,
"workersCoop": true
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": false
}
},
"rotational-judging": {
"size": {
"oneMember": false,
"twoToFive": false,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": true,
"forProfit": false,
"nonprofit": false,
"openSource": true,
"mutualAid": true, "mutualAid": true,
"workersCoop": true "workersCoop": true
}, },
@@ -521,21 +608,79 @@
"enterprise": true "enterprise": true
} }
}, },
"supermajority-vote": { "safety-lead": {
"size": { "size": {
"oneMember": false, "oneMember": false,
"twoToFive": false, "twoToFive": true,
"sixToTwelve": true, "sixToTwelve": true,
"thirteenToOneHundred": true, "thirteenToOneHundred": true,
"oneHundredToOneHundredK": true "oneHundredToOneHundredK": true
}, },
"orgType": { "orgType": {
"dao": true, "dao": false,
"forProfit": false, "forProfit": false,
"nonprofit": true, "nonprofit": true,
"openSource": true, "openSource": false,
"mutualAid": true, "mutualAid": true,
"workersCoop": true "workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"whistleblower-protection": {
"size": {
"oneMember": true,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": false,
"workersCoop": false
},
"scale": {
"global": true,
"national": true,
"regional": true,
"local": true
},
"maturity": {
"earlyStage": true,
"growthStage": true,
"established": true,
"enterprise": true
}
},
"working-with-children": {
"size": {
"oneMember": false,
"twoToFive": true,
"sixToTwelve": true,
"thirteenToOneHundred": true,
"oneHundredToOneHundredK": true
},
"orgType": {
"dao": false,
"forProfit": false,
"nonprofit": true,
"openSource": false,
"mutualAid": true,
"workersCoop": false
}, },
"scale": { "scale": {
"global": true, "global": true,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,8 @@
[
{"id": "founder-1", "size": "1", "kinds": ["worker co-op"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "founder-2", "size": "1", "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "founder-3", "size": "1", "kinds": ["nonprofit"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": false, "risky": false}},
{"id": "founder-4", "size": "1", "kinds": ["for-profit"], "scale": "regional", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "founder-5", "size": "1", "kinds": ["open source"], "scale": "global", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "founder-6", "size": "1", "kinds": ["dao"], "scale": "global", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}}
]
@@ -0,0 +1,94 @@
[
{"id": "aid-01", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-02", "size": "6-12", "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": true, "employer": false, "sponsor": false, "records": true, "risky": true}, "alsoPlausible": {"size": ["13-100"]}},
{"id": "aid-03", "size": "6-12", "kinds": ["mutual aid"], "scale": "regional", "stage": "early", "answers": {"network": true, "nonvoters": true, "employer": false, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"size": ["13-100"]}},
{"id": "aid-04", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": false, "risky": false}},
{"id": "aid-05", "size": null, "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": false, "risky": true}},
{"id": "aid-06", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}, "alsoPlausible": {"kinds": ["nonprofit"]}},
{"id": "aid-07", "size": "6-12", "kinds": ["nonprofit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": true, "risky": false}, "alsoPlausible": {"scale": ["regional"]}},
{"id": "aid-08", "size": "13-100", "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "established", "answers": {"network": true, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-09", "size": "6-12", "kinds": ["nonprofit"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"stage": ["growth"]}},
{"id": "aid-10", "size": null, "kinds": [], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-11", "size": "100+", "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-12", "size": "2-5", "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": true, "risky": false}},
{"id": "aid-13", "size": null, "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": true, "risky": false}},
{"id": "aid-14", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": true, "risky": true}},
{"id": "aid-15", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": false, "risky": true}, "alsoPlausible": {"kinds": ["nonprofit"]}},
{"id": "aid-16", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "growth", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-17", "size": "6-12", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-18", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-19", "size": "2-5", "kinds": ["mutual aid"], "scale": "local", "stage": "growth", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-20", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-21", "size": null, "kinds": ["mutual aid"], "scale": "local", "stage": "growth", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": true, "risky": false}, "alsoPlausible": {"scale": ["regional"]}},
{"id": "aid-22", "size": "13-100", "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-23", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": false, "risky": true}, "alsoPlausible": {"kinds": ["nonprofit"]}},
{"id": "aid-24", "size": "13-100", "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": true, "risky": false}},
{"id": "aid-25", "size": "6-12", "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "growth", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}, "alsoPlausible": {"size": ["13-100"]}},
{"id": "aid-26", "size": "6-12", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-27", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": true, "risky": false}, "alsoPlausible": {"kinds": ["nonprofit"]}},
{"id": "aid-28", "size": "100+", "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": true, "risky": false}},
{"id": "aid-29", "size": "100+", "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-30", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-31", "size": "13-100", "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-32", "size": null, "kinds": ["mutual aid"], "scale": "local", "stage": null, "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-33", "size": "100+", "kinds": ["mutual aid", "nonprofit"], "scale": "regional", "stage": "established", "answers": {"network": true, "nonvoters": true, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-34", "size": "2-5", "kinds": ["mutual aid", "nonprofit"], "scale": "regional", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": true, "records": true, "risky": false}, "alsoPlausible": {"size": ["6-12"]}},
{"id": "aid-35", "size": null, "kinds": ["mutual aid", "nonprofit"], "scale": "regional", "stage": "growth", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "aid-36", "size": "2-5", "kinds": ["mutual aid", "nonprofit"], "scale": "regional", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": true, "records": true, "risky": false}, "alsoPlausible": {"size": ["6-12"]}},
{"id": "aid-37", "size": "6-12", "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": true, "records": false, "risky": false}},
{"id": "aid-38", "size": "100+", "kinds": ["nonprofit"], "scale": "local", "stage": "early", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": true, "records": false, "risky": false}},
{"id": "aid-39", "size": "13-100", "kinds": ["nonprofit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": true, "risky": true}},
{"id": "aid-40", "size": "13-100", "kinds": ["mutual aid", "nonprofit"], "scale": "local", "stage": "established", "answers": {"network": true, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "aid-41", "size": "13-100", "kinds": ["mutual aid"], "scale": "regional", "stage": "early", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": false, "records": true, "risky": false}},
{"id": "aid-42", "size": "2-5", "kinds": ["nonprofit"], "scale": "regional", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": true, "risky": false}},
{"id": "aid-43", "size": "100+", "kinds": ["mutual aid"], "scale": "local", "stage": "early", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-01", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}, "alsoPlausible": {"size": ["100+"], "scale": ["regional"]}},
{"id": "coop-02", "size": "13-100", "kinds": ["for-profit"], "scale": "national", "stage": "established", "answers": {"network": true, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-03", "size": "13-100", "kinds": ["for-profit"], "scale": "national", "stage": "established", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-04", "size": "6-12", "kinds": ["worker co-op", "for-profit", "open source"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-05", "size": "100+", "kinds": ["worker co-op", "for-profit"], "scale": "regional", "stage": "growth", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": true, "records": true, "risky": true}},
{"id": "coop-06", "size": "100+", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-07", "size": "100+", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-08", "size": "6-12", "kinds": ["worker co-op", "for-profit"], "scale": "regional", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"size": ["13-100"]}},
{"id": "coop-09", "size": "2-5", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-10", "size": "6-12", "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "growth", "answers": {"network": false, "nonvoters": true, "employer": false, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"size": ["13-100"]}},
{"id": "coop-11", "size": "2-5", "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"size": ["13-100"]}},
{"id": "coop-12", "size": null, "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "growth", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"scale": ["regional"]}},
{"id": "coop-13", "size": "6-12", "kinds": ["worker co-op", "for-profit"], "scale": "regional", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}, "alsoPlausible": {"stage": ["early"]}},
{"id": "coop-14", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "regional", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-15", "size": null, "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-16", "size": "6-12", "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-17", "size": null, "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-18", "size": "2-5", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "early", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": true, "records": false, "risky": true}},
{"id": "coop-19", "size": "2-5", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}, "alsoPlausible": {"kinds": ["mutual aid"]}},
{"id": "coop-20", "size": null, "kinds": ["worker co-op"], "scale": "regional", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": true, "records": false, "risky": true}},
{"id": "coop-21", "size": "6-12", "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-22", "size": null, "kinds": ["worker co-op", "for-profit"], "scale": null, "stage": null, "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"kinds": ["nonprofit"]}},
{"id": "coop-23", "size": "2-5", "kinds": ["worker co-op"], "scale": "local", "stage": null, "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-24", "size": null, "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-25", "size": "6-12", "kinds": ["worker co-op"], "scale": "local", "stage": "early", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-26", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-27", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-28", "size": "13-100", "kinds": ["worker co-op"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-29", "size": "6-12", "kinds": ["worker co-op"], "scale": "regional", "stage": "established", "answers": {"network": true, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}, "alsoPlausible": {"size": ["100+"]}},
{"id": "coop-30", "size": "100+", "kinds": ["worker co-op"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-31", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"size": ["100+"]}},
{"id": "coop-32", "size": "100+", "kinds": ["worker co-op"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-33", "size": "100+", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "enterprise", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-34", "size": "100+", "kinds": ["worker co-op"], "scale": "global", "stage": "enterprise", "answers": {"network": true, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-35", "size": "100+", "kinds": ["worker co-op", "for-profit"], "scale": "global", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-36", "size": "100+", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "growth", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-37", "size": "6-12", "kinds": ["worker co-op"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-38", "size": "6-12", "kinds": ["worker co-op", "open source"], "scale": "global", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-39", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-40", "size": "100+", "kinds": ["worker co-op"], "scale": "regional", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-41", "size": "100+", "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-42", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": true, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-43", "size": "13-100", "kinds": ["worker co-op"], "scale": "regional", "stage": "established", "answers": {"network": true, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}, "alsoPlausible": {"size": ["2-5", "6-12"]}},
{"id": "coop-44", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "global", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-45", "size": "13-100", "kinds": ["worker co-op"], "scale": "local", "stage": "established", "answers": {"network": true, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": false}, "alsoPlausible": {"size": ["100+"]}},
{"id": "coop-46", "size": "2-5", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": true, "employer": false, "sponsor": false, "records": false, "risky": false}},
{"id": "coop-47", "size": "13-100", "kinds": ["worker co-op"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": false, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-48", "size": "13-100", "kinds": ["worker co-op", "for-profit"], "scale": "national", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": true}},
{"id": "coop-49", "size": "6-12", "kinds": ["worker co-op", "for-profit"], "scale": "local", "stage": "established", "answers": {"network": false, "nonvoters": false, "employer": true, "sponsor": false, "records": false, "risky": false}}
]
+319
View File
@@ -0,0 +1,319 @@
{
"about": "Each template's chips as card and value ids, made from COMPOSITION_BY_SLUG in repo/prisma/seed.ts, the chips agreed on 27 October. Joining by id means a relabel never breaks a template. Today the seed joins by title through methodSlugFromTitle.",
"templates": {
"benevolent-dictator": {
"decisionApproaches": [
"one-person-decides",
"hierarchical-decision-making"
],
"membership": [
"join-by-contributing",
"contributors-and-maintainers"
],
"conflictManagement": [
"conflict-ladder",
"managerial-decision"
],
"communication": [
"email-distribution-list",
"github-gitlab"
],
"coreValues": [
"reliability",
"stewardship",
"leadership"
]
},
"consensus": {
"decisionApproaches": [
"decision-speeds",
"consensus-decision-making",
"modified-consensus"
],
"membership": [
"members-approve-new-members",
"vouching"
],
"conflictManagement": [
"conflict-ladder",
"mediation"
],
"communication": [
"how-we-run-meetings",
"in-person-meetings",
"loomio"
],
"coreValues": [
"consensus",
"community-care",
"horizontalism"
]
},
"consensus-clusters": {
"decisionApproaches": [
"consent-sociocracy",
"delegated-decision-making"
],
"membership": [
"join-by-contributing",
"orientation"
],
"conflictManagement": [
"conflict-ladder",
"circle-processes",
"restorative-practices"
],
"communication": [
"how-we-run-meetings",
"shared-records",
"slack"
],
"coreValues": [
"decentralization",
"adaptability",
"autonomy"
]
},
"devolution": {
"decisionApproaches": [
"one-person-decides",
"delegated-decision-making"
],
"membership": [
"starting-with-one-person",
"open-admission"
],
"conflictManagement": [
"conflict-ladder",
"conflict-workshops",
"managerial-decision"
],
"communication": [
"discord",
"github-gitlab"
],
"coreValues": [
"capacity-building",
"education",
"maintenance"
]
},
"do-ocracy": {
"decisionApproaches": [
"do-ocracy",
"lazy-consensus",
"two-person-sign-off"
],
"membership": [
"join-by-contributing",
"rest-and-workload-limits"
],
"conflictManagement": [
"conflict-ladder",
"mediation"
],
"communication": [
"github-gitlab",
"discord"
],
"coreValues": [
"agency",
"autonomy",
"voluntarism"
]
},
"elected-board": {
"decisionApproaches": [
"elected-board-of-directors",
"general-meeting",
"majority-rule"
],
"membership": [
"application-and-review",
"membership-agreement"
],
"conflictManagement": [
"conflict-ladder",
"mediation",
"conflict-working-group"
],
"communication": [
"email-distribution-list",
"slack"
],
"coreValues": [
"accountability",
"transparency",
"trust"
]
},
"federated-clusters": {
"decisionApproaches": [
"consensus-seeking-with-delegates"
],
"membership": [
"member-groups",
"membership-agreement"
],
"conflictManagement": [
"conflict-ladder",
"hearing-panel",
"non-binding-arbitration"
],
"communication": [
"matrix-element",
"discourse-forum"
],
"coreValues": [
"interoperability",
"localism",
"interdependence"
]
},
"liquid-democracy": {
"decisionApproaches": [
"liquid-democracy",
"continuous-voting"
],
"membership": [
"open-admission",
"one-person-one-account"
],
"conflictManagement": [
"conflict-ladder",
"non-binding-arbitration"
],
"communication": [
"loomio",
"discourse-forum"
],
"coreValues": [
"agency",
"flexibility",
"transparency"
]
},
"petition": {
"decisionApproaches": [
"petition-and-referendum",
"majority-rule",
"ranked-choice-voting"
],
"membership": [
"open-admission",
"one-person-one-account"
],
"conflictManagement": [
"conflict-ladder",
"non-binding-arbitration",
"legal-action"
],
"communication": [
"loomio",
"discourse-forum"
],
"coreValues": [
"freedom-of-information",
"accountability",
"participation"
]
},
"quadratic-governance": {
"decisionApproaches": [
"quadratic-voting",
"two-person-sign-off"
],
"membership": [
"one-person-one-account",
"member-share-or-dues"
],
"conflictManagement": [
"conflict-ladder",
"conflict-working-group"
],
"communication": [
"discourse-forum",
"discord"
],
"coreValues": [
"fairness",
"innovation",
"independence"
]
},
"self-appointed-board": {
"decisionApproaches": [
"appointed-board",
"advisory-committees",
"coordinating-committee"
],
"membership": [
"application-and-review",
"paid-staff"
],
"conflictManagement": [
"conflict-ladder",
"harassment-and-discrimination-complaints",
"hearing-panel"
],
"communication": [
"video-meetings",
"email-distribution-list"
],
"coreValues": [
"stewardship",
"resilience",
"reliability"
]
},
"solidarity-network": {
"decisionApproaches": [
"do-ocracy",
"modified-consensus",
"shared-money"
],
"membership": [
"open-admission",
"vouching",
"member-groups"
],
"conflictManagement": [
"conflict-ladder",
"restorative-practices"
],
"communication": [
"signal",
"matrix-element"
],
"coreValues": [
"solidarity",
"mutual-aid",
"anti-oppression"
]
},
"sortition-jury": {
"decisionApproaches": [
"panel-drawn-by-lot",
"rotating-roles"
],
"membership": [
"open-admission",
"roster-upkeep"
],
"conflictManagement": [
"conflict-ladder",
"hearing-panel"
],
"communication": [
"in-person-meetings",
"video-meetings",
"shared-records"
],
"coreValues": [
"fairness",
"equity",
"transparency"
]
}
}
}
+39 -5
View File
@@ -3,8 +3,10 @@ import type {
CreateFlowState, CreateFlowState,
} from "../../app/(app)/create/types"; } from "../../app/(app)/create/types";
import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json"; import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json";
import templateChipsFile from "../../data/templates/templateChips.json";
import { assignTemplateMethodSlugsToPrefill } from "./customRuleFacets"; import { assignTemplateMethodSlugsToPrefill } from "./customRuleFacets";
import { methodSlugFromTitle } from "./methodSlugFromTitle"; import { methodSlugFromTitle } from "./methodSlugFromTitle";
import type { DeckId } from "./deckTypes";
type TemplateEntry = { title: unknown }; type TemplateEntry = { title: unknown };
type TemplateSection = { categoryName: unknown; entries: unknown }; type TemplateSection = { categoryName: unknown; entries: unknown };
@@ -33,15 +35,19 @@ function normaliseCategoryKey(name: string): string {
return name.toLowerCase().replace(/[^a-z]+/g, ""); return name.toLowerCase().replace(/[^a-z]+/g, "");
} }
/** Preset core-value labels with the chip id (1-based preset index as string) the select screen expects. */ /** Preset core values keyed by the stable id in `coreValues.json`. */
type CorePresetRow = { id: string; label: string }; type CorePresetRow = { id: string; label: string };
const CORE_VALUE_PRESETS: readonly CorePresetRow[] = (() => { const CORE_VALUE_PRESETS: readonly CorePresetRow[] = (() => {
const raw = (coreValuesMessages as { values: unknown }).values; const raw = (coreValuesMessages as { values: unknown }).values;
if (!Array.isArray(raw)) return []; if (!Array.isArray(raw)) return [];
return raw.map((v, i) => ({ const out: CorePresetRow[] = [];
id: String(i + 1), for (const value of raw) {
label: typeof v === "string" ? v : (v as { label: string }).label, if (!value || typeof value !== "object") continue;
})); const row = value as { id?: unknown; label?: unknown };
if (typeof row.id !== "string" || typeof row.label !== "string") continue;
out.push({ id: row.id, label: row.label });
}
return out;
})(); })();
function buildCoreValuePrefill( function buildCoreValuePrefill(
@@ -95,9 +101,37 @@ function buildCoreValuePrefill(
* *
* Returns an empty object for malformed bodies (no sections array). * Returns an empty object for malformed bodies (no sections array).
*/ */
type TemplateChipSet = Record<DeckId | "coreValues", string[]>;
function prefillFromTemplateChips(slug: string): Partial<CreateFlowState> | null {
const templates = (
templateChipsFile as { templates: Record<string, TemplateChipSet> }
).templates;
const chips = templates[slug];
if (!chips) return null;
const selected = new Set(chips.coreValues);
return {
selectedCoreValueIds: [...chips.coreValues],
coreValuesChipsSnapshot: CORE_VALUE_PRESETS.map((preset) => ({
id: preset.id,
label: preset.label,
state: selected.has(preset.id) ? "selected" : "unselected",
})),
selectedDecisionApproachIds: [...chips.decisionApproaches],
selectedMembershipMethodIds: [...chips.membership],
selectedConflictManagementIds: [...chips.conflictManagement],
selectedCommunicationMethodIds: [...chips.communication],
};
}
export function buildTemplateCustomizePrefill( export function buildTemplateCustomizePrefill(
body: unknown, body: unknown,
templateSlug?: string,
): Partial<CreateFlowState> { ): Partial<CreateFlowState> {
if (templateSlug) {
const fromIds = prefillFromTemplateChips(templateSlug);
if (fromIds) return fromIds;
}
if (!body || typeof body !== "object") return {}; if (!body || typeof body !== "object") return {};
const sections = (body as { sections?: unknown }).sections; const sections = (body as { sections?: unknown }).sections;
if (!Array.isArray(sections)) return {}; if (!Array.isArray(sections)) return {};
+20 -4
View File
@@ -1,5 +1,11 @@
import facetGroups from "../../data/create/customRule/_facetGroups.json"; import facetGroups from "../../data/create/customRule/_facetGroups.json";
import type { CreateFlowState } from "../../app/(app)/create/types"; import type { CreateFlowState } from "../../app/(app)/create/types";
import {
resolveMaturityChipId,
resolveOrgChipId,
resolveScaleChipId,
resolveSizeChipId,
} from "./communityChipIds";
const FACET_GROUPS = ["size", "orgType", "scale", "maturity"] as const; const FACET_GROUPS = ["size", "orgType", "scale", "maturity"] as const;
type FacetGroupId = (typeof FACET_GROUPS)[number]; type FacetGroupId = (typeof FACET_GROUPS)[number];
@@ -32,6 +38,13 @@ const STATE_KEY_BY_GROUP: Record<FacetGroupId, keyof CreateFlowState> = {
maturity: "selectedMaturityIds", maturity: "selectedMaturityIds",
}; };
const RESOLVE_BY_GROUP: Record<FacetGroupId, (id: string) => string> = {
size: resolveSizeChipId,
orgType: resolveOrgChipId,
scale: resolveScaleChipId,
maturity: resolveMaturityChipId,
};
function readChipIds( function readChipIds(
state: CreateFlowState, state: CreateFlowState,
group: FacetGroupId, group: FacetGroupId,
@@ -49,11 +62,14 @@ export function buildFacetQueryString(state: CreateFlowState): string {
const params = new URLSearchParams(); const params = new URLSearchParams();
for (const group of FACET_GROUPS) { for (const group of FACET_GROUPS) {
const valuesById = CHIP_TO_VALUE_BY_GROUP[group]; const valuesById = CHIP_TO_VALUE_BY_GROUP[group];
const resolve = RESOLVE_BY_GROUP[group];
const seen = new Set<string>();
for (const chipId of readChipIds(state, group)) { for (const chipId of readChipIds(state, group)) {
const valueId = valuesById[chipId]; const valueId = valuesById[resolve(chipId)] ?? valuesById[chipId];
if (valueId) { if (valueId === "enterprise") continue;
params.append(`facet.${group}`, valueId); if (!valueId || seen.has(valueId)) continue;
} seen.add(valueId);
params.append(`facet.${group}`, valueId);
} }
} }
return params.toString(); return params.toString();
+6 -1
View File
@@ -20,6 +20,7 @@ import {
replaceMethodSectionsWithMethodSelections, replaceMethodSectionsWithMethodSelections,
withoutUnpublishedDecisionConsensus, withoutUnpublishedDecisionConsensus,
} from "./ruleSectionsFromMethodSelections"; } from "./ruleSectionsFromMethodSelections";
import { LAUNCH_SUPERMAJORITY_SHARE } from "./sharedTermsSection";
import { templateCategoryToGroupKey } from "./templateReviewMapping"; import { templateCategoryToGroupKey } from "./templateReviewMapping";
export { isDocumentEntry } from "./documentEntryGuards"; export { isDocumentEntry } from "./documentEntryGuards";
@@ -167,7 +168,11 @@ export function buildPublishPayload(
); );
} }
const document: Record<string, unknown> = { sections, coreValues }; const document: Record<string, unknown> = {
sections,
coreValues,
supermajorityShare: LAUNCH_SUPERMAJORITY_SHARE,
};
if (hasAnyMethodSelection(methodSelections)) { if (hasAnyMethodSelection(methodSelections)) {
document.methodSelections = methodSelections; document.methodSelections = methodSelections;
} }
+128
View File
@@ -0,0 +1,128 @@
import type { CommunityStructureChipSnapshotRow } from "../../app/(app)/create/types";
import type { Stage } from "./deckTypes";
/**
* Stable chip ids for Create Community. Old drafts stored a 1-based position.
* Enterprise and Larger organization both load as Established.
*/
const SIZE_BY_POSITION = [
"oneMember",
"twoToFive",
"sixToTwelve",
"thirteenToOneHundred",
"oneHundredToOneHundredK",
] as const;
const ORG_BY_POSITION = [
"workersCoop",
"mutualAid",
"openSource",
"nonprofit",
"forProfit",
"dao",
] as const;
const SCALE_BY_POSITION = ["local", "regional", "national", "global"] as const;
const MATURITY_BY_POSITION = [
"earlyStage",
"growthStage",
"established",
"established",
] as const;
const MATURITY_LABEL_ALIASES: Record<string, Stage> = {
"Early stage": "earlyStage",
"Growth stage": "growthStage",
Established: "established",
Enterprise: "established",
"Larger organization": "established",
};
const ORG_LABEL_ALIASES: Record<string, string> = {
"Worker cooperative": "workersCoop",
"Worker co-op": "workersCoop",
"Mutual aid": "mutualAid",
"Open source project": "openSource",
Nonprofit: "nonprofit",
"For profit business": "forProfit",
DAO: "dao",
};
function fromPosition(
id: string,
positions: readonly string[],
): string | null {
if (!/^\d+$/.test(id)) return null;
const index = Number(id) - 1;
if (index < 0 || index >= positions.length) return null;
return positions[index] ?? null;
}
export function resolveSizeChipId(id: string): string {
return fromPosition(id, SIZE_BY_POSITION) ?? id;
}
export function resolveOrgChipId(id: string, label?: string): string {
if (label && ORG_LABEL_ALIASES[label]) return ORG_LABEL_ALIASES[label];
return fromPosition(id, ORG_BY_POSITION) ?? id;
}
export function resolveScaleChipId(id: string): string {
return fromPosition(id, SCALE_BY_POSITION) ?? id;
}
export function resolveMaturityChipId(id: string, label?: string): string {
if (label && MATURITY_LABEL_ALIASES[label]) return MATURITY_LABEL_ALIASES[label];
if (id === "enterprise" || id === "larger-organization") return "established";
return fromPosition(id, MATURITY_BY_POSITION) ?? id;
}
export function resolveChipIdList(
ids: readonly string[] | undefined,
resolve: (id: string) => string,
): string[] | undefined {
if (!ids) return undefined;
const out: string[] = [];
const seen = new Set<string>();
for (const id of ids) {
const next = resolve(id);
if (!next || seen.has(next)) continue;
seen.add(next);
out.push(next);
}
return out;
}
type PresetChip = { id: string; label: string };
/**
* Rewrite a saved chip snapshot onto the current preset ids and labels.
* Custom rows stay. Several maturity answers stay, as distinct ids.
*/
export function migrateChipSnapshot(
rows: CommunityStructureChipSnapshotRow[] | undefined,
presets: readonly PresetChip[],
resolve: (id: string, label?: string) => string,
): CommunityStructureChipSnapshotRow[] | undefined {
if (!rows || rows.length === 0) return rows;
const presetIds = new Set(presets.map((p) => p.id));
const selected = new Set<string>();
const customs: CommunityStructureChipSnapshotRow[] = [];
for (const row of rows) {
const nextId = resolve(row.id, row.label);
if (presetIds.has(nextId)) {
if (row.state === "selected") selected.add(nextId);
continue;
}
customs.push({ ...row, id: nextId });
}
return [
...presets.map((preset) => ({
id: preset.id,
label: preset.label,
state: selected.has(preset.id) ? "selected" : "unselected",
})),
...customs,
];
}
+1 -1
View File
@@ -8,7 +8,7 @@ import {
} from "./duplicateMethodCardModalDraft"; } from "./duplicateMethodCardModalDraft";
import { moveFacetSelectionIdToFront } from "./methodCardSelectionOrder"; import { moveFacetSelectionIdToFront } from "./methodCardSelectionOrder";
export const MAX_SELECTED_CORE_VALUES = 5; export const MAX_SELECTED_CORE_VALUES = 6;
/** Remove a chip from snapshot, selection ids, and per-chip detail overrides. */ /** Remove a chip from snapshot, selection ids, and per-chip detail overrides. */
export function removeCoreValueChipFromDraft( export function removeCoreValueChipFromDraft(
+8 -3
View File
@@ -3,11 +3,16 @@ import type {
CreateFlowState, CreateFlowState,
} from "../../app/(app)/create/types"; } from "../../app/(app)/create/types";
type CoreValuePreset = { label: string; meaning: string; signals: string }; type CoreValuePreset = {
id: string;
label: string;
meaning: string;
signals: string;
};
function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[] { function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[] {
return presets.map((row, i) => ({ return presets.map((row) => ({
id: String(i + 1), id: row.id,
label: row.label, label: row.label,
state: "unselected" as const, state: "unselected" as const,
})); }));
+283
View File
@@ -0,0 +1,283 @@
/**
* Reference engine for RECOMMENDED and the seven, as agreed on 24 and 27 October 2026.
*
* Pure functions with no I/O, written to be ported into
* useFacetRecommendations.ts (deriveCompactCards) and methodRecommendations.ts.
* The README in this package states the same rule in words.
*/
import communicationFacets from "../../data/create/customRule/communication.json";
import conflictFacets from "../../data/create/customRule/conflictManagement.json";
import decisionFacets from "../../data/create/customRule/decisionApproaches.json";
import jobsFile from "../../data/create/customRule/_jobs.json";
import membershipFacets from "../../data/create/customRule/membership.json";
import type {
CardEngineEntry,
DeckEngine,
DeckId,
DeckRecommendation,
FacetFile,
GroupAnswers,
JobsFile,
Kind,
MatchedFacet,
RankTier,
RecommendedCard,
RecommendationReason,
SizeBand,
TraitKey,
} from "./deckTypes";
export interface DeckData {
deck: DeckId;
facets: FacetFile;
engine: DeckEngine;
config: Pick<
JobsFile,
"displayLimit" | "maxAddedInSeven" | "tierOrder" | "traitKeys" | "sizeAnswers"
>;
}
export interface RecommendOptions {
/** How many recommended cards show before See all. A screen that shows five passes 5. */
limit?: number;
}
interface Evaluated {
id: string;
entry: CardEngineEntry;
eligible: boolean;
matched: MatchedFacet[];
fitsAnswer?: TraitKey;
openedBy?: TraitKey;
score: number;
otherKindHits: number;
}
/** Normalizes setup answers. A group with other kinds and no main kind gets the first as main. */
export function normalizeAnswers(answers: GroupAnswers): Required<GroupAnswers> {
const others = [...(answers.otherKinds ?? [])];
const mainKind = answers.mainKind ?? others.shift() ?? null;
return {
size: answers.size ?? null,
mainKind,
otherKinds: others.filter((k) => k !== mainKind),
scale: [...new Set(answers.scale ?? [])],
stage: answers.stage ?? null,
traits: answers.traits ?? {},
};
}
function sizeBandOf(size: string | null, data: DeckData): SizeBand | null {
if (!size) return null;
const band = data.config.sizeAnswers[size];
if (!band) throw new Error(`Unknown size answer ${size}`);
return band;
}
function evaluate(
id: string,
data: DeckData,
a: Required<GroupAnswers>,
band: SizeBand | null,
ticked: Set<TraitKey>,
): Evaluated {
const f = data.facets[id];
const entry = data.engine.cards[id];
if (!f || !entry) throw new Error(`Card ${id} is missing from the facet or jobs file`);
const traits = entry.traits ?? {};
const fitsSize = band ? f.size[band] : true;
const fitsKind = a.mainKind ? f.orgType[a.mainKind] : true;
const openedBy = (traits.opens ?? []).find((k) => ticked.has(k));
const isBeside = (traits.beside ?? []).length > 0;
// Each facet group counts once. Scale earns its point only if the card fits
// every scale the group ticked (27 Oct, call 2).
const fitsScale = a.scale.length > 0 && a.scale.every((s) => f.scale[s]);
const fitsStage = a.stage !== null && f.maturity[a.stage];
const fitsAnswer = (traits.fits ?? []).find((k) => ticked.has(k));
const matched: MatchedFacet[] = [];
if (band && fitsSize) matched.push("size");
if (a.mainKind && fitsKind) matched.push("kind");
if (fitsScale) matched.push("scale");
if (fitsStage) matched.push("stage");
return {
id,
entry,
// Size and the main kind decide eligibility, or a ticked answer opens the card.
// A card an answer adds never competes for a job.
eligible: !isBeside && fitsSize && (fitsKind || openedBy !== undefined),
matched,
fitsAnswer,
openedBy,
// Inside a job, scale, stage and the one point from answers decide (27 Oct, call 1).
score: (fitsScale ? 1 : 0) + (fitsStage ? 1 : 0) + (fitsAnswer ? 1 : 0),
otherKindHits: a.otherKinds.filter((k: Kind) => f.orgType[k]).length,
};
}
/** Best first. Score, then other kinds, then row order inside the job. */
function compareCandidates(x: Evaluated, y: Evaluated): number {
return (
y.score - x.score ||
y.otherKindHits - x.otherKindHits ||
x.entry.priority - y.entry.priority
);
}
/** What separated the winner from the runner-up. */
function decidedBy(c: Evaluated[]): RecommendedCard["decidedBy"] {
if (c.length < 2) return "only";
if (c[0].score !== c[1].score) return "score";
if (c[0].otherKindHits !== c[1].otherKindHits) return "otherKinds";
return "rowOrder";
}
export function recommendDeck(
data: DeckData,
answers: GroupAnswers,
options: RecommendOptions = {},
): DeckRecommendation {
const limit = options.limit ?? data.config.displayLimit;
const a = normalizeAnswers(answers);
const band = sizeBandOf(a.size, data);
const ticked = new Set<TraitKey>(data.config.traitKeys.filter((k) => a.traits[k] === true));
const jobs = data.engine.jobs;
const jobIndex = new Map(jobs.map((j, i) => [j.id, i]));
const allInDeckOrder = deckOrder(data);
const anyFacet = band !== null || a.mainKind !== null || a.scale.length > 0 || a.stage !== null;
const fallback = (): DeckRecommendation => ({
deck: data.deck,
fallback: true,
recommended: [],
shown: [],
waiting: [],
seeAll: allInDeckOrder,
});
if (!anyFacet) return fallback();
const cardIds = Object.keys(data.engine.cards);
const evaluated = new Map(cardIds.map((id) => [id, evaluate(id, data, a, band, ticked)]));
const tierRank = (t: RankTier) => data.config.tierOrder.indexOf(t);
const recommended: (RecommendedCard & { sortKey: number[]; showKey: number[] })[] = [];
// One card per job that has an eligible card. The runner-up is its alternative.
for (const job of jobs) {
if (job.displayTier === "none") continue;
const candidates = [...evaluated.values()]
.filter((e) => e.entry.job === job.id && e.eligible)
.sort(compareCandidates);
const winner = candidates[0];
if (!winner) continue;
const reason: RecommendationReason =
winner.openedBy && !winner.matched.includes("kind")
? { kind: "opened", matched: winner.matched, answer: winner.openedBy }
: { kind: "fit", matched: winner.matched, ...(winner.fitsAnswer ? { answer: winner.fitsAnswer } : {}) };
recommended.push({
id: winner.id,
job: job.id,
showsWith: job.id,
tier: job.displayTier as RankTier,
tag: winner.entry.everyRuleHas ? "everyRuleHas" : "recommended",
reason,
...(candidates[1] ? { alternative: candidates[1].id } : {}),
score: winner.score,
decidedBy: decidedBy(candidates),
sortKey: [tierRank(job.displayTier as RankTier), job.rank ?? 0, 0],
showKey: [jobIndex.get(job.id)!, 0, 0],
});
}
// Cards a ticked answer adds beside a job, whatever the group's size or kind.
const added = data.engine.addedOrder.filter((id) => {
const e = evaluated.get(id)!;
return (e.entry.traits?.beside ?? []).some((k) => ticked.has(k));
});
added.forEach((id, i) => {
const e = evaluated.get(id)!;
const besideJob = e.entry.traits!.besideJob!;
const answer = e.entry.traits!.beside!.find((k) => ticked.has(k))!;
const inSevenSlot = i < data.config.maxAddedInSeven;
recommended.push({
id,
job: e.entry.job,
showsWith: besideJob,
tier: "answers",
tag: e.entry.everyRuleHas ? "everyRuleHas" : "recommended",
reason: { kind: "added", answer },
// The first added cards rank right after the safety jobs, any more rank last.
sortKey: inSevenSlot ? [tierRank("answers"), 0, i] : [data.config.tierOrder.length, 0, i],
showKey: [jobIndex.get(besideJob)!, 1, i],
});
});
if (recommended.length === 0) return fallback();
const compareKeys = (x: number[], y: number[]) => {
for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return x[i] - y[i];
return 0;
};
// Rank decides which cards show. Deck order decides where they show.
const ranked = [...recommended].sort((x, y) => compareKeys(x.sortKey, y.sortKey));
const shownIds = new Set(ranked.slice(0, limit).map((r) => r.id));
const strip = ({ sortKey: _s, showKey: _w, ...card }: (typeof recommended)[number]): RecommendedCard => card;
const inShowOrder = [...recommended].sort((x, y) => compareKeys(x.showKey, y.showKey));
const shown = inShowOrder.filter((r) => shownIds.has(r.id)).map(strip);
const waiting = inShowOrder.filter((r) => !shownIds.has(r.id)).map(strip);
const recommendedIds = new Set(recommended.map((r) => r.id));
return {
deck: data.deck,
fallback: false,
recommended: ranked.map(strip),
shown,
waiting,
seeAll: [...waiting.map((r) => r.id), ...allInDeckOrder.filter((id) => !recommendedIds.has(id))],
};
}
/** Every card in deck order, job by job, then by priority inside each job. */
export function deckOrder(data: DeckData): string[] {
const jobIndex = new Map(data.engine.jobs.map((j, i) => [j.id, i]));
const key = (id: string) => {
const e = data.engine.cards[id];
return (jobIndex.get(e.job) ?? data.engine.jobs.length) * 1000 + e.priority;
};
return Object.keys(data.engine.cards).sort((x, y) => key(x) - key(y));
}
/**
* The template path (27 Oct). A group that starts from a template answers no setup
* question and gets no recommendations. Each deck shows the template's cards first,
* in the template's order, with no tag and no seven-card cut, then the rest of the
* deck in deck order. templateCardIds come from templateChips.json.
*/
const jobs = jobsFile as JobsFile;
const FACETS: Record<DeckId, FacetFile> = {
decisionApproaches: decisionFacets as FacetFile,
membership: membershipFacets as FacetFile,
conflictManagement: conflictFacets as FacetFile,
communication: communicationFacets as FacetFile,
};
/** One deck's facets, jobs and display config, ready for {@link recommendDeck}. */
export function deckData(deck: DeckId): DeckData {
return {
deck,
facets: FACETS[deck],
engine: jobs.decks[deck],
config: {
displayLimit: jobs.displayLimit,
maxAddedInSeven: jobs.maxAddedInSeven,
tierOrder: jobs.tierOrder,
traitKeys: jobs.traitKeys,
sizeAnswers: jobs.sizeAnswers,
},
};
}
export function templateDeck(data: DeckData, templateCardIds: string[]): { deck: DeckId; first: string[]; rest: string[] } {
const live = new Set(Object.keys(data.engine.cards));
const first = [...new Set(templateCardIds)].filter((id) => live.has(id));
const chosen = new Set(first);
return { deck: data.deck, first, rest: deckOrder(data).filter((id) => !chosen.has(id)) };
}
+137
View File
@@ -0,0 +1,137 @@
/** Deck engine types. Shape matches the 2 Nov 2026 deck package. */
export const DECK_IDS = [
"decisionApproaches",
"membership",
"conflictManagement",
"communication",
] as const;
export type DeckId = (typeof DECK_IDS)[number];
export const SIZE_BANDS = [
"oneMember",
"twoToFive",
"sixToTwelve",
"thirteenToOneHundred",
"oneHundredToOneHundredK",
] as const;
export type SizeBand = (typeof SIZE_BANDS)[number];
export const KINDS = [
"workersCoop",
"mutualAid",
"nonprofit",
"forProfit",
"openSource",
"dao",
] as const;
export type Kind = (typeof KINDS)[number];
export const SCALES = ["local", "regional", "national", "global"] as const;
export type Scale = (typeof SCALES)[number];
export const STAGES = [
"earlyStage",
"growthStage",
"established",
"enterprise",
] as const;
export type Stage = (typeof STAGES)[number];
export const TRAIT_KEYS = [
"network",
"nonvoters",
"employer",
"sponsor",
"records",
"risky",
] as const;
export type TraitKey = (typeof TRAIT_KEYS)[number];
export const DISPLAY_TIERS = ["frame", "safety", "fitted", "floor", "none"] as const;
export type DisplayTier = (typeof DISPLAY_TIERS)[number];
export type RankTier = Exclude<DisplayTier, "none"> | "answers";
export interface CardFacets {
size: Record<SizeBand, boolean>;
orgType: Record<Kind, boolean>;
scale: Record<Scale, boolean>;
maturity: Record<Stage, boolean>;
}
export type FacetFile = Record<string, CardFacets>;
export interface CardTraits {
beside?: TraitKey[];
besideJob?: string;
fits?: TraitKey[];
opens?: TraitKey[];
}
export interface CardEngineEntry {
job: string;
priority: number;
everyRuleHas?: boolean;
traits?: CardTraits;
}
export interface JobDefinition {
id: string;
label: string;
displayTier: DisplayTier;
rank?: number;
why: string;
}
export interface DeckEngine {
jobs: JobDefinition[];
addedOrder: string[];
cards: Record<string, CardEngineEntry>;
}
export interface JobsFile {
about: string;
version: string;
displayLimit: number;
maxAddedInSeven: number;
tierOrder: RankTier[];
traitKeys: TraitKey[];
sizeAnswers: Record<string, SizeBand>;
decks: Record<DeckId, DeckEngine>;
}
export interface GroupAnswers {
size?: string | null;
mainKind?: Kind | null;
otherKinds?: Kind[];
scale?: Scale[];
stage?: Stage | null;
traits?: Partial<Record<TraitKey, boolean>>;
}
export type MatchedFacet = "size" | "kind" | "scale" | "stage";
export type RecommendationReason =
| { kind: "fit"; matched: MatchedFacet[]; answer?: TraitKey }
| { kind: "opened"; matched: MatchedFacet[]; answer: TraitKey }
| { kind: "added"; answer: TraitKey };
export interface RecommendedCard {
id: string;
job: string;
showsWith: string;
tier: RankTier;
tag: "recommended" | "everyRuleHas";
reason: RecommendationReason;
alternative?: string;
score?: number;
decidedBy?: "only" | "score" | "otherKinds" | "rowOrder";
}
export interface DeckRecommendation {
deck: DeckId;
fallback: boolean;
recommended: RecommendedCard[];
shown: RecommendedCard[];
waiting: RecommendedCard[];
seeAll: string[];
}
+15 -29
View File
@@ -113,9 +113,6 @@ export function emptyMembershipDetails(): MembershipMethodDetailEntry {
return { eligibility: "", joiningProcess: "", expectations: "" }; return { eligibility: "", joiningProcess: "", expectations: "" };
} }
/** Default consensus level used when a **catalog** preset omits a value. */
export const DECISION_CONSENSUS_LEVEL_DEFAULT = 75;
export function decisionApproachPresetFor( export function decisionApproachPresetFor(
id: string, id: string,
): DecisionApproachDetailEntry { ): DecisionApproachDetailEntry {
@@ -128,13 +125,8 @@ export function decisionApproachPresetFor(
stepByStepInstructions: asString(s.stepByStepInstructions), stepByStepInstructions: asString(s.stepByStepInstructions),
objectionsDeadlocks: asString(s.objectionsDeadlocks), objectionsDeadlocks: asString(s.objectionsDeadlocks),
}; };
if (method) { if (method && typeof s.consensusLevel === "number" && Number.isFinite(s.consensusLevel)) {
entry.consensusLevel = asNumberClamped( entry.consensusLevel = asNumberClamped(s.consensusLevel, 0, 100, 0);
s.consensusLevel,
0,
100,
DECISION_CONSENSUS_LEVEL_DEFAULT,
);
} }
return entry; return entry;
} }
@@ -189,35 +181,29 @@ export function emptyConflictManagementDetails(): ConflictManagementDetailEntry
export function coreValuePresetFor(chipId: string): CoreValueDetailEntry { export function coreValuePresetFor(chipId: string): CoreValueDetailEntry {
const values = (coreValuesMessages as { values?: unknown }).values; const values = (coreValuesMessages as { values?: unknown }).values;
if (!Array.isArray(values)) return { meaning: "", signals: "" }; if (!Array.isArray(values)) return { meaning: "", signals: "" };
const idx = Number.parseInt(chipId, 10); for (const row of values) {
if (!Number.isInteger(idx) || idx < 1 || idx > values.length) { if (!row || typeof row !== "object") continue;
return { meaning: "", signals: "" }; const o = row as Record<string, unknown>;
if (o.id !== chipId) continue;
return {
meaning: asString(o.meaning),
signals: asString(o.signals),
};
} }
const row = values[idx - 1]; return { meaning: "", signals: "" };
if (!row || typeof row !== "object") return { meaning: "", signals: "" };
const o = row as Record<string, unknown>;
return {
meaning: asString(o.meaning),
signals: asString(o.signals),
};
} }
/** /** Preset chip id for a core value label, or `null` when the label is bespoke. */
* Preset chip id for a core value label (`"1"` … `"n"` from
* `CoreValuesSelectScreen`), or `null` when the label is bespoke.
*/
export function resolveCoreValueChipIdFromLabel(label: string): string | null { export function resolveCoreValueChipIdFromLabel(label: string): string | null {
const t = label.trim(); const t = label.trim();
if (!t) return null; if (!t) return null;
const values = (coreValuesMessages as { values?: unknown }).values; const values = (coreValuesMessages as { values?: unknown }).values;
if (!Array.isArray(values)) return null; if (!Array.isArray(values)) return null;
for (let i = 0; i < values.length; i++) { for (const row of values) {
const row = values[i];
if (typeof row === "string" && row.trim() === t) return String(i + 1);
if (!row || typeof row !== "object") continue; if (!row || typeof row !== "object") continue;
const o = row as Record<string, unknown>; const o = row as Record<string, unknown>;
if (typeof o.label === "string" && o.label.trim() === t) { if (typeof o.label === "string" && o.label.trim() === t && typeof o.id === "string") {
return String(i + 1); return o.id;
} }
} }
return null; return null;
+87
View File
@@ -0,0 +1,87 @@
import type { CreateFlowState } from "../../app/(app)/create/types";
import type { RequestedFacets } from "../server/validation/methodFacetsSchemas";
import {
resolveMaturityChipId,
resolveOrgChipId,
resolveScaleChipId,
resolveSizeChipId,
} from "./communityChipIds";
import type { GroupAnswers, Kind, Scale, Stage, TraitKey } from "./deckTypes";
import { KINDS, SCALES, STAGES, TRAIT_KEYS } from "./deckTypes";
function isKind(value: string): value is Kind {
return (KINDS as readonly string[]).includes(value);
}
function isScale(value: string): value is Scale {
return (SCALES as readonly string[]).includes(value);
}
function isStage(value: string): value is Stage {
return (STAGES as readonly string[]).includes(value);
}
/** Larger organization and Enterprise both score as Established. */
export function stageForScoring(value: string | null | undefined): Stage | null {
if (!value) return null;
if (value === "enterprise" || value === "larger-organization") return "established";
return isStage(value) ? value : null;
}
/**
* One size, one main kind, every ticked scale, and one maturity answer.
* Extra maturity answers stay on the draft and do not add points.
*/
export function groupAnswersFromRequestedFacets(
facets: RequestedFacets,
traits?: Partial<Record<TraitKey, boolean>>,
): GroupAnswers {
const kinds = (facets.orgType ?? []).filter(isKind);
const scales = (facets.scale ?? []).filter(isScale);
const maturity = (facets.maturity ?? [])
.map((value) => stageForScoring(value))
.filter((value): value is Stage => value !== null);
return {
size: facets.size?.[0] ?? null,
mainKind: kinds[0] ?? null,
otherKinds: kinds.slice(1),
scale: scales,
stage: maturity[0] ?? null,
traits,
};
}
function traitRecord(
raw: CreateFlowState["setupTraits"],
): Partial<Record<TraitKey, boolean>> | undefined {
if (!raw) return undefined;
const out: Partial<Record<TraitKey, boolean>> = {};
for (const key of TRAIT_KEYS) {
if (typeof raw[key] === "boolean") out[key] = raw[key];
}
return out;
}
export function groupAnswersFromCreateFlowState(
state: CreateFlowState,
): GroupAnswers {
const size = (state.selectedCommunitySizeIds ?? []).map(resolveSizeChipId)[0] ?? null;
const kinds = (state.selectedOrganizationTypeIds ?? [])
.map((id) => resolveOrgChipId(id))
.filter(isKind);
const scale = (state.selectedScaleIds ?? [])
.map(resolveScaleChipId)
.filter(isScale);
const maturity = (state.selectedMaturityIds ?? [])
.map((id) => stageForScoring(resolveMaturityChipId(id)))
.filter((value): value is Stage => value !== null);
const uniqueScale = [...new Set(scale)];
return {
size,
mainKind: kinds[0] ?? null,
otherKinds: kinds.slice(1).filter((kind, index, all) => all.indexOf(kind) === index),
scale: uniqueScale,
stage: maturity[0] ?? null,
traits: traitRecord(state.setupTraits),
};
}
+175
View File
@@ -0,0 +1,175 @@
import type { CreateFlowState } from "../../app/(app)/create/types";
import idAliasesFile from "../../data/create/customRule/_idAliases.json";
import type { DeckId } from "./deckTypes";
import { DECK_IDS } from "./deckTypes";
type MovedTo = { deck: string; id: string };
type MethodAlias =
| string
| {
target: string | null;
movedTo?: MovedTo[];
};
type IdAliasesFile = {
methods: Record<string, Record<string, MethodAlias>>;
coreValues: {
oldPositionToNewId: Record<string, string | null>;
labelMerges: Record<string, string | null>;
};
};
const aliases = idAliasesFile as IdAliasesFile;
function isDeckId(value: string): value is DeckId {
return (DECK_IDS as readonly string[]).includes(value);
}
export type ResolvedMethodId = { deck: DeckId; id: string };
/**
* Old card id → new card id. A cut card follows `movedTo` when that card
* lives in a deck. `null` means the card was cut and has nowhere to go.
*/
export function resolveMethodId(
deck: DeckId,
id: string,
): ResolvedMethodId | null {
const raw = aliases.methods[deck]?.[id];
if (raw === undefined) return { deck, id };
if (typeof raw === "string") return { deck, id: raw };
if (typeof raw.target === "string" && raw.target.length > 0) {
return { deck, id: raw.target };
}
const moved = raw.movedTo?.find((row) => isDeckId(row.deck));
if (moved && isDeckId(moved.deck)) return { deck: moved.deck, id: moved.id };
return null;
}
/** Old 1-based core-value position → stable id. `null` means the value was cut. */
export function resolveCoreValueId(id: string): string | null {
if (Object.prototype.hasOwnProperty.call(aliases.coreValues.oldPositionToNewId, id)) {
return aliases.coreValues.oldPositionToNewId[id] ?? null;
}
return id;
}
export function resolveCoreValueLabel(label: string): string | null {
if (Object.prototype.hasOwnProperty.call(aliases.coreValues.labelMerges, label)) {
return aliases.coreValues.labelMerges[label];
}
return label;
}
function remapIdList(
ids: string[] | undefined,
deck: DeckId,
into: Map<DeckId, string[]>,
): void {
if (!ids) return;
for (const id of ids) {
const next = resolveMethodId(deck, id);
if (!next) continue;
const list = into.get(next.deck) ?? [];
if (!list.includes(next.id)) list.push(next.id);
into.set(next.deck, list);
}
}
const METHOD_ID_KEYS = {
decisionApproaches: "selectedDecisionApproachIds",
membership: "selectedMembershipMethodIds",
conflictManagement: "selectedConflictManagementIds",
communication: "selectedCommunicationMethodIds",
} as const satisfies Record<DeckId, keyof CreateFlowState>;
const DETAIL_KEYS = {
decisionApproaches: "decisionApproachDetailsById",
membership: "membershipMethodDetailsById",
conflictManagement: "conflictManagementDetailsById",
communication: "communicationMethodDetailsById",
} as const satisfies Record<DeckId, keyof CreateFlowState>;
/**
* Remap saved method and core-value ids onto the remade decks.
* Safe to run more than once: stable ids pass through.
*/
export function migrateSavedDeckIds(
state: CreateFlowState,
): CreateFlowState {
const byDeck = new Map<DeckId, string[]>();
for (const deck of DECK_IDS) {
const key = METHOD_ID_KEYS[deck];
const ids = state[key];
remapIdList(Array.isArray(ids) ? (ids as string[]) : undefined, deck, byDeck);
}
const detailsByDeck = new Map<DeckId, Record<string, unknown>>();
for (const deck of DECK_IDS) {
const key = DETAIL_KEYS[deck];
const details = state[key];
if (!details || typeof details !== "object") continue;
for (const [id, value] of Object.entries(details as Record<string, unknown>)) {
const resolved = resolveMethodId(deck, id);
if (!resolved) continue;
const next = detailsByDeck.get(resolved.deck) ?? {};
if (next[resolved.id] === undefined) next[resolved.id] = value;
detailsByDeck.set(resolved.deck, next);
}
}
const detailsOut: Partial<CreateFlowState> = {};
for (const deck of DECK_IDS) {
if (!detailsByDeck.has(deck) && state[DETAIL_KEYS[deck]] === undefined) continue;
(detailsOut as Record<string, unknown>)[DETAIL_KEYS[deck]] =
detailsByDeck.get(deck) ?? {};
}
const coreIds: string[] = [];
const seenValues = new Set<string>();
for (const id of state.selectedCoreValueIds ?? []) {
const next = resolveCoreValueId(id);
if (!next || seenValues.has(next)) continue;
seenValues.add(next);
coreIds.push(next);
}
const coreDetails = state.coreValueDetailsByChipId;
let nextCoreDetails = coreDetails;
if (coreDetails) {
nextCoreDetails = {};
for (const [id, value] of Object.entries(coreDetails)) {
const next = resolveCoreValueId(id);
if (!next || nextCoreDetails[next]) continue;
nextCoreDetails[next] = value;
}
}
const seenSnapshot = new Set<string>();
const snapshot = state.coreValuesChipsSnapshot?.flatMap((row) => {
const nextId = resolveCoreValueId(row.id);
if (!nextId || seenSnapshot.has(nextId)) return [];
seenSnapshot.add(nextId);
const mergedLabel = resolveCoreValueLabel(row.label);
if (mergedLabel === null) return [];
return [{ ...row, id: nextId, label: mergedLabel }];
});
const methodPatch: Partial<CreateFlowState> = {};
for (const deck of DECK_IDS) {
const key = METHOD_ID_KEYS[deck];
if (state[key] !== undefined || byDeck.has(deck)) {
(methodPatch as Record<string, unknown>)[key] = byDeck.get(deck) ?? [];
}
}
return {
...state,
...methodPatch,
...detailsOut,
...(state.selectedCoreValueIds
? { selectedCoreValueIds: coreIds }
: {}),
...(snapshot ? { coreValuesChipsSnapshot: snapshot } : {}),
...(nextCoreDetails ? { coreValueDetailsByChipId: nextCoreDetails } : {}),
};
}
+69 -4
View File
@@ -1,24 +1,89 @@
import type { CreateFlowState } from "../../app/(app)/create/types"; import type {
CommunityStructureChipSnapshotRow,
CreateFlowState,
} from "../../app/(app)/create/types";
import communityStructure from "../../messages/en/create/community/communityStructure.json";
import {
migrateChipSnapshot,
resolveChipIdList,
resolveMaturityChipId,
resolveOrgChipId,
resolveScaleChipId,
resolveSizeChipId,
} from "./communityChipIds";
import { migrateSavedDeckIds } from "./idAliases";
/** Legacy `currentStep` values mapped to the current `CreateFlowStep` id. */ /** Legacy `currentStep` values mapped to the current `CreateFlowStep` id. */
const LEGACY_CREATE_FLOW_STEP_RENAMES: Readonly<Record<string, string>> = { const LEGACY_CREATE_FLOW_STEP_RENAMES: Readonly<Record<string, string>> = {
"right-rail": "decision-approaches", "right-rail": "decision-approaches",
}; };
function chipPresets(
rows: readonly { id: string; label: string }[],
): { id: string; label: string }[] {
return rows.map((row) => ({ id: row.id, label: row.label }));
}
function migrateCommunityChips(state: CreateFlowState): CreateFlowState {
const snapshots = state.communityStructureChipSnapshots;
const maturity = migrateChipSnapshot(
snapshots?.maturity as CommunityStructureChipSnapshotRow[] | undefined,
chipPresets(communityStructure.maturityOptions),
resolveMaturityChipId,
);
const organizationTypes = migrateChipSnapshot(
snapshots?.organizationTypes,
chipPresets(communityStructure.organizationTypes),
resolveOrgChipId,
);
const scale = migrateChipSnapshot(
snapshots?.scale,
chipPresets(communityStructure.scaleOptions),
resolveScaleChipId,
);
return {
...state,
selectedCommunitySizeIds: resolveChipIdList(
state.selectedCommunitySizeIds,
resolveSizeChipId,
),
selectedOrganizationTypeIds: resolveChipIdList(
state.selectedOrganizationTypeIds,
resolveOrgChipId,
),
selectedScaleIds: resolveChipIdList(state.selectedScaleIds, resolveScaleChipId),
selectedMaturityIds: resolveChipIdList(
state.selectedMaturityIds,
resolveMaturityChipId,
),
...(snapshots || maturity || organizationTypes || scale
? {
communityStructureChipSnapshots: {
...snapshots,
...(organizationTypes ? { organizationTypes } : {}),
...(scale ? { scale } : {}),
...(maturity ? { maturity } : {}),
},
}
: {}),
};
}
/** /**
* Normalizes parsed draft JSON before merging into create-flow context. * Normalizes parsed draft JSON before merging into create-flow context.
* Renames deprecated step ids so old drafts and bookmarks stay valid. * Renames deprecated step ids and remaps deck and chip ids from before the remake.
*/ */
export function migrateLegacyCreateFlowState( export function migrateLegacyCreateFlowState(
raw: Record<string, unknown> | null | undefined, raw: Record<string, unknown> | null | undefined,
): CreateFlowState { ): CreateFlowState {
if (!raw || typeof raw !== "object") return {}; if (!raw || typeof raw !== "object") return {};
const step = raw.currentStep; const step = raw.currentStep;
let state = raw as CreateFlowState;
if (typeof step === "string") { if (typeof step === "string") {
const next = LEGACY_CREATE_FLOW_STEP_RENAMES[step]; const next = LEGACY_CREATE_FLOW_STEP_RENAMES[step];
if (next !== undefined) { if (next !== undefined) {
return { ...raw, currentStep: next } as CreateFlowState; state = { ...state, currentStep: next } as CreateFlowState;
} }
} }
return raw as CreateFlowState; return migrateCommunityChips(migrateSavedDeckIds(state));
} }
+15 -15
View File
@@ -16,30 +16,30 @@ import type { TemplateFacetGroupKey } from "./templateReviewMapping";
import { RULE_SECTION_CATEGORY } from "./ruleSectionsFromMethodSelections"; import { RULE_SECTION_CATEGORY } from "./ruleSectionsFromMethodSelections";
const COMM_LABELS: Record<string, string> = { const COMM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle & Scope", corePrinciple: "Core principle and scope",
logisticsAdmin: "Logistics, Admin & Norms", logisticsAdmin: "How it works",
codeOfConduct: "Code of Conduct", codeOfConduct: "Norms",
}; };
const MEM_LABELS: Record<string, string> = { const MEM_LABELS: Record<string, string> = {
eligibility: "Eligibility & Philosophy", eligibility: "Who it covers and why",
joiningProcess: "Joining Process", joiningProcess: "How it works",
expectations: "Expectations & Removal", expectations: "Expectations and limits",
}; };
const DEC_LABELS: Record<string, string> = { const DEC_LABELS: Record<string, string> = {
corePrinciple: "Core Principle", corePrinciple: "Core principle",
applicableScope: "Applicable Scope", applicableScope: "Applicable scope",
stepByStepInstructions: "Step-by-Step Instructions", stepByStepInstructions: "Step-by-step instructions",
consensusLevel: "Consensus Level", consensusLevel: "Consensus level",
objectionsDeadlocks: "Objections & Deadlocks", objectionsDeadlocks: "Objections and deadlocks",
}; };
const CM_LABELS: Record<string, string> = { const CM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle", corePrinciple: "Core principle",
applicableScope: "Applicable Scope", applicableScope: "Applicable scope",
processProtocol: "Process Protocol", processProtocol: "How it works",
restorationFallbacks: "Restoration & Fallbacks", restorationFallbacks: "Protections and fallbacks",
}; };
const LABELS_BY_GROUP: Record< const LABELS_BY_GROUP: Record<
@@ -10,6 +10,7 @@ import type { PublishedMethodSelections } from "./buildPublishPayload";
import type { StoredLastPublishedRule } from "./lastPublishedRule"; import type { StoredLastPublishedRule } from "./lastPublishedRule";
import { normalizePublishedDocumentForEdit } from "./normalizePublishedDocumentForEdit"; import { normalizePublishedDocumentForEdit } from "./normalizePublishedDocumentForEdit";
import { methodLabelFor } from "./finalReviewChipPresets"; import { methodLabelFor } from "./finalReviewChipPresets";
import { migrateSavedDeckIds } from "./idAliases";
import type { TemplateFacetGroupKey } from "./templateReviewMapping"; import type { TemplateFacetGroupKey } from "./templateReviewMapping";
function customMethodCardMetaFromPublishedSelections( function customMethodCardMetaFromPublishedSelections(
@@ -236,5 +237,5 @@ export function createFlowStateFromPublishedRule(
/** Drop template `sections` so final-review uses `methodSelections` / selected ids (edit path). */ /** Drop template `sections` so final-review uses `methodSelections` / selected ids (edit path). */
out.sections = []; out.sections = [];
return out; return migrateSavedDeckIds(out);
} }
@@ -26,6 +26,7 @@ import {
sectionFromDecision, sectionFromDecision,
sectionFromMembership, sectionFromMembership,
} from "./ruleSectionsFromMethodSelections"; } from "./ruleSectionsFromMethodSelections";
import { withSharedTerms } from "./sharedTermsSection";
import { import {
templateCategoryToGroupKey, templateCategoryToGroupKey,
type TemplateFacetGroupKey, type TemplateFacetGroupKey,
@@ -50,30 +51,30 @@ const CANONICAL_DISPLAY_SECTION_ORDER = [
] as const; ] as const;
const COMM_LABELS: Record<string, string> = { const COMM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle & Scope", corePrinciple: "Core principle and scope",
logisticsAdmin: "Logistics, Admin & Norms", logisticsAdmin: "How it works",
codeOfConduct: "Code of Conduct", codeOfConduct: "Norms",
}; };
const MEM_LABELS: Record<string, string> = { const MEM_LABELS: Record<string, string> = {
eligibility: "Eligibility & Philosophy", eligibility: "Who it covers and why",
joiningProcess: "Joining Process", joiningProcess: "How it works",
expectations: "Expectations & Removal", expectations: "Expectations and limits",
}; };
const DEC_LABELS: Record<string, string> = { const DEC_LABELS: Record<string, string> = {
corePrinciple: "Core Principle", corePrinciple: "Core principle",
applicableScope: "Applicable Scope", applicableScope: "Applicable scope",
stepByStepInstructions: "Step-by-Step Instructions", stepByStepInstructions: "Step-by-step instructions",
consensusLevel: "Consensus Level", consensusLevel: "Consensus level",
objectionsDeadlocks: "Objections & Deadlocks", objectionsDeadlocks: "Objections and deadlocks",
}; };
const CM_LABELS: Record<string, string> = { const CM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle", corePrinciple: "Core principle",
applicableScope: "Applicable Scope", applicableScope: "Applicable scope",
processProtocol: "Process Protocol", processProtocol: "How it works",
restorationFallbacks: "Restoration & Fallbacks", restorationFallbacks: "Protections and fallbacks",
}; };
function needsPlaceholderPresetEnrichment(entry: CommunityRuleEntry): boolean { function needsPlaceholderPresetEnrichment(entry: CommunityRuleEntry): boolean {
@@ -305,6 +306,7 @@ export function parsePublishedDocumentForCommunityRuleDisplay(
); );
} }
const combined = displaySections.map(enrichDisplaySection); const sorted = sortSectionsCanonical(displaySections.map(enrichDisplaySection));
return sortSectionsCanonical(combined); if (sorted.length === 0) return sorted;
return withSharedTerms(sorted);
} }
+87
View File
@@ -0,0 +1,87 @@
/**
* The 92 real-group profiles from product/real-group-profiles.md, read as setup answers,
* and the one-member founders added to the test set on 27 Oct.
* The real-group fixture is copied unchanged. Two readings are made here, both stated
* in the README. The first kind listed is the main kind, as the review read its worked
* examples. An alsoPlausible kind is read as another kind that fits.
*/
import type { GroupAnswers, Kind, Scale, SizeBand, Stage, TraitKey } from "./deckTypes";
export interface ReviewProfile {
id: string;
size: string | null;
kinds: string[];
scale: string | null;
stage: string | null;
answers: Record<TraitKey, boolean>;
alsoPlausible?: { size?: string[]; kinds?: string[]; scale?: string[]; stage?: string[] };
}
const KIND: Record<string, Kind> = {
"worker co-op": "workersCoop",
"mutual aid": "mutualAid",
nonprofit: "nonprofit",
"for-profit": "forProfit",
"open source": "openSource",
dao: "dao",
};
const SIZE: Record<string, SizeBand> = {
"1": "oneMember",
"2-5": "twoToFive",
"6-12": "sixToTwelve",
"13-100": "thirteenToOneHundred",
"100+": "oneHundredToOneHundredK",
};
const STAGE: Record<string, Stage> = {
early: "earlyStage",
growth: "growthStage",
established: "established",
enterprise: "enterprise",
};
function kind(k: string): Kind {
const v = KIND[k];
if (!v) throw new Error(`Unknown kind ${k}`);
return v;
}
export function toAnswers(p: ReviewProfile): GroupAnswers {
const kinds = p.kinds.map(kind);
return {
size: p.size ? SIZE[p.size] : null,
mainKind: kinds[0] ?? null,
otherKinds: kinds.slice(1),
scale: p.scale ? [p.scale as Scale] : [],
stage: p.stage ? STAGE[p.stage] : null,
traits: { ...p.answers },
};
}
export interface ProfileVariant {
profile: string;
variant: string;
answers: GroupAnswers;
}
/** The profile as given, then once per alsoPlausible answer swapped in. */
export function variantsOf(p: ReviewProfile): ProfileVariant[] {
const base = toAnswers(p);
const out: ProfileVariant[] = [{ profile: p.id, variant: "base", answers: base }];
for (const s of p.alsoPlausible?.size ?? []) {
out.push({ profile: p.id, variant: `size ${s}`, answers: { ...base, size: SIZE[s] } });
}
for (const k of p.alsoPlausible?.kinds ?? []) {
out.push({
profile: p.id,
variant: `also ${k}`,
answers: { ...base, otherKinds: [...(base.otherKinds ?? []), kind(k)] },
});
}
for (const s of p.alsoPlausible?.scale ?? []) {
out.push({ profile: p.id, variant: `scale ${s}`, answers: { ...base, scale: [s as Scale] } });
}
for (const s of p.alsoPlausible?.stage ?? []) {
out.push({ profile: p.id, variant: `stage ${s}`, answers: { ...base, stage: STAGE[s] } });
}
return out;
}
+47 -20
View File
@@ -5,6 +5,7 @@ import type {
} from "../../app/components/type/CommunityRule/CommunityRule.types"; } from "../../app/components/type/CommunityRule/CommunityRule.types";
import type { PublishedMethodSelections } from "./buildPublishPayload"; import type { PublishedMethodSelections } from "./buildPublishPayload";
import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks"; import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks";
import decisionApproachesMessages from "../../messages/en/create/customRule/decisionApproaches.json";
import { decisionApproachScopeForPublish } from "./decisionApproachKeyResources"; import { decisionApproachScopeForPublish } from "./decisionApproachKeyResources";
import { templateCategoryToGroupKey } from "./templateReviewMapping"; import { templateCategoryToGroupKey } from "./templateReviewMapping";
@@ -77,6 +78,11 @@ export function labeledBlocksFromCustomMethodCardFieldBlocks(
export type CommunityRuleEntryFromChipOptions = { export type CommunityRuleEntryFromChipOptions = {
consensusLevelKey?: string; consensusLevelKey?: string;
/**
* Copy used when `consensusLevel` is absent. `null` means the figure was
* dropped on purpose (a seeded share on an empty custom card) and must not print.
*/
missingConsensusBody?: string | null;
customFieldBlocks?: CustomMethodCardFieldBlock[]; customFieldBlocks?: CustomMethodCardFieldBlock[];
/** Wizard step-2 policy description (`customMethodCardMetaById.supportText`). */ /** Wizard step-2 policy description (`customMethodCardMetaById.supportText`). */
supportText?: string; supportText?: string;
@@ -92,32 +98,42 @@ export const RULE_SECTION_CATEGORY = {
} as const; } as const;
const COMM_LABELS: Record<string, string> = { const COMM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle & Scope", corePrinciple: "Core principle and scope",
logisticsAdmin: "Logistics, Admin & Norms", logisticsAdmin: "How it works",
codeOfConduct: "Code of Conduct", codeOfConduct: "Norms",
}; };
const MEM_LABELS: Record<string, string> = { const MEM_LABELS: Record<string, string> = {
eligibility: "Eligibility & Philosophy", eligibility: "Who it covers and why",
joiningProcess: "Joining Process", joiningProcess: "How it works",
expectations: "Expectations & Removal", expectations: "Expectations and limits",
}; };
const DEC_LABELS: Record<string, string> = { const DEC_LABELS: Record<string, string> = {
corePrinciple: "Core Principle", corePrinciple: "Core principle",
applicableScope: "Applicable Scope", applicableScope: "Applicable scope",
stepByStepInstructions: "Step-by-Step Instructions", stepByStepInstructions: "Step-by-step instructions",
consensusLevel: "Consensus Level", consensusLevel: "Consensus level",
objectionsDeadlocks: "Objections & Deadlocks", objectionsDeadlocks: "Objections and deadlocks",
}; };
const CM_LABELS: Record<string, string> = { const CM_LABELS: Record<string, string> = {
corePrinciple: "Core Principle", corePrinciple: "Core principle",
applicableScope: "Applicable Scope", applicableScope: "Applicable scope",
processProtocol: "Process Protocol", processProtocol: "How it works",
restorationFallbacks: "Restoration & Fallbacks", restorationFallbacks: "Protections and fallbacks",
}; };
function consensusLevelBody(
value: unknown,
whenMissing: string | null,
): string | null {
const copy = decisionApproachesMessages.consensusLevelDisplay;
if (typeof value !== "number" || Number.isNaN(value)) return whenMissing;
if (value === 0) return copy.noSetShare;
return `${value}%`;
}
export function nonEmptyTrimmed(s: unknown): string | null { export function nonEmptyTrimmed(s: unknown): string | null {
if (typeof s !== "string") return null; if (typeof s !== "string") return null;
const t = s.trim(); const t = s.trim();
@@ -158,15 +174,18 @@ export function blocksFromKeyedRecord(
labelByKey: Record<string, string>, labelByKey: Record<string, string>,
options?: { options?: {
consensusLevelKey?: string; consensusLevelKey?: string;
missingConsensusBody?: string | null;
}, },
): CommunityRuleLabeledBlock[] { ): CommunityRuleLabeledBlock[] {
const blocks: CommunityRuleLabeledBlock[] = []; const blocks: CommunityRuleLabeledBlock[] = [];
const whenMissing =
options?.missingConsensusBody === undefined
? decisionApproachesMessages.consensusLevelDisplay.supermajorityShare
: options.missingConsensusBody;
for (const [key, label] of Object.entries(labelByKey)) { for (const [key, label] of Object.entries(labelByKey)) {
if (options?.consensusLevelKey === key) { if (options?.consensusLevelKey === key) {
const n = sections[key]; const body = consensusLevelBody(sections[key], whenMissing);
if (typeof n === "number" && !Number.isNaN(n)) { if (body) blocks.push({ label, body });
blocks.push({ label, body: `${n}%` });
}
continue; continue;
} }
const raw = sections[key]; const raw = sections[key];
@@ -189,7 +208,10 @@ export function communityRuleEntryFromMethodChip(
sections, sections,
labelByKey, labelByKey,
options?.consensusLevelKey options?.consensusLevelKey
? { consensusLevelKey: options.consensusLevelKey } ? {
consensusLevelKey: options.consensusLevelKey,
missingConsensusBody: options.missingConsensusBody,
}
: undefined, : undefined,
); );
const wizardBlocks = const wizardBlocks =
@@ -323,6 +345,11 @@ export function sectionFromDecision(
DEC_LABELS, DEC_LABELS,
{ {
consensusLevelKey: "consensusLevel", consensusLevelKey: "consensusLevel",
missingConsensusBody:
typeof sec.consensusLevel === "number" &&
!Number.isNaN(sec.consensusLevel)
? null
: undefined,
customFieldBlocks: customFieldBlocksById?.[m.id], customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText, supportText: m.supportText,
}, },
+31
View File
@@ -0,0 +1,31 @@
import type { CommunityRuleSection } from "../../app/components/type/CommunityRule/CommunityRule.types";
import sharedTerms from "../../messages/en/create/sharedTerms.json";
/** Fixed at launch. Cards that omit a consensus level read this share. */
export const LAUNCH_SUPERMAJORITY_SHARE = 75;
type SharedTerm = { term: string; meaning: string };
/**
* Every shared term, definitions only. Fallbacks for a skipped topic do not print.
* Use-case example Rules do not call this.
*/
export function sharedTermsSection(): CommunityRuleSection {
const terms = (sharedTerms as { terms: SharedTerm[] }).terms;
return {
categoryName: sharedTerms.sectionTitle,
entries: terms.map((term) => ({
title: term.term,
body: term.meaning,
})),
};
}
export function withSharedTerms(
sections: CommunityRuleSection[],
): CommunityRuleSection[] {
if (sections.some((section) => section.categoryName === sharedTerms.sectionTitle)) {
return sections;
}
return [...sections, sharedTermsSection()];
}
+5 -11
View File
@@ -21,7 +21,7 @@ export type CatalogMethodDto = {
}; };
export type CatalogCoreValueDto = { export type CatalogCoreValueDto = {
/** 1-based position in `coreValues.json` (`"1"`, `"2"`, …). */ /** Stable id from `coreValues.json` `values[].id`. */
id: string; id: string;
label: string; label: string;
meaning: string; meaning: string;
@@ -107,23 +107,17 @@ export function getCatalogMethod(
return row ? rowToCatalogMethod(row) : null; return row ? rowToCatalogMethod(row) : null;
} }
/** All preset core values, in `coreValues.json` order (ids `"1"` … `"n"`). */ /** All preset core values, in `coreValues.json` order, keyed by stable id. */
export function listCatalogCoreValues(): CatalogCoreValueDto[] { export function listCatalogCoreValues(): CatalogCoreValueDto[] {
const values = (coreValuesMessages as { values?: unknown }).values; const values = (coreValuesMessages as { values?: unknown }).values;
if (!Array.isArray(values)) return []; if (!Array.isArray(values)) return [];
const out: CatalogCoreValueDto[] = []; const out: CatalogCoreValueDto[] = [];
for (let i = 0; i < values.length; i++) { for (const row of values) {
const row = values[i];
const id = String(i + 1);
if (typeof row === "string") {
out.push({ id, label: row, meaning: "", signals: "" });
continue;
}
if (!row || typeof row !== "object") continue; if (!row || typeof row !== "object") continue;
const o = row as Record<string, unknown>; const o = row as Record<string, unknown>;
if (typeof o.label !== "string") continue; if (typeof o.id !== "string" || typeof o.label !== "string") continue;
out.push({ out.push({
id, id: o.id,
label: o.label, label: o.label,
meaning: typeof o.meaning === "string" ? o.meaning : "", meaning: typeof o.meaning === "string" ? o.meaning : "",
signals: typeof o.signals === "string" ? o.signals : "", signals: typeof o.signals === "string" ? o.signals : "",
+26 -41
View File
@@ -1,3 +1,5 @@
import { groupAnswersFromRequestedFacets } from "../create/groupAnswers";
import { deckData, recommendDeck } from "../create/deckRecommend";
import { prisma } from "./db"; import { prisma } from "./db";
import { isDatabaseConfigured } from "./env"; import { isDatabaseConfigured } from "./env";
import { import {
@@ -27,57 +29,40 @@ export type ListMethodRecommendationsResult = {
}; };
/** /**
* Returns the per-method match scores for `section`, given `facets`. * Ranks one deck with the job rule. Empty facets return no scores so callers
* Returns `null` so callers can fall back to messages-file order when DB * keep deck order. The first maturity value is the only one that scores.
* is unavailable or the query fails. * Scores decrease along the engine's display order so a score sort matches it.
*
* Notes:
* - Empty facets ⇒ `rankedSlugs: []`, `matchesBySlug: {}` (caller falls back
* to authoring order).
* - Sort is `score` desc only — re-stabilising into authoring order is the
* caller's job (the wizard already iterates the on-disk `methods[]` array).
*/ */
export async function listMethodRecommendations(args: { export async function listMethodRecommendations(args: {
section: SectionId; section: SectionId;
facets: RequestedFacets; facets: RequestedFacets;
}): Promise<ListMethodRecommendationsResult | null> { }): Promise<ListMethodRecommendationsResult | null> {
if (!isDatabaseConfigured()) return null;
const requested = flattenRequestedFacets(args.facets); const requested = flattenRequestedFacets(args.facets);
if (requested.length === 0) { if (requested.length === 0) {
return { rankedSlugs: [], matchesBySlug: {} }; return { rankedSlugs: [], matchesBySlug: {} };
} }
try { const answers = groupAnswersFromRequestedFacets(args.facets);
const rows = await prisma.methodFacet.findMany({ const recommendation = recommendDeck(deckData(args.section), answers);
where: { const orderedIds = recommendation.fallback
section: args.section, ? recommendation.seeAll
matches: true, : [
OR: requested.map(({ group, value }) => ({ group, value })), ...recommendation.shown.map((card) => card.id),
}, ...recommendation.seeAll,
select: { slug: true, group: true, value: true }, ];
}); const matchesBySlug: Record<string, MethodRanking["matches"]> = {};
orderedIds.forEach((id, index) => {
const matchesBySlug: Record<string, MethodRanking["matches"]> = {}; const card = recommendation.recommended.find((row) => row.id === id);
for (const row of rows) { const matched =
const key = `${row.group}:${row.value}`; card?.reason.kind === "fit" || card?.reason.kind === "opened"
const entry = ? card.reason.matched
matchesBySlug[row.slug] ?? : [];
(matchesBySlug[row.slug] = { score: 0, matchedFacets: [] }); matchesBySlug[id] = {
if (!entry.matchedFacets.includes(key)) { score: orderedIds.length - index,
entry.matchedFacets.push(key); matchedFacets: matched,
entry.score += 1; };
} });
} return { rankedSlugs: orderedIds, matchesBySlug };
const rankedSlugs = Object.entries(matchesBySlug)
.sort(([, a], [, b]) => b.score - a.score)
.map(([slug]) => slug);
return { rankedSlugs, matchesBySlug };
} catch {
return null;
}
} }
/** /**
@@ -115,6 +115,16 @@ export const createFlowStateSchema = z
selectedOrganizationTypeIds: z.array(z.string()).optional(), selectedOrganizationTypeIds: z.array(z.string()).optional(),
selectedScaleIds: z.array(z.string()).optional(), selectedScaleIds: z.array(z.string()).optional(),
selectedMaturityIds: z.array(z.string()).optional(), selectedMaturityIds: z.array(z.string()).optional(),
setupTraits: z
.object({
network: z.boolean().optional(),
nonvoters: z.boolean().optional(),
employer: z.boolean().optional(),
sponsor: z.boolean().optional(),
records: z.boolean().optional(),
risky: z.boolean().optional(),
})
.optional(),
communityStructureChipSnapshots: communityStructureChipSnapshots:
communityStructureChipSnapshotsSchema.optional(), communityStructureChipSnapshotsSchema.optional(),
selectedCoreValueIds: z.array(z.string()).max(200).optional(), selectedCoreValueIds: z.array(z.string()).max(200).optional(),
+1 -1
View File
@@ -147,7 +147,7 @@ export const GOVERNANCE_TEMPLATE_HOME_SLUGS: readonly string[] = [
"consensus", "consensus",
"do-ocracy", "do-ocracy",
"devolution", "devolution",
"quadratic-governance", "solidarity-network",
]; ];
export function getGovernanceTemplatesForHome(): GovernanceTemplateCatalogEntry[] { export function getGovernanceTemplatesForHome(): GovernanceTemplateCatalogEntry[] {
@@ -4,10 +4,10 @@
"description": "Choose how many people you think will be in your community in the next year or two. Your selection here will determine what governance patterns are recommended later in the process." "description": "Choose how many people you think will be in your community in the next year or two. Your selection here will determine what governance patterns are recommended later in the process."
}, },
"communitySizes": [ "communitySizes": [
{ "label": "1 member" }, { "id": "oneMember", "label": "1 member" },
{ "label": "2-5 members" }, { "id": "twoToFive", "label": "2-5 members" },
{ "label": "6-12 members" }, { "id": "sixToTwelve", "label": "6-12 members" },
{ "label": "13-100 members" }, { "id": "thirteenToOneHundred", "label": "13-100 members" },
{ "label": "100-100,000 members" } { "id": "oneHundredToOneHundredK", "label": "100-100,000 members" }
] ]
} }
@@ -16,23 +16,22 @@
"addButtonText": "Add maturity" "addButtonText": "Add maturity"
}, },
"organizationTypes": [ "organizationTypes": [
{ "label": "Worker cooperative" }, { "id": "workersCoop", "label": "Worker co-op" },
{ "label": "Mutual aid" }, { "id": "mutualAid", "label": "Mutual aid" },
{ "label": "Open source project" }, { "id": "openSource", "label": "Open source project" },
{ "label": "Nonprofit" }, { "id": "nonprofit", "label": "Nonprofit" },
{ "label": "For profit business" }, { "id": "forProfit", "label": "For profit business" },
{ "label": "DAO" } { "id": "dao", "label": "DAO" }
], ],
"scaleOptions": [ "scaleOptions": [
{ "label": "Local" }, { "id": "local", "label": "Local" },
{ "label": "Regional" }, { "id": "regional", "label": "Regional" },
{ "label": "National" }, { "id": "national", "label": "National" },
{ "label": "Global" } { "id": "global", "label": "Global" }
], ],
"maturityOptions": [ "maturityOptions": [
{ "label": "Early stage" }, { "id": "earlyStage", "label": "Early stage" },
{ "label": "Growth stage" }, { "id": "growthStage", "label": "Growth stage" },
{ "label": "Established" }, { "id": "established", "label": "Established" }
{ "label": "Enterprise" }
] ]
} }
+274 -94
View File
@@ -1,14 +1,14 @@
{ {
"_comment": "Create flow – communication step: page, cards, and add-platform modals", "_comment": "Create flow – communication step: page, cards, and add-platform modals",
"page": { "page": {
"compactTitle": "How should this community communicate with each-other?", "compactTitle": "How does your group talk, meet and keep its records?",
"compactDescriptionBefore": "You can select multiple methods for different needs or ", "compactDescriptionBefore": "Pick a card for each part of how your group communicates, from chat and meetings to who holds the keys. Combine them or ",
"compactDescriptionLinkLabel": "add", "compactDescriptionLinkLabel": "add",
"compactDescriptionAfter": " your own.", "compactDescriptionAfter": " your own.",
"expandedTitle": "What method should this community use to communicate with eachother?", "expandedTitle": "How does your group talk, meet and keep its records?",
"expandedDescriptionBefore": "You can select multiple methods for different needs or ", "expandedDescriptionBefore": "Pick a card for each part of how your group communicates, from chat and meetings to who holds the keys. Combine them or ",
"expandedDescriptionAfter": " your own", "expandedDescriptionAfter": " your own.",
"seeAllLink": "See all communication approaches" "seeAllLink": "See all communication cards"
}, },
"confirmModal": { "confirmModal": {
"title": "Confirm selection", "title": "Confirm selection",
@@ -16,125 +16,305 @@
"nextButtonText": "Confirm" "nextButtonText": "Confirm"
}, },
"addPlatform": { "addPlatform": {
"nextButtonText": "Add Platform" "nextButtonText": "Add card"
}, },
"removePlatform": { "removePlatform": {
"nextButtonText": "Remove" "nextButtonText": "Remove"
}, },
"sectionHeadings": { "sectionHeadings": {
"corePrinciple": "Core Principle & Scope", "corePrinciple": "Core principle and scope",
"logisticsAdmin": "Logistics, Admin & Norms", "logisticsAdmin": "How it works",
"codeOfConduct": "Code of Conduct" "codeOfConduct": "Norms"
}, },
"methods": [ "methods": [
{ {
"id": "in-person-meetings", "id": "how-we-run-meetings",
"label": "In-Person Meetings", "label": "How we run meetings",
"supportText": "Physical gatherings for high-bandwidth communication and relationship building.", "supportText": "Every meeting has a facilitator, a note taker and an agenda of proposals, ends on time and plans for childcare and interpretation.",
"sections": { "sections": {
"corePrinciple": "We value the highest bandwidth of communication, physical presence, to build trust that digital tools cannot match. Consequently, we reserve this high-trust space for annual retreats, strategic planning, and high-stakes interpersonal repair where body language is essential.", "corePrinciple": "A method is only as good as the meeting it lives in. Meetings are where we decide what needs all of us, so we run them so everyone can take part, not only the loudest or those with the most free time. Roles hold the process so that nobody holds the room. This covers every meeting of the whole group or a working group, in person or on a call. It pulls against spontaneity, and it breaks when the same few people always hold the roles.",
"logisticsAdmin": "Logistics focus on physical accessibility, venue security, and travel equity. Organizers control entry via keys or door staff. Culturally, participants are expected to maintain mission focus and adhere strictly to the itinerary to respect everyone's time. Side conversations or distracting behaviors that derail the agenda are discouraged. We prioritize efficiency and shared attention over multitasking.", "logisticsAdmin": "The whole group meets at least monthly, and a tenth of active members can call an extra meeting. Each meeting has a facilitator, a note taker and a timekeeper, rotating every three months. The agenda goes where every member will see it three days ahead, and each item is a proposal or a report, never just a topic. For each proposal we take clarifying questions, then reactions, then concerns and what would resolve them, then decide by the method our Rule names. People who have spoken less speak first. The note taker writes decisions, not debate, into our records within two days. We end on time, and unfinished items go to a thread with a deadline or the next meeting.",
"codeOfConduct": "We aspire to operate within these principles. We don't need to see eye to eye on everything, but we believe the world can be improved by collective action. Aspire to do no harm to members of this community. Violence or physical intimidation will not be tolerated. We have a zero-tolerance policy for racism, sexism, and bigotry. Do not engage in activities at the venue that attract unwanted legal attention or endanger the group's physical safety." "codeOfConduct": "The facilitator keeps us on the question and off the loudest voice, and decides nothing. One person speaks at a time, and the timekeeper is allowed to be annoying. We ask each new member about access needs and plan for them, such as childcare, interpretation, captions, a call-in line or a step-free room, and meetings that run past a mealtime have food. Anyone can ask for a pause or a break without explaining why."
}
},
{
"id": "announcements-channel",
"label": "Announcements channel",
"supportText": "We keep one channel every member joins and only a few post in, for decisions, meetings and urgent news.",
"sections": {
"corePrinciple": "Important news drowns in a busy chat, and a member who misses a proposal cannot object to it. So we keep one channel that every member joins and can read without following anything else. This is where every member will see it, the place this Rule sends proposals, decisions, meeting times and urgent news. In a group small enough for one chat, that chat is the channel and everyone posts in it. It breaks when it fills with chatter or leaves out members who are not on the app.",
"logisticsAdmin": "Only two to four posters the group names for a year can write here, and members can react but not reply. Posters put up any member’s proposal or objection within a day, as written, unless it shares private details. Each post covers one thing, in every language our members read, says what was decided or proposed, any deadline and where to discuss it, and links to our records. A deadline counts from when every member has been sent the news. Members not on the app get the same news by email, text or phone within a day. It doubles as our records only in an app where new members can read every past post, and then it has no disappearing timer.",
"codeOfConduct": "Posters share decisions, proposals and objections as written, never their own opinions, the roster, addresses or where members meet. Discussion happens where the post says, not here. Members keep notifications on for this channel even if they mute the rest. Any member can ask a poster to correct a post, and the correction goes up as a new post. If no poster can be reached when news is urgent, any member posts it in our main chat marked as an announcement, and a poster repeats it here."
}
},
{
"id": "discourse-forum",
"label": "Discourse (forum)",
"supportText": "We hold long discussions and shape proposals on a Discourse forum, where threads stay organized and searchable for years.",
"sections": {
"corePrinciple": "Chat moves too fast for decisions that need thought, and a busy chat buries proposals. A forum keeps each topic in its own thread that anyone can read later, so members in any time zone can take part and newcomers can see how we got here. We use it to discuss and shape proposals before they are decided and to answer common questions once for everyone. It pulls against speed, and it breaks when the real discussion happens somewhere else.",
"logisticsAdmin": "Our forum runs on Discourse, open source software that we pay a host to run. At least two admins hold admin rights and the domain. Categories follow our working groups, with one for proposals, where each post states the proposal, its deadline and how it will be decided. Trust levels let active members do more over time, and moderators close a thread when it has done its job. Each decision is summed up at the end of its thread and logged in our records.",
"codeOfConduct": "Search before starting a topic, and give it a title that says what it is about. Stay on the thread’s topic. Moderators move off-topic replies rather than delete them, and say why. Posts about hard subjects can sit behind a hidden details block, so readers choose when to open them. Admins and our host can read every post, private messages included, so nothing about one person’s situation goes on the forum."
} }
}, },
{ {
"id": "signal", "id": "signal",
"label": "Signal", "label": "Signal",
"supportText": "Encrypted messaging for high-security, private coordination.", "supportText": "We coordinate in encrypted Signal groups, with usernames instead of phone numbers and messages that disappear on a timer.",
"sections": { "sections": {
"corePrinciple": "We prioritize privacy and security above all else, accepting limited features to guarantee our communications cannot be surveilled. This acts as our \"Tactical\" layer for high-risk coordination, direct action planning, and medical/legal support where data hygiene is critical.", "corePrinciple": "Signal encrypts every message and call end to end, is run by a nonprofit and is built to keep as little as it can about who talks to whom. We use it for day-to-day coordination and for anything we would not want read aloud at a public meeting. Signal protects a message on its way, not an unlocked phone, a screenshot or a member who forwards things. A big group anyone can join is public whatever the app says, so sensitive work stays in small groups of named members.",
"logisticsAdmin": "Admins are technical stewards, not status holders; they manage entry/removal and must not change settings without explicit cause. Disappearing messages are mandatory (standard 4-week timer) to prevent data liability. Norms focus on \"Attention Economy\": use emoji reactions instead of \"thanks\" messages to avoid push notification spam. Only join channels where you can actively contribute.", "logisticsAdmin": "As of 2026 Signal still needs a phone number to sign up, but members can hide it and join with a username. Our groups require admin approval for anyone joining by link. Two to four admins approve and remove members and change settings only when the group agrees or our conflict process pauses someone. To stop harm any admin can remove someone for up to 24 hours and tells the other admins the same day. Disappearing messages are on, four weeks for most groups, one week for shift channels and one day for small groups that discuss a particular person. Timers are turned off in any chat under a legal hold. Our announcements channel is never our records, since members who join later cannot see what was sent before. Pinned messages disappear on the same timer as the rest, so decisions go to our records before they vanish.",
"codeOfConduct": "This space relies on collective responsibility. Posting content that attracts unwanted legal attention or exposes members' real-world identities without consent is prohibited. We aspire to do no harm by practicing strict operational security. Intentionally leaking information violates our safety. We have a zero-tolerance policy for racism, sexism, and bigotry. Willfully spreading false information that endangers the group is grounds for removal." "codeOfConduct": "Share your username, not your number, and check that your number is hidden from people who do not have it. Before a sensitive one-to-one chat, compare safety numbers with the other member in person. Keep a screen lock on and message previews off your lock screen, and never link Signal to a computer others use. Admins remove members who have left the group within a week."
}
},
{
"id": "video-meetings",
"label": "Video Meetings",
"supportText": "Synchronous video calls for remote face-to-face interaction.",
"sections": {
"corePrinciple": "We prioritize synchronous connection to read facial expressions without the barrier of travel, using this tool for weekly syncs and quick consensus checks that benefit from real-time debate before moving to a vote.",
"logisticsAdmin": "The host manages technical security via waiting rooms to prevent intrusion. Culturally, the focus is on maximizing the value of synchronous time. Norms include muting when not speaking, using the \"Raise Hand\" feature to queue, and utilizing the chat box for non-interruptive side comments. Distractions should be minimized to ensure the meeting stays on track and ends on time.",
"codeOfConduct": "We have a zero-tolerance policy for racism, sexism, and bigotry, whether spoken or shared in the chat. We aspire to do no harm. \"Zoom-bombing\" or broadcasting graphic content is prohibited. Willfully spreading obviously false information will not be tolerated. Do not discuss sensitive data that could attract unwanted legal scrutiny if the recording were subpoenaed. The Host will remove anyone threatening the group's safety."
}
},
{
"id": "loomio",
"label": "Loomio",
"supportText": "Decision-making platform for proposals, voting, and consensus.",
"sections": {
"corePrinciple": "We focus specifically on decision-making, separating \"chatter\" from \"signal\" to count every voice. This is our \"Ballot Box\" layer, used strictly for formal proposals, board elections, and consensus checks, distinct from discussion layers.",
"logisticsAdmin": "Groups are invite-only to protect voting integrity. Polls have set deadlines. Norms dictate that users must read the full proposal before voting, use the \"Abstain\" option if uninformed, and provide constructive feedback for any \"No\" vote.",
"codeOfConduct": "Our core unity is tied to collective action. Therefore, any attempt to fraudulently manipulate votes violates our principles. We have zero tolerance for hate speech in proposal comments. Disagreement should be expressed through the voting tools, not through harmful attacks. Do not use proposals to provoke unwanted legal attention. Aspire to do no harm to the democratic process."
}
},
{
"id": "matrix-element",
"label": "Matrix / Element",
"supportText": "Decentralized, encrypted chat protocol for sovereignty and interoperability.",
"sections": {
"corePrinciple": "We value data sovereignty and federation, owning our infrastructure to ensure we cannot be deplatformed. This serves as our \"Sovereign Archive\" for internal governance and cross-org collaboration, replacing corporate tools for groups requiring total data ownership.",
"logisticsAdmin": "Infrastructure is self-hosted with users managing their own encryption keys. Bridges connect to other networks. Culturally, this is an asynchronous space, so users should not expect immediate replies. Norms include verifying your own devices to prevent encryption errors and using the \"Reply\" function heavily.",
"codeOfConduct": "Users must not post content that attracts unwanted legal scrutiny to the server or its operators. We share a core belief that the world can be improved by collective action, so we do not tolerate behavior that sabotages that goal. We have zero tolerance for hate speech or harassment. Aspire to do no harm. While we value free expression, doxxing or targeted abuse will result in a ban from our room."
}
},
{
"id": "github-gitlab",
"label": "GitHub / GitLab",
"supportText": "Version control platforms for code collaboration and issue tracking.",
"sections": {
"corePrinciple": "We view \"code as law,\" believing the history of a decision is as important as the outcome. This platform serves as our \"System of Record\" for code changes, text-based bylaw amendments, and technical governance where an immutable audit trail is required.",
"logisticsAdmin": "Access is strictly managed via repository permissions and branch protection rules. History is immutable. Culturally, users are expected to write descriptive commit messages and use Pull Request templates to explain the \"why.\" Reviews should be professional and constructive, focusing on the technical merits rather than the personality of the author.",
"codeOfConduct": "Avoid rude, inconsiderate, or harmful behaviors in code reviews. We do not tolerate the exposure of private identities or the insertion of malicious code. We aspire to operate within these principles and maintain this repo as a space where hate isn't welcome. Do not upload proprietary IP or content that attracts unwanted legal attention to the project. Willfully spreading false technical information to sabotage the project is grounds for a ban."
}
},
{
"id": "discord",
"label": "Discord",
"supportText": "Real-time chat and voice server organized by topic channels.",
"sections": {
"corePrinciple": "We create a vibrant, persistent community hub for presence and social connection. It functions as the \"Community Living Room\" for general socializing and working group coordination, feeding into more formal tools like Loomio for decisions.",
"logisticsAdmin": "Access is hierarchically managed via Roles and server ownership is tied to an organizational account. Bots automate logging. Norms dictate that users check Pinned Messages before asking questions, keep topics in their designated channels, and use \"Threads\" for deep dives. Voice channels are casual drop-in spaces.",
"codeOfConduct": "We have a zero-tolerance policy for racism, sexism, and bigotry. We aspire to do no harm. Avoid rude or inconsiderate behavior. Do not post content that attracts unwanted legal attention to the server, such as pirated material or illicit threats. Exposing private identities is prohibited. Maintaining a space where hate isn't welcome is everyone's responsibility, not just moderators'."
}
},
{
"id": "email-distribution-list",
"label": "Email Distribution List",
"supportText": "Asynchronous announcements and formal threading via email.",
"sections": {
"corePrinciple": "We prioritize universality and reliability, using the lowest common denominator tool to ensure everyone is included. This functions as our \"Broadcast\" layer for official announcements and legal notices, ensuring members off chat apps still receive critical information.",
"logisticsAdmin": "Lists are split into \"Announce\" and \"Discuss.\" Admins approve subscriptions manually. Culturally, users are expected to respect the inbox. Reply directly to the sender for personal matters, avoid \"Reply All\" storms, update the Subject Line if the topic shifts, and format emails clearly.",
"codeOfConduct": "We aspire to do no harm in our communications. Using the list to spread obviously false information, racist tropes, or to intentionally target members for harassment is prohibited. Do not use this list to distribute content that attracts unwanted legal attention to the organization. We don't need to see eye to eye, but rude behaviors that degrade our collective dialogue will result in moderation."
} }
}, },
{ {
"id": "slack", "id": "slack",
"label": "Slack", "label": "Slack",
"supportText": "Structured workplace chat for teams and organizations.", "supportText": "We coordinate our work in Slack channels, and move decisions to our records because the free plan hides and deletes old messages.",
"sections": { "sections": {
"corePrinciple": "We focus on structured productivity, treating communication like a digital office to maximize output. This is our \"Operations\" layer used for daily project management and file sharing, distinct from the governance or social layers.", "corePrinciple": "Slack suits work that runs on channels, threads and shared files, and many members already know it from their jobs. It is a workplace tool, not a private one. Slack is not end-to-end encrypted, so the company holds everything we post and can be made to hand it over. We use it to coordinate the work, never for anything about one person or that could put a member at risk. It breaks when it becomes our only record or when members are expected to answer at all hours.",
"logisticsAdmin": "The workspace is paid to retain history. Admins provision accounts for staff and guests. Culturally, users should use public channels by default to share knowledge, strictly observe \"Do Not Disturb\" hours to respect work-life balance, and use \"Threads\" to prevent channel flooding.", "logisticsAdmin": "As of 2026 a free Slack workspace shows only the last 90 days of messages and files, and since August 2024 Slack has deleted anything older than a year from free workspaces. So decisions and how-tos go to our records the week they are made. Under a legal hold, an admin exports the channels it covers before Slack deletes anything. The workspace is owned by a group account with at least two admins. Partners get only the channels we share with them, as guests where our plan allows. Channels are open to every member by default so knowledge is shared, and private channels hold only what needs them.",
"codeOfConduct": "Intentionally harming members via bullying, sexual harassment, or retaliation is prohibited. We maintain a zero-tolerance policy for bigotry in both public channels and DMs. Aspire to be professional. Do not use company channels for content that attracts unwanted legal attention or liability. We believe collective action improves the world, and toxic behavior undermines that work." "codeOfConduct": "Reply in threads so channels stay readable. Use scheduled sending instead of messaging people late, and nobody is expected to answer outside their working hours or agreed shifts. Direct messages follow the same norms as channels. Nobody pastes passwords, personal details or anything about one person’s pay or health into Slack. Apps and bots are added only by admins after a second member agrees."
}
},
{
"id": "matrix-element",
"label": "Matrix / Element",
"supportText": "We chat on Matrix through the Element app, on a server we choose, so no single company owns our conversations.",
"sections": {
"corePrinciple": "Matrix is an open protocol, like email, so we can change servers, talk with groups on other servers and leave without losing each other. We use it for day-to-day chat and for rooms shared with other groups. Running a server is labor, so we pay a host or use one run by a group we trust. Encryption hides what we write, not who is in a room, when they post or the room’s name, which every server in the room can see. It breaks when members lose their recovery key.",
"logisticsAdmin": "As of 2026 the matrix.org server limits uploads for free accounts, so our server comes from a host on the list at matrix.org or from a group we trust. Our rooms sit in one space with at least two administrators. Private rooms have encryption on and join by invitation only. Members save their recovery key in a password manager the day they join. Bridges to other apps stay out of private rooms, because a bridge must decrypt messages to pass them on, so whoever runs it can read them.",
"codeOfConduct": "Use threads and mention people only when you need them, since members on other servers may see notifications late. Never invite a bridge or a bot into a private room. Before trusting a member’s new device in a sensitive room, confirm it with them in person or on a call. Anything said in a public room can be copied by every server that joins it, so treat it as public."
}
},
{
"id": "discord",
"label": "Discord",
"supportText": "We run a Discord server for open chat, voice rooms and working groups, and keep anything sensitive off it.",
"sections": {
"corePrinciple": "Discord gives a spread-out group a living room, with channels, threads, drop-in voice rooms and roles that help newcomers find their way. It is good for presence and open work. As of 2026 Discord encrypts voice and video calls end to end but not text, so the company holds everything we write. We use it for open work and social time, never for anything about one person or that could put a member at risk. It breaks when one person owns the server and walks away.",
"logisticsAdmin": "The server is owned by a group account, never a member’s personal account, and at least two members hold administrator rights with two-factor authentication on. New accounts start in a welcome channel that explains how to join until a member gives them a role. Roles open the channels each member works in. No channel is marked age-restricted and we meet in voice channels, not stage channels, so nobody must show Discord a face scan or ID. Bots are added only when two admins agree, with the fewest permissions they need. Decisions made on Discord are copied to our records, because one hacked admin account can wipe a server.",
"codeOfConduct": "Check pinned messages and the channel topic before asking, and use threads for long discussions. Moderators use a timeout before anything stronger and log each action with its reason in the moderators’ channel. Never click a link in a direct message that asks you to log in or claims to come from Discord or an admin, since our admins never message first."
}
},
{
"id": "shared-records",
"label": "Shared records",
"supportText": "We keep our Rule, decisions, minutes and how-tos in one place every member can find, with a steward who keeps it current.",
"sections": {
"corePrinciple": "Knowledge that lives in one person’s head leaves with them, and whoever remembers holds power over everyone who has to ask. So we write down how things work and why we decided what we did, and keep it in one place the group controls. These are our records, the place this Rule means whenever it says so. They hold the how and the why, never details about people. This breaks when the group chat becomes the archive or nobody keeps the records current.",
"logisticsAdmin": "Our records are one shared folder or wiki with an index on its first page, and, where we share a space, a printed copy in a binder there. They hold this Rule, a decision log with one dated line for each decision that is hard to undo or touches others, minutes, notes for each role and how-tos for the work. The group names a steward for one year to keep them tidy and current. At least two members can open and change every part. Twice a year the steward prints what changed for the binder. Once a year we check that the rest of us could open everything if the steward disappeared tomorrow.",
"codeOfConduct": "Our records never hold addresses, phone numbers, the roster or anyone’s private situation, which stay with the few members who need them. Members fix what is wrong in the records rather than working around it, and do not copy records into chats or personal accounts. Any member can ask the steward to correct or add something, and disputes about what was decided go to the next meeting. If the steward falls behind two months running, the members name another. A copy stored with a company is never our only copy."
}
},
{
"id": "code-of-conduct",
"label": "Code of conduct",
"supportText": "One code covers how we treat each other in every channel, meeting and space, with a clear way to report problems.",
"sections": {
"corePrinciple": "We can disagree hard and still work together, so long as we treat each other with care. One code applies everywhere we meet, in person, on calls and in every app, so nobody has to learn different rules for each tool. Rules for a particular tool add to this code and never replace it. It protects members and the people we serve, says what we expect, how to raise a problem and what happens next. It breaks when nobody acts on it, or when it is used to silence disagreement.",
"logisticsAdmin": "Anyone can raise a problem with a member they trust or with two members named each year to receive reports. Reports stay with the people who need to know. Any admin or moderator can hide a post or pause someone from a channel for up to 24 hours to stop harm, and tells a report receiver the same day. Anything more goes to our conflict process, and a group that employs anyone keeps written harassment and discrimination procedures. Threats, harassment, abuse and discrimination go straight to its formal steps. If anyone is in immediate danger we get help from outside the group. Nobody loses their place, shifts or pay for raising a concern.",
"codeOfConduct": "Treat everyone with respect and argue with ideas, not people. We do not tolerate harassment, threats or discrimination based on race, ethnicity, national origin, gender, sexuality, disability, religion, age, class or immigration status. Never share anyone’s name, number, photo, address or story without consent, or post the roster or where members meet in any shared or public channel. Never knowingly spread false information. Anyone can ask to move a heated written conversation to a call or meeting, and it pauses until then. When told you caused harm, listen first."
}
},
{
"id": "records-about-people",
"label": "Records about people",
"supportText": "We collect only what the work needs about people, keep it locked away and delete it on a set date.",
"sections": {
"corePrinciple": "The safest record about a person is the one we never made. Names, addresses, needs and stories help us do the work, and they can hurt people if they leak, are seized or outlast their use. So we ask only for what the work needs now, keep it with the fewest members who need it and give everything a date to be deleted. This covers aid requests, intake notes, sign-in sheets, delivery lists and staff files. It pulls against convenience, since some things get asked twice. Keeping no records about the people we serve is a choice this card allows. A group that makes it writes We keep no records about the people we serve in place of the steps, and no member keeps lists, notes or photos of them on any device or in any chat.",
"logisticsAdmin": "The group names two members to hold these records for one year. A request form asks only for a first name or nickname, one way to reach the person and what they need, and says when it will be deleted. Requests that come in any other way go into one sheet only the holders can open, never shared by link, and the original is deleted. Where we must follow someone over time, each request gets a number and only the two holders can join numbers to names. Paper is shredded on its date and never left in a car overnight. Records the law requires, such as payroll and tax files, stay locked for as long as the law says.",
"codeOfConduct": "Nobody asks about immigration status, income or someone’s story unless the work cannot be done without it, even for a funder, and anyone can leave a question blank. Members never photograph, forward or copy records about people into chats or personal accounts. The holders confirm each deletion at the next meeting and pause it for anything under a legal hold. A leak is reported to the holders at once, and the people affected hear about it from us plainly and soon."
}
},
{
"id": "accounts-and-keys",
"label": "Accounts and keys",
"supportText": "We keep a private list of every account, login, domain and key, who holds each one and when we last checked.",
"sections": {
"corePrinciple": "Whoever sets up an account usually ends up owning it, so one tired person can lock the rest of us out, by accident or on purpose. Every account, login, domain, door code and physical key belongs to the group, not to the person who holds it. Admin rights are a job the group hands out with a term, not a reward. This covers email, chat, files, our domain, social and payment apps and the keys to our spaces. Bank accounts and wallets follow our rules for shared money.",
"logisticsAdmin": "Two members, or a founder and someone they trust, keep an access map, a private list of every account and key that shows which roles hold it, whether two-factor authentication is on, where the recovery codes are and when it was last checked. It names roles, not people, and lives in our password manager or a locked drawer, never in shared text. Group accounts use a group email address, and our domain is registered in the group’s name with privacy on and no member’s home address. Admin rights last one year. Once a year the keepers check the map with the members, close access nobody needs and confirm who could get in if a holder could not.",
"codeOfConduct": "Nobody shares a login in a chat, an email or a document, and nobody creates an account for the group with their personal email. Two-factor authentication is on wherever offered, and at least two members can pass it. When anyone leaves a role or the group, the keepers update the map that week, for founders as much as anyone. Any member can ask who holds an account and get an answer. A holder who loses a phone, laptop or key tells the keepers the same day, without blame."
}
},
{
"id": "email-distribution-list",
"label": "Email distribution list",
"supportText": "We send announcements and formal notices by email, so members who are not on our chat app still hear everything.",
"sections": {
"corePrinciple": "Email works on any device and with any provider, and no single company controls it. It is our slowest channel and the one that reaches the most people, including members without smartphones and those who will not join a chat app. Formal notices, such as a call to a general meeting, go here so every member has them in writing. A founder can start the list before anyone else joins. Email is rarely end-to-end encrypted, so it never carries details about one person or where members meet.",
"logisticsAdmin": "We keep two lists. The announce list carries every announcement, and if we have no announcements channel it is where every member will see it. Only two members the group names for a year can send to it, and they send any member’s proposal within a day. The discussion list is open to every member for longer conversations. Two list keepers named for a year add members when they join and remove them when they leave. The lists run on a service in the group’s name, never one member’s personal account, and the keepers keep our own copy of the addresses. Until we have a list, one member sends with every address in the blind copy line.",
"codeOfConduct": "Subject lines say what a message is about and any deadline, and change when the topic does. Reply to the sender for anything personal and to the whole list only when everyone needs it. Never forward a list message outside the group without asking its author, and never put members’ email addresses in the visible to or copy lines. Messages about one person, money worries or conflicts go to the people concerned, not to the list."
}
},
{
"id": "github-gitlab",
"label": "GitHub / GitLab",
"supportText": "Our code, this Rule and our decisions live in a repository, where every change has a reason and a reviewer.",
"sections": {
"corePrinciple": "In an open project the repository is where the work and its history already live, so we keep our records there too. Changes to this Rule are reviewed like code, so members can see who changed what and why, and merge only once the method our Rule names for changing it has passed. This Rule and our decision log sit in a public repository only if the group agrees. This covers code, this Rule, proposals and decisions about the project. A repository’s history keeps whatever was ever committed, so it is the wrong place for anything private about people.",
"logisticsAdmin": "The repository holds a decision log, a code of conduct and a guide for new contributors, and this Rule if we publish it. A proposal is an issue or pull request, linked where every member will see it, with a deadline in its first lines, and the result is written in the thread before it closes. Once we have two maintainers, every change to the main branch needs a review from someone other than its author, and a request for changes holds it until resolved or the group decides. At least two maintainers hold owner rights on the organization, package registries and domain, and everyone with write access uses two-factor authentication. We keep a full copy outside the platform, since any platform can lock an account.",
"codeOfConduct": "Reviews are about the change, never the person who wrote it, and every rejection comes with a reason. Nobody commits names, addresses, keys or passwords, and anything committed by mistake is treated as leaked and changed at once. A thread that turns heated is locked and moved to a call. Reports about behavior go privately to two members named to receive them, never into a public issue. Security problems are reported privately and fixed before they are discussed in the open."
}
},
{
"id": "front-door",
"label": "Front door",
"supportText": "People who need us or want to join can always find us through one public contact that two members answer.",
"sections": {
"corePrinciple": "Links rot, pages go stale and a founder’s personal number stops working, and people who need us find a dark room. So we keep one front door that does not depend on any one member, a phone number and email address in the group’s name and one public page that says who we are and how to reach us. It keeps members’ own names and numbers off public posts. This breaks when nobody checks it or when messages that come in go unanswered.",
"logisticsAdmin": "Our front door is a phone number and an email address in the group’s name, given on one public page or pinned post. Two members, or the founder until a second member joins, answer it for three months at a time and reply within two days. They pass each request to the right working group one to one, never in a group chat, and delete the original message once it is passed on. Legal papers, lawyers’ letters and official demands are kept, never deleted, and go to the members who handle legal matters the same day. Once a month one of them checks that the page is current, every link works and the number rings. When we change tools, the old page stays up with one post that says where we went.",
"codeOfConduct": "Whoever answers speaks for the front door, not for themselves, and never gives out a member’s personal number or says where anyone lives or meets. Replies say plainly what we can and cannot do. Messages that threaten or harass are saved, passed to the members who handle safety and never answered alone. The public page never shows where members meet, and it changes only when the two members who answer agree."
}
},
{
"id": "in-person-meetings",
"label": "In-person meetings",
"supportText": "We meet face to face for our regular meetings, our biggest decisions and hard conversations, in places every member can reach.",
"sections": {
"corePrinciple": "For a group that lives in one area, meeting in person is where trust is built, the biggest decisions are made and hard conversations go best. So we meet face to face whenever we can, not only once a year. This covers our regular meetings and any meeting about a conflict or a big decision. Meeting in person pulls against reach, since some members cannot travel or have no one to watch their children, so members can join by phone or video when they ask.",
"logisticsAdmin": "We meet at a set time each month in a place that is step-free, near transit and free or cheap to use. Members learn the place from another member directly, in person, by a call or a one-to-one message, and it never goes into a group channel, a public post, a flyer, a shared document or this Rule, except a notice to members the law requires. Posts for public events give only what newcomers need. The members who hold the space’s keys open and close it. Someone greets newcomers at the door and explains how the meeting works. When the place changes at short notice, the key holders tell each member directly or through calls on our phone tree.",
"codeOfConduct": "The meeting starts on time for whoever is there. Phones stay on silent and side conversations move outside. Members who feel unsafe can leave at any time without explaining, and someone checks in with them afterward. Nobody brings a guest to a member meeting without asking first. We stay a while after the meeting when we can, since much of the trust gets built there."
}
},
{
"id": "video-meetings",
"label": "Video meetings",
"supportText": "When we cannot be in one room, we meet on a call every member can join by link or by phone.",
"sections": {
"corePrinciple": "Video calls let members who are spread out, busy or unable to travel meet face to face without the cost of getting there. We use them for regular meetings of a spread-out group and for conversations that would take too long in writing. Calls tire people faster than rooms and leave out members with poor connections, so we keep them short and offer a phone line. Most video services can see who joined and when, and cloud recordings live on their servers.",
"logisticsAdmin": "The call link goes out in member channels only, and calls about sensitive matters use a waiting room or a password, with the host admitting only people they recognize. Hosting rotates, and a second member can take over if the host drops. Calls last an hour at most unless the group agrees to more. Members can join by phone where the service allows it, and we turn on captions whenever anyone asks. Anything about one person, or that could put members at risk, goes on an end-to-end encrypted call, such as a Signal call.",
"codeOfConduct": "Cameras are optional and nobody is asked why theirs is off. Mute when not speaking and use the raise hand button or the chat to join the queue. The chat follows the same norms as the call and is not saved unless the group agrees. Nobody records the call and no automated note taker joins unless everyone present agrees at the start, and any recording is deleted once the notes are in our records."
}
},
{
"id": "phone-and-text-tree",
"label": "Phone and text tree",
"supportText": "When news is urgent, a chain of members calls or texts everyone, including members without smartphones or data.",
"sections": {
"corePrinciple": "Not everyone is on the same app or has data every day, and urgent news cannot wait for someone to check a chat. A phone tree passes news by call or text from member to member until everyone has it, and confirms that they did. We use it only for urgent news, such as a canceled shift, a change of place or a safety warning. It breaks when the list goes stale or a caller is away and nobody notices.",
"logisticsAdmin": "Two members keep the tree for one year. It holds only a first name or nickname and one number for each member, and lives on paper and in an encrypted note, never in a shared document or chat. Each caller has three to five people to reach and a backup caller. Anyone with urgent news tells a keeper, who starts the tree. Each caller reaches their people within an hour, leaves a short message when there is no answer and tells the keeper who was not reached. The keepers test the tree every three months and update it after each test.",
"codeOfConduct": "Messages are short, say what happened and what to do, and never name anyone’s situation or say where anyone lives. Text messages may not be encrypted, so a new meeting place or anything sensitive goes by call or a one-to-one encrypted message instead. Nobody uses the tree’s numbers for anything else. A member can leave the tree or change their number at any time by telling a keeper."
}
},
{
"id": "deciding-in-threads",
"label": "Deciding in threads",
"supportText": "Between meetings we post proposals in writing with a deadline, show consent with a reaction and move long threads to a meeting.",
"sections": {
"corePrinciple": "Most decisions are too small to wait for a meeting, but a chat thread makes a poor meeting. So when we decide in writing, we post a clear proposal with a deadline, make it easy to agree or object and record the result where we can find it. This covers proposals in any chat, email list or forum between meetings. It pulls against quick replies, and it breaks when a thread runs on without a proposal or a proposal never reaches members who are not on that app.",
"logisticsAdmin": "A proposal starts with the word Proposal and says what we would do, who does it, the deadline and which method our Rule uses to decide it. It is announced where every member will see it, with a link to the thread for replies, or posted in a working group’s channel for that group’s own work. Members who agree can say so with one agreed reaction, and concerns go in replies. When the deadline passes, the proposer posts the result, pins it and adds a line to our records. A thread that passes thirty messages without a decision is a meeting trying to happen, so the proposer schedules a call or takes it to the next meeting.",
"codeOfConduct": "Keep replies about the proposal and take side talk elsewhere. Say what you would change, not only that you disagree. Nobody is pressed to react before the deadline, and silence counts as agreement only where the method our Rule names says so. Do not edit a proposal after people have reacted. Post a new version and restart the deadline."
}
},
{
"id": "loomio",
"label": "Loomio",
"supportText": "We discuss and decide proposals in Loomio between meetings, so members who cannot attend still have their say.",
"sections": {
"corePrinciple": "Not every member can come to every meeting, and a decision made by whoever showed up shuts the rest out. Loomio, built by a worker cooperative, keeps a discussion and its proposal together, shows who has responded and closes on a set date. We use it for proposals that need every member’s view, between meetings or instead of them, and it keeps each decision with its reasons. It pulls against speed, and it breaks when members stop checking it and a few voices decide.",
"logisticsAdmin": "As of 2026 Loomio offers a 14-day free trial for up to 10 people and paid plans after that, with a discount for nonprofits, and its open source code can be self-hosted. Our group is invite-only and two admins add and remove members. Each proposal names the method our Rule uses for that kind of decision, its deadline and who will act on it. We set it to email every member when a proposal opens and a day before it closes. The outcome is posted where every member will see it and logged in our records.",
"codeOfConduct": "Read the proposal and the discussion before you respond. If you object, say what harm you see and what would resolve it. Choose abstain when you have no view, so others can see you read it. Keep the discussion in the thread rather than in side chats. Loomio keeps what we post on its servers, like most web tools, so a proposal never names a person’s aid, health or pay, which go to the smallest circle that can decide them."
} }
}, },
{ {
"id": "whatsapp", "id": "whatsapp",
"label": "WhatsApp", "label": "WhatsApp",
"supportText": "Ubiquitous mobile chat for quick, informal coordination.", "supportText": "Where WhatsApp is the app everyone already has, we coordinate there with numbers kept private and sensitive work kept elsewhere.",
"sections": { "sections": {
"corePrinciple": "We lower the barrier to entry, meeting people where they are with a tool that requires zero training. This acts as the \"Field\" layer for neighborhood pods, urgent mutual aid, specifically for logistics that require immediate mobile notifications.", "corePrinciple": "WhatsApp is on almost every phone, so it reaches members who will never install another app. It encrypts what we write end to end, but Meta, the company that runs it, still keeps who is in each group and who talks to whom and when. So we use it for everyday coordination and news, and we keep anything about one person, money worries or plans that could put members at risk in Signal or on a call. It breaks when the group chat turns into our roster.",
"logisticsAdmin": "Organized as a \"Community\" with nested groups. Safety relies on vetting members before adding them as phone numbers are exposed. Norms include strict \"Quiet Hours\", keeping the main feed clear for logistics, and moving social chatter to \"Off-topic\" groups.", "logisticsAdmin": "We link our groups under one parent group with an announcement group that only admins post in, and a small group for each working group or area. Two to four admins approve everyone who joins and remove members who leave. Invite links are shared one to one, never posted in public, and reset if they spread. Members turn on encrypted backups. As of 2026 WhatsApp is still rolling out usernames, and members of an ordinary group can see each other’s phone numbers, so we tell new members that before we add them.",
"codeOfConduct": "Harvesting members' phone numbers for harassment is a severe violation. Willfully spreading misleading information that endangers the community is grounds for removal. We aspire to do no harm. Racism, sexism, and abusive language are never acceptable. Do not post content that attracts unwanted legal attention. Maintaining a safe group is our collective responsibility." "codeOfConduct": "Keep the announcement group for news and the small groups for the work. Never add someone to a group without asking them first, since adding them shows their number to everyone in it. In any group that discusses plans or people, turn on disappearing messages and Advanced Chat Privacy, and turn off sharing recent messages with new members. Turn disappearing messages off in any chat under a legal hold. Never post the roster, addresses, where members meet or anyone’s situation in a group, whatever its size."
} }
}, },
{ {
"id": "discourse-forum", "id": "chat-channels",
"label": "Discourse (Forum)", "label": "Chat channels",
"supportText": "Long-form, threaded discussion board for deep asynchronous conversation.", "supportText": "Whatever app we use, we keep a few named channels, keep shift channels clear for the work and set quiet hours.",
"sections": { "sections": {
"corePrinciple": "We value depth and deliberation, moving complex discussions away from chat to ensure they are thoughtful. This functions as our \"Library\" layer for policy development and proposal shaping, where rough ideas are refined before going to a vote.", "corePrinciple": "A chat that does everything buries the message that matters. So whatever app we use, we split our chat into a few channels with clear purposes and agree how to use them. Clear channels let someone on a shift find what they need at a glance, and let members take part without reading everything. This covers every group chat and channel we run. It breaks when channels multiply faster than anyone can follow, or when the real conversation moves to private messages.",
"logisticsAdmin": "Accounts are tied to a membership database. Trust Levels unlock features automatically. Culturally, users are expected to search before posting, write descriptive titles, and cite sources. Emotional argumentation is discouraged in favor of logic. Off-topic replies are moved by moderators.", "logisticsAdmin": "Official channels share a prefix, such as one emoji, so members can tell them from side chats. We keep one channel for announcements, one for general chat, one for each working group, one for each shift or action and one for social chat. A shift or action channel carries only what people need to do the work and is closed when the work ends. Any working group can open a channel and names a member to look after it. Twice a year we close the channels nobody uses.",
"codeOfConduct": "We aspire to minimize avoidant strategies like content warnings, trusting members to engage with reality constructively. However, intentional harm, rude behavior, and personal attacks are prohibited. Zero tolerance for racism, sexism, and bigotry. Do not post content that attracts unwanted legal attention. Constructive disagreement is welcome. Willfully false information is not." "codeOfConduct": "Post in the channel that fits and take off-topic talk to the social channel. Acknowledge a message with a reaction instead of a reply that pings everyone. Join announcements and the channels where you work. Ask the sender before forwarding a message outside its channel, unless you are reporting harm. During quiet hours, from ten at night to seven in the morning, nobody expects a reply and only urgent safety news goes out, except on shifts. Decisions made in chat go to our records, since chat is where memory starts."
}
},
{
"id": "field-communication",
"label": "Field communication",
"supportText": "During shifts and actions we use one clear channel, set check-in times and an off-site contact, so nobody goes missing unnoticed.",
"sections": {
"corePrinciple": "In the field, a message lost in chatter can be a call for help that nobody sees. During shifts, distributions and actions we keep one channel for the work, check in at set times and keep one member off site who acts if someone goes quiet. This covers any time members work apart from each other or in risky places, and it works alongside any on-site lead our Rule names. It breaks when the channel fills with chatter or nobody notices a missed check-in.",
"logisticsAdmin": "Before each shift, the member leading it opens a channel or radio channel for it and shares the check-in times. Everyone on shift has a buddy. One member stays off site as the contact and holds an emergency contact for each person on shift, given only if they choose. If a check-in is fifteen minutes late, the contact calls the person, then their buddy, then the member leading the shift, and gets help from outside the group if anyone may be in danger. Only members who have practiced with radios use them. The channel is cleared or closed when the shift ends.",
"codeOfConduct": "The shift channel carries only what people need to act, such as where, when, who and what is needed. Social chat and debates wait until after the shift. Nobody posts photos, faces, plates or anyone’s home address, and where the shift works goes only to the people on it. Legal names and health details go only face to face, by phone or to the off-site contact, never over radio, which scanners can hear. Members on shift keep their phones charged, locked and on."
}
},
{
"id": "password-manager",
"label": "Password manager",
"supportText": "Every group login lives in a shared password vault with long random passwords, two-factor codes and two vault admins.",
"sections": {
"corePrinciple": "Passwords kept in someone’s head, a browser or a sticky note leave with that person or leak with their laptop. A shared password manager keeps every group login in one encrypted vault, so the people who need a login can use it without anyone having to remember it. It also refuses to fill a password into a fake site, which stops many scams. The vault is the one small hierarchy worth keeping, because we can check it. It breaks when the only vault admin disappears.",
"logisticsAdmin": "We use a password manager that encrypts the vault end to end, such as Bitwarden, which as of 2026 offers a free organization for two people and paid plans for more. Every group login moves into it with a long random password, two-factor authentication on and its recovery codes stored beside it. Two members are vault owners with full rights, and each keeps the recovery code for their own vault login on paper, outside the vault. Members see only the logins their roles need. When someone leaves a role, their vault access is removed and the passwords they could see are changed that week.",
"codeOfConduct": "Nobody copies a group password out of the vault into a chat, a document or a browser. Members lock the vault when they step away and use a strong master password they use nowhere else. A member who thinks a password has leaked tells a vault admin at once, and it is changed without blame. Vault admins open only what their own work needs."
}
},
{
"id": "security-plan",
"label": "Security plan",
"supportText": "Once a year we ask what we hold and who might want it, and we plan for a bad day.",
"sections": {
"corePrinciple": "Every group has a security policy, written or not, and the unwritten one is usually whatever the person who set things up did. So once a year we decide together what to protect and from whom, before a crisis decides for us. Most trouble is ordinary, such as a lost phone, a scam, a former member with a password or harassment, and some groups also face searches or subpoenas. This covers our devices, accounts and records. It pulls against ease, and a practice nobody follows protects nobody.",
"logisticsAdmin": "Once a year, for about 45 minutes, we answer four questions and date the answers in our records. What do we hold? Who might want it? What would it cost the people involved if they got it? What will we actually keep doing? Every member’s phone has a passcode of at least six digits and current updates, and members at actions know how to turn off face and fingerprint unlock quickly. We check any request for money or passwords by calling the person on a different channel. We write down who we call on a bad day, such as a lawyer and a digital security helpline.",
"codeOfConduct": "When something goes wrong we contain it, tell people and learn. We change passwords, sign out of every device and remove the lost device or account. The people whose information was touched hear it from us plainly and soon. We change the practice rather than blame the person, and a member who reports a mistake within the hour has helped us. Nobody hands over records or devices voluntarily before a lawyer has read the paperwork, since a request is not an order, and anything it touches goes on a legal hold."
}
},
{
"id": "speaking-for-the-group",
"label": "Speaking for the group",
"supportText": "Named members speak for the group to the press and online, and nobody shares photos or details of people without consent.",
"sections": {
"corePrinciple": "What one member says in public can be taken as our position, and one photo can put a member or a neighbor at risk. So a few members we name speak for the group, and everyone else speaks only for themselves. This covers the press, our social media accounts, public statements and filming at our events. Harassment usually starts with a name or a face, so we keep members’ legal names, faces and home areas out of what we publish. It breaks when anyone answers for the group.",
"logisticsAdmin": "The group names two spokespeople for one year and lists the accounts that post for us. A statement that takes a new position is a decision of the whole group, and routine posts such as event times need no approval. Photos at our events are taken with consent, from behind or with faces covered by a solid block and location data removed, and never of people receiving aid. When a reporter or someone filming arrives, members say they do not speak for the group and point to a spokesperson. Our posts never say where an action or a meeting is until it is safe.",
"codeOfConduct": "Speak for yourself, not for the group, unless you are a named spokesperson. Never tag, name or post a photo of a member or anyone we serve without their consent. Do not share where members live, work or meet. If asked about a conflict inside the group, say only that we have a process for it. A member harassed because of our work gets the group’s support, and we document the harassment together."
}
},
{
"id": "newsletter",
"label": "Newsletter",
"supportText": "We send a regular newsletter to supporters and neighbors who asked for it, with no tracking and an easy way out.",
"sections": {
"corePrinciple": "Social media rents us our reach and can take it away, so we keep our own list of people who want to hear from us. A newsletter tells supporters, neighbors and partners what we are doing and how to help, in our own words and on our own schedule. It is for people outside the group as well as members, and it never replaces our member announcements. A list of supporters can be leaked or seized, so it holds only email addresses.",
"logisticsAdmin": "Two members edit the newsletter and send it on a set schedule, such as monthly. People join only by signing up themselves, and every issue has a one-click way to leave. The list lives on a service in the group’s name, and the editors export our own copy each quarter. We turn off open and click tracking where the service allows it. The list is never shared, sold or used for anything else. The postal address email services require in each issue is a post office box or our fiscal sponsor’s, never a member’s home.",
"codeOfConduct": "Write about what we do, not about the people we serve, and never include anyone’s name, photo or story without their consent. Asks for money say plainly what the money is for. Each issue says who we are and how to reach our front door. Mistakes are corrected in the next issue."
}
},
{
"id": "facebook-and-instagram",
"label": "Facebook and Instagram",
"supportText": "We keep a Facebook group or page that points people to us, and organize somewhere we control.",
"sections": {
"corePrinciple": "Many neighbors look for local groups on Facebook and Instagram, so a page or group there helps people who need us find us. But Meta can read everything posted and decides who sees each post, and it can close a group without warning. So we treat it as a way in, not a home. It points people to our front door and public events, while our organizing, our list of members and our records live elsewhere. It breaks when the group lives only on Facebook or Instagram.",
"logisticsAdmin": "At least two members are admins, each with two-factor authentication on, so the group never depends on one person’s account. One pinned post says who we are, what we do and how to reach our front door, and an admin checks it each month. We point people to our page, which they can message without joining anything. Any group we keep is private, and its questions ask only what keeps out spam, never anyone’s status or situation. Requests for help that arrive as comments or messages, on Facebook or Instagram, are moved to our front door and then deleted. Legal papers and official demands are kept and go to the members who handle legal matters the same day. People who want news from us are pointed to our newsletter, never added to member channels.",
"codeOfConduct": "Never post where members meet, live or work, or anyone’s name, photo or situation without their consent. Do not argue with trolls. Hide or delete their posts and tell the other admins. Comments asking for help get a reply pointing to our front door, never a public discussion of the need. When the group is large or under attack, admins approve posts before they appear."
}
},
{
"id": "telegram",
"label": "Telegram",
"supportText": "Where members already use Telegram, we use it only for announcements and open discussion, because group chats there are not end-to-end encrypted.",
"sections": {
"corePrinciple": "Telegram is popular for large groups and broadcast channels, and many members outside the United States already use it every day. It is not a private tool. As of 2026 Telegram stores ordinary chats and every group chat on its servers without end-to-end encryption, and only secret chats between two people are end-to-end encrypted. So we treat everything posted in a Telegram group as readable by the company and by anyone who can make it hand records over. We use it for announcements and open discussion only.",
"logisticsAdmin": "Our Telegram presence is a broadcast channel for announcements and, if we need one, a discussion group linked to it. The channel is owned by an account registered to the group’s own phone number, and two to four admins hold admin rights with two-step verification on. Members set their phone number to be seen by nobody and join with a username. Coordination about people, money or plans that could put members at risk happens in Signal or on a call.",
"codeOfConduct": "Post nothing in Telegram that names, shows or locates a member or anyone we serve. Do not move a conversation into a secret chat to make it private, since secret chats cover only two people. Move it to Signal instead. Admins remove spam and impersonators quickly and pin a note naming the real admins, who never message first."
}
},
{
"id": "zulip",
"label": "Zulip",
"supportText": "We discuss in Zulip, where every message has a topic, so long conversations stay easy to follow and find later.",
"sections": {
"corePrinciple": "In most chat apps a busy channel becomes one long stream, and anyone who was away cannot catch up. Zulip gives every message a topic inside its channel, so members can read one conversation at a time and return to it months later. It is open source and exports in full, so we can leave with our history. Like most workplace chat it is not end-to-end encrypted, so it never carries anything about one person or that could put members at risk.",
"logisticsAdmin": "As of 2026 Zulip Cloud’s free plan searches only the last 10,000 messages, and Zulip gives its Standard plan free to open source projects and most small nonprofits that apply. We apply before we outgrow the free plan, or we pay. The organization has at least two owners and is registered to a group email address. Each working group gets a channel, and each question or proposal gets its own topic. When a topic is settled it is marked resolved and the decision goes to our records.",
"codeOfConduct": "Start a new topic for each new question, with a name that says what it is about. Reply in the topic you are answering, and move stray messages to the right topic. Mention people only when you need them. Nobody is expected to read every topic, so anything every member must know goes where every member will see it."
} }
} }
] ]
@@ -2,13 +2,13 @@
"_comment": "Create flow — conflict management (Figma Flow — Compact Card Stack `20879:15979`)", "_comment": "Create flow — conflict management (Figma Flow — Compact Card Stack `20879:15979`)",
"page": { "page": {
"compactTitle": "How should conflicts be managed\nin your group?", "compactTitle": "How should conflicts be managed\nin your group?",
"compactDescriptionBefore": "You can also combine or ", "compactDescriptionBefore": "Start with the conflict ladder, then pick a card for serious cases, safety and repair. Combine them or ",
"compactDescriptionLinkLabel": "add", "compactDescriptionLinkLabel": "add",
"compactDescriptionAfter": " new approaches to the list", "compactDescriptionAfter": " your own.",
"expandedTitle": "How should conflicts be managed in your group?", "expandedTitle": "How should conflicts be managed in your group?",
"expandedDescriptionBefore": "You can also combine or ", "expandedDescriptionBefore": "Start with the conflict ladder, then pick a card for serious cases, safety and repair. Combine them or ",
"expandedDescriptionAfter": " new approaches to the list", "expandedDescriptionAfter": " your own.",
"seeAllLink": "See all conflict management approaches" "seeAllLink": "See all conflict cards"
}, },
"confirmModal": { "confirmModal": {
"title": "Confirm selection", "title": "Confirm selection",
@@ -16,294 +16,384 @@
"nextButtonText": "Confirm" "nextButtonText": "Confirm"
}, },
"addApproach": { "addApproach": {
"nextButtonText": "Add Approach" "nextButtonText": "Add card"
}, },
"removeApproach": { "removeApproach": {
"nextButtonText": "Remove" "nextButtonText": "Remove"
}, },
"sectionHeadings": { "sectionHeadings": {
"corePrinciple": "Core Principle", "corePrinciple": "Core principle",
"applicableScope": "Applicable Scope", "applicableScope": "Applicable scope",
"processProtocol": "Process Protocol", "processProtocol": "How it works",
"restorationFallbacks": "Restoration & Fallbacks" "restorationFallbacks": "Protections and fallbacks"
}, },
"scopeAddButtonLabel": "Add Applicable Scope", "scopeAddButtonLabel": "Add a scope",
"applicableScopePlaceholder": "Describe when and where this approach applies", "applicableScopePlaceholder": "Describe when and where this card applies",
"methods": [ "methods": [
{ {
"id": "peer-mediation", "id": "conflict-ladder",
"label": "Peer Mediation", "label": "Conflict ladder",
"supportText": "Trained members within the organization mediate disputes among peers.", "supportText": "We talk one-on-one, then each brings a friend to negotiate, and the people we name step in last, or first for safety.",
"sections": { "sections": {
"corePrinciple": "We democratize conflict skills. Instead of relying on professional outsiders, trained peers help their colleagues resolve disputes, reinforcing the idea that we take care of each other.", "corePrinciple": "We cannot end conflicts, only manage them, and distance can be a fair answer. A conflict belongs in the smallest room that can hold it, because each person added raises the price of backing down. So we add people one step at a time, unless safety, shared money or a power gap cannot wait. Friends carry informal power, so their step has written limits. Nobody loses their place, shifts or pay for raising a concern. The ladder breaks when a conflict reaches the group chat first.",
"applicableScope": [ "applicableScope": [
"Low-level friction", "Conflict between two people",
"misunderstandings", "Friction over work or tone",
"and minor grievances between peers." "Broken agreements"
], ],
"processProtocol": "A volunteer peer (who is not a manager) invites the disputants to a private chat. Using a simple script, they ask questions like 'Tell us your side,' 'Tell us what you need,' and 'What can you agree to?'. The peer keeps the conversation focused on future interactions rather than past grievances. The disputants retain full control over the resolution.", "processProtocol": "First we sort. A disagreement about what to do is a decision. Threats, violence, stalking, harassment, discrimination, abuse, fear of the other person and worries about shared money skip to step three. Someone whose pay, shifts, membership or aid the other controls can skip step one. Step one. The two talk in person or by call, agree one small change and a check-in two or three weeks out. Step two. Each picks one friend, inside or outside the group, whom the other has no reason to distrust. The friends learn what each wants and the least each could accept, meet without them and propose written terms within about two weeks. Or the two meet with one person both trust.",
"restorationFallbacks": "The goal is a verbal agreement to try a new way of interacting. If the peer mediator determines the issue is too complex or involves serious harassment, they are required to refer the case to professional Mediation or a Judicial Committee." "restorationFallbacks": "Friends keep what they hear private and never decide. We keep a short list of members willing to be a friend for anyone, and anyone without a friend picks from it. A failed or refused step moves up. Step three is two people close to neither person, from inside or outside the group, who hear both, keep the two apart if safety needs it and propose what happens next. A broken agreement moves up a step. The gap a conflict showed goes to our next meeting without the quarrel. In immediate danger, anyone gets help from outside the group first."
} }
}, },
{ {
"id": "conflict-resolution-council", "id": "precautionary-suspension",
"label": "Conflict Resolution Council", "label": "Precautionary suspension",
"supportText": "Senior members with institutional knowledge provide guidance or decisions.", "supportText": "When someone may put others at risk, we pause their access for up to 30 days while a fair process starts.",
"sections": { "sections": {
"corePrinciple": "We rely on the wisdom of experienced members to guide us back to our values. This body acts as the 'keepers of the culture,' providing high-context advice to resolve friction.", "corePrinciple": "Safety comes first and a fair process follows. When someone may put others at risk, we pause their access at once instead of waiting for a hearing. A pause is not a finding, and we say so. It protects people and what we share while the facts are gathered, and it ends on a date. It pulls against fairness to the person paused, so it is short, explained and open to review. It breaks when a pause becomes a quiet way to remove someone.",
"applicableScope": [ "applicableScope": [
"Smoldering cultural tensions or gray-area cases where the Code of Conduct isn't clearly violated but trust is eroding." "Threats, violence or abuse",
"Harassment or stalking",
"Misuse of money, keys or equipment",
"Breaking someone’s privacy"
], ],
"processProtocol": "Disputants submit a request to the standing Council, which reviews the submission and may interview witnesses to understand the cultural context. The Council deliberates privately, referencing the community's values and history, before issuing a formal 'Opinion' or recommendation. While the Council holds significant influence (approx. 50% 'weight'), their recommendation is not strictly binding; it relies on social pressure and respect for the elders to encourage adoption.", "processProtocol": "A pause is for threats, violence, stalking, harassment, abuse, being impaired on a shift, breaking someone’s privacy or misusing shared money, keys or equipment. We name two members each year who can pause someone’s roles and access to spaces, shifts, keys, accounts and chats for up to 30 days. When we have named none or one is involved, any two members not involved can. Within a day they tell the person in writing why, what is paused and until when, and tell members only what changes in the work. Our conflict process takes up the case within a week. In immediate danger, any member gets help from outside the group first and can ask a person to leave for the day.",
"restorationFallbacks": "The artifact is a formal 'Council Opinion' that clarifies how community values apply to the situation. If the parties ignore this guidance, the Council may recommend formal disciplinary action or a community vote to enforce the standard." "restorationFallbacks": "The paused person keeps aid, pay and any vote, and can give their side to two uninvolved people, who review the pause within a week. The pause ends on its date or when the case closes, and those hearing it can extend it once, by up to 30 days or until a removal vote ends. If unfounded, we tell everyone who was told. A pause never keeps anyone out of their own home. Whoever raised the concern keeps their place. If only one member is uninvolved, that member and a trusted outsider can pause someone, and anyone who feels unsafe can step back at no cost."
} }
}, },
{ {
"id": "facilitated-negotiation", "id": "graduated-responses",
"label": "Facilitated Negotiation", "label": "Graduated responses",
"supportText": "A neutral facilitator helps guide the negotiation process.", "supportText": "When someone breaks our agreements or causes harm, our response starts small and grows only if the behavior goes on.",
"sections": { "sections": {
"corePrinciple": "Neutral support helps parties navigate emotional barriers to reach their own agreement. We recognize that sometimes communication breaks down not because of the issue, but because of the dynamic between people.", "corePrinciple": "Most people change when asked plainly, and a response bigger than the harm drives people away and makes the next report less likely. So our responses start small, address behavior rather than character and grow only when the behavior goes on, though serious harm can start higher. Every response says what has to change and by when. This pulls against the wish for a quick punishment, and it breaks when steps are skipped for people we dislike and waived for friends.",
"applicableScope": [ "applicableScope": [
"Heated interpersonal disputes where direct communication has failed but the parties are still willing to talk." "Broken agreements",
"Harm to people or shared things",
"Discipline where we employ people"
], ],
"processProtocol": "A neutral facilitator joins the discussion to set strict ground rules, such as 'no interrupting' and 'speak from I'. The facilitator manages the emotional temperature while parties take turns sharing their perspectives. By reframing toxic language—turning accusations like 'You're a liar' into statements of impact like 'I feel untrusted'—the facilitator helps the disputants focus on the substantive issues. The parties themselves retain the power to co-create and finalize the solution.", "processProtocol": "Two people close to neither person, members or not, propose a response, starting from the smallest that will work. In order, a private conversation about what must change, a written agreement to repair the harm, such as an apology or making good a loss, boundaries such as different shifts or no contact, a written warning, stepping back from a role, keys or money for a set time, time away from our work and spaces for up to three months, and removal. The person a concern is about hears it in writing and can answer before any step beyond a conversation. Each response is written down with what must change and a review date.",
"restorationFallbacks": "The process concludes with a signed or verbal Memorandum of Understanding outlining the agreed-upon behaviors. If this fails to hold, the next step is often formal Mediation or Non-Binding Arbitration to bring in more structured guidance." "restorationFallbacks": "A response takes effect when the person accepts it or as a decision of the whole group. Each response ends on its review date unless the behavior goes on. Removal needs our supermajority share of the active members who vote, if our quorum votes, after the person has been heard. Where we employ people, these are also our written discipline steps and follow employment law. No response cuts off aid."
} }
}, },
{ {
"id": "ad-hoc-arbitration", "id": "conflict-working-group",
"label": "Ad Hoc Arbitration", "label": "Conflict working group",
"supportText": "Arbitrators are chosen specifically for a particular case.", "supportText": "Two to five members named in advance hear what the first steps could not settle and propose a plan the members approve.",
"sections": { "sections": {
"corePrinciple": "We value speed and specific expertise. For technical or niche disputes, we appoint a temporary judge who knows the subject matter better than a general tribunal.", "corePrinciple": "Serious conflicts need people with time and distance, chosen before anyone needs them. No small group should hold the power to punish, so our working group gathers facts and proposes, and the people involved or the members decide. Its members act as facilitators and fact-finders, never judges. Members see only a short summary, because a case is about real people. It pulls against speed, and it breaks when the same few people hear every case for years.",
"applicableScope": [ "applicableScope": [
"Technical disputes (e.g.", "When the first steps fail",
"code architecture)", "Safety and money concerns",
"artistic differences", "Conflicts across a power gap",
"or specific one-off grievances." "Complaints from outside the group"
], ],
"processProtocol": "Parties agree that they are deadlocked and mutually select a single expert they both trust to act as the arbitrator. They sign an agreement beforehand to abide by this person's decision, effectively handing over 100% of the decision power. Each side presents their technical arguments, and the arbitrator reviews the evidence to issue a final ruling on the specific issue.", "processProtocol": "At a meeting we name two to five members for one year and tell every member, and the people we work with, how to reach them. They hear what the first steps could not settle. When a case comes in, two of them close to neither person meet each person separately within a week, with a friend present if wanted. In a safety case they keep the two apart first, at no cost to whoever raised it. They write down what happened, what is disputed and a plan of specific changes, boundaries, repair or agreed rest leave, with a review date. If both people accept the plan, it takes effect and members hear only what changes in the work.",
"restorationFallbacks": "The arbitrator's ruling is final and binding for that specific issue, allowing work to proceed. Importantly, this specific ruling does not create a binding precedent for future, unrelated conflicts." "restorationFallbacks": "Otherwise members get a summary without private details, and the plan takes effect after 15 days unless a member raises a concern. A standing concern sends the plan to a vote, where it needs our supermajority share of the active members who vote, if our quorum votes. The people involved can answer in writing and neither vote nor count. A failed plan is revised within two weeks, with any separation kept meanwhile. Removal needs our supermajority share of the active members who vote, if our quorum votes, after the person has been heard. It never cuts anyone off from aid. Notes are deleted two years after a case closes."
}
},
{
"id": "conflict-workshops",
"label": "Conflict Workshops",
"supportText": "Structured sessions where parties collaboratively resolve disputes and improve future interactions.",
"sections": {
"corePrinciple": "We view conflict competence as a skill to be learned. By practicing in a low-stakes environment, we immunize the community against toxic fighting when high-stakes issues arise.",
"applicableScope": [
"Preventative care during 'peacetime' or as a mandatory reset when the community vibe feels toxic."
],
"processProtocol": "A trainer assesses the community's conflict style and gathers the group for a structured session. Participants engage in role-playing exercises, such as 'The Angry Neighbor', to practice active listening and Non-Violent Communication (NVC). The group debriefs what worked and commits to using new tools. This is a capacity-building exercise where no binding decisions are made.",
"restorationFallbacks": "The outcome is increased capacity and a shared vocabulary for handling tension. While there is no ruling, a member's refusal to participate in mandatory workshops may be documented as a lack of commitment to the group's health."
}
},
{
"id": "supermajority-vote",
"label": "Supermajority Vote",
"supportText": "Members vote to resolve a dispute democratically.",
"sections": {
"corePrinciple": "We use the weight of the community to resolve binary impasses. When a decision must be made and consensus is impossible, a decisive vote allows the group to move on.",
"applicableScope": [
"Final deadlock on policy decisions or removal of a member after other methods have failed."
],
"processProtocol": "The conflict is crystallized into a clear, binary proposal (e.g., 'Should we remove member X?'). A debate period allows arguments for and against to be presented. A vote is then taken, typically via secret ballot to protect relationships. For the decision to be binding, the 'Yes' votes must exceed a high threshold—usually 75% (or 2/3rds, depending on bylaws)—effectively transferring 100% of the decision power to the voting body.",
"restorationFallbacks": "This produces a binding, final decision. The minority is expected to 'disagree and commit' to the result. Continued resistance or sabotage after a vote is considered a violation of community norms and is often grounds for leaving the group."
}
},
{
"id": "interest-based-bargaining",
"label": "Interest-Based Bargaining",
"supportText": "Focuses on underlying interests rather than fixed positions to find win-win solutions.",
"sections": {
"corePrinciple": "We separate the people from the problem. By focusing on why someone wants something (interests) rather than what they demand (positions), we can often find 'win-win' scenarios.",
"applicableScope": [
"Resource allocation disputes",
"scheduling conflicts",
"or disagreements over project direction."
],
"processProtocol": "Parties list their specific demands ('Positions') and then peel back the layers by asking 'Why do you want this?' for each one. This uncovers the underlying 'Interest'—for example, 'I want the 5pm slot' becomes 'I need to pick up my kids'. The parties identify shared interests and brainstorm solutions that satisfy these core needs. They retain full authority to accept or reject the final trade-off.",
"restorationFallbacks": "The result is a trade-off agreement or contract. If parties remain stuck in their positions and cannot find a win-win, the process typically escalates to facilitated negotiation or mediation."
}
},
{
"id": "restorative-practices",
"label": "Restorative Practices",
"supportText": "Dialogue-focused methods for understanding and repairing harm.",
"sections": {
"corePrinciple": "We view conflict as a wound in the community, not a legal infraction. Our goal is accountability and repair: the offender must understand the impact of their actions and work to make it right.",
"applicableScope": [
"Harassment",
"Code of Conduct violations",
"or interpersonal harm where the offender admits responsibility."
],
"processProtocol": "A facilitator holds pre-conference meetings with both parties to ensure readiness. A joint dialogue is then convened where the harmed party shares the impact of the actions ('When you did X, I felt Y'). The offender practices deep listening and reflects back what they heard without defending themselves. Together, they co-create a 'Repair Plan' to address the harm, retaining ownership of the solution.",
"restorationFallbacks": "The artifact is a signed 'Repair Plan' detailing specific actions (apologies, education, community service). If the offender refuses to follow through on the plan they helped create, the process converts to a punitive one, such as a Tribunal hearing or expulsion."
} }
}, },
{ {
"id": "mediation", "id": "mediation",
"label": "Mediation", "label": "Mediation",
"supportText": "A neutral third party assists parties in reaching a voluntary agreement.", "supportText": "A mediator from outside the conflict helps the people in it reach their own agreement and decides nothing for them.",
"sections": { "sections": {
"corePrinciple": "We empower disputants to solve their own problems with structure. A mediator manages the process, but the parties own the outcome, ensuring they actually buy into the solution.", "corePrinciple": "In a small group nobody is neutral, so when the first steps fail we bring in a mediator from outside the conflict. The mediator runs the conversation and the people involved make every decision, so they own what they agree. We look past what each side demands to what each needs. Mediation needs both people to choose it and speak freely, so it is not for cases where one fears the other. It pulls against speed, since a good mediator takes time to find.",
"applicableScope": [ "applicableScope": [
"Deep-seated interpersonal conflicts", "When the first steps fail",
"co-founder disputes", "Disputes between founders or owners",
"or recurring friction between working groups." "Working groups that are stuck"
], ],
"processProtocol": "The mediator conducts separate 'intake' calls to hear each side's story privately before convening a joint session. During the session, the mediator uses 'looping' to ensure parties feel heard and may call a 'Caucus' (private meeting) if emotions run high. The mediator guides the parties to generate options and stress-test them, but the final decision power remains 0% with the mediator and 100% with the parties.", "processProtocol": "Either person can ask for mediation, and the other can say yes or no. We keep in our records two mediators we can call, such as a member with mediation skills, a trusted person from a neighboring group or a local mediation center. Both people agree on who. The mediator meets each person alone first, then together. They ask what each needs and why and help them weigh options. Either can stop at any time. Whatever they agree is written down with a check-in date two or three weeks out. The mediator keeps what they hear private and decides nothing. Between working groups or member groups, each side sends one or two people.",
"restorationFallbacks": "The outcome is a written Mediation Agreement. If mediation fails due to impasse, the parties must explicitly decide whether to 'agree to disagree' and live with the conflict, or escalate to Binding Arbitration to force a resolution." "restorationFallbacks": "Without agreement, the two can live with the difference, keep apart or go to two people we name each year to hear conflicts, from outside the group if we are small. Those two also hear threats, abuse and harassment unless the person harmed asks for mediation, and their proposal takes effect when both accept it or as a decision of the whole group. A mediator’s fee over $500 is a decision of the whole group."
} }
}, },
{ {
"id": "circle-processes", "id": "harassment-and-discrimination-complaints",
"label": "Circle Processes", "label": "Harassment and discrimination complaints",
"supportText": "A structured format for open dialogue with equal input from all involved.", "supportText": "We keep a written way to report harassment and discrimination, with two people to tell and no punishment for reporting.",
"sections": { "sections": {
"corePrinciple": "We prioritize equality and connection. By sitting in a circle and using a talking piece, we dismantle hierarchies and force deep listening, allowing the emotional truth of a conflict to surface.", "corePrinciple": "Everyone who works or volunteers with us can take part free from harassment and discrimination. A worker co-op or any group with staff is an employer, with legal duties that a friendly process cannot meet. So these reports skip the first steps of our conflict process and go to people ready to take them, whatever the role of the person reporting. It pulls against keeping things in the family, and it breaks when the only people to tell are friends of the person reported.",
"applicableScope": [ "applicableScope": [
"Community-wide trauma", "Harassment, including sexual harassment",
"grief processing", "Discrimination",
"or when a conflict has rippled out to affect the whole group." "Retaliation for reporting"
], ],
"processProtocol": "Participants gather in a circle with a centerpiece, and a 'Keeper' opens the session with a poem or quote to set the tone. A talking piece is introduced, granting the holder sole permission to speak. The group moves through rounds answering specific questions like 'What is the hardest part of this for you?', ensuring every voice is heard equally. No decision is forced; the power resides in the collective understanding generated by the circle.", "processProtocol": "Harassment means unwelcome conduct, including sexual harassment, that makes it harder for someone to work or take part. Discrimination means treating someone worse because of race, color, religion, sex, pregnancy, sexual orientation, gender identity, national origin, age, disability or another trait the law protects. We name at least two people to receive reports, of different genders where we can, and at least one who manages nobody or is a trusted outsider. Everyone learns who they are when they start. A report can be spoken or written. Within a week someone not involved, from outside the group if we are small, hears each person separately and decides what more likely than not happened.",
"restorationFallbacks": "The goal is a collective sense of understanding or a 'Group Covenant' describing how we want to be together. If specific harm is identified that requires repair, the circle may spin off into a separate Restorative Justice process." "restorationFallbacks": "Meanwhile we keep the two apart at no cost to the person who reported. Both people get the outcome in writing, and a finding leads to a response under our conflict process that stops the conduct. Nobody loses their place, shifts or pay for reporting, helping with a report or being a witness. Records stay private and apart from other files as long as the law requires. Anyone can also go to a government agency or a lawyer, and those have deadlines. We ask a lawyer what our state adds."
} }
}, },
{ {
"id": "judicial-committees", "id": "code-of-conduct-team",
"label": "Judicial Committees", "label": "Code of conduct team",
"supportText": "A standing committee responsible for adjudicating disputes.", "supportText": "A small team named in advance takes reports about conduct in our spaces, keeps them private and responds in proportion.",
"sections": { "sections": {
"corePrinciple": "We ensure consistency and due process. By having a standing body, we remove bias and ensure that every conflict is judged against the same set of bylaws.", "corePrinciple": "Where many people take part and few know each other, conduct problems need a known door and people with the authority to act. Our code of conduct team takes reports from anyone in our spaces, protects the people who make them and responds to behavior in proportion to the harm. Because it decides, its members serve fixed terms, step aside from cases they are close to and can be overruled on appeal. It pulls against openness, and it breaks when a team shields its friends.",
"applicableScope": [ "applicableScope": [
"Interpretation of bylaws", "Conduct problems in our spaces",
"contested elections", "Reports from anyone in our spaces",
"or allegations of abuse of power by leadership." "Online and in-person spaces"
], ],
"processProtocol": "A formal complaint is filed with the Committee, which is composed of elected or appointed members serving fixed terms. The Committee holds a formal hearing where both sides present evidence and call witnesses. The proceedings are recorded. The Committee then deliberates in closed session to determine if the rules were violated, holding 100% of the decision power.", "processProtocol": "We name two to seven people for one year as our code of conduct team to hear reports about conduct in our spaces. Until two members can serve, the person who started the group serves with a trusted person from outside. Everyone in our spaces can reach the team privately, with a second contact for reports about a team member. A report gets a reply within three days. Two team members close to neither person hear from both and decide within two weeks. They choose the smallest response that will stop the behavior, from a private word to a time-limited suspension from our spaces, or for someone who is not a member, removal from them.",
"restorationFallbacks": "The Committee issues a written 'Judgment' that is binding. This may include penalties like censure, removal from office, or expulsion. The decision is recorded in the organization's case law to guide future rulings." "restorationFallbacks": "The person who reported learns the outcome, without the other person’s private details. Either side can appeal within 14 days to team members who took no part, or else to two members outside the team. A missed deadline sends the report to the second contact. Anyone removed can ask for review after a year. Removing a member needs our supermajority share of the active members who vote, if our quorum votes, after the member has been heard. Removal never cuts anyone off from aid. Reports are deleted after two years unless the person is reported again. Nobody loses their place for making a report."
}
},
{
"id": "hearing-panel",
"label": "Hearing panel",
"supportText": "Three members drawn from a pool we elect hear each serious case and decide in writing, and either side can appeal.",
"sections": {
"corePrinciple": "When a group is too large for members to know everyone in a case, fairness comes from a known procedure rather than from knowing each other. A panel of members hears serious cases and questions about what our Rule means, gives both sides the same chance to be heard and decides on what is in front of it. Members elect the pool and panelists change with each case, so no standing body gathers power. It pulls against speed, and it breaks when panels drift close to leaders.",
"applicableScope": [
"Serious cases in a large group",
"What our Rule means",
"Disputed elections",
"Complaints about role holders"
],
"processProtocol": "Each year the members elect a pool of nine to fifteen members to hear cases and two members to receive complaints. The panel hears each case, and the two receivers handle it until the panel is drawn. Within a week of a written complaint, the receivers draw three panelists by lot from the pool. Panelists close to either side step aside, and each side can turn down one panelist without a reason. The panel sends both sides the complaint and hears each, with a friend or advocate if wanted, and any witnesses. Within 30 days it decides whether our Rule was broken and what response follows, in writing with reasons and without private details.",
"restorationFallbacks": "The decision takes effect when the panel issues it. Either side can appeal within 14 days over a broken procedure or new facts, to three panelists who took no part. A late panel is replaced if either side asks. A finding for removal goes to a vote of the active members after the person has been heard and needs our supermajority share of those who vote, if our quorum votes. Removal never cuts anyone off from aid. Nobody loses their place for bringing a case or being a witness. Case notes are deleted two years after a case closes."
} }
}, },
{ {
"id": "managerial-decision", "id": "managerial-decision",
"label": "Managerial Decision", "label": "Managerial decision",
"supportText": "A manager or leader makes a binding decision.", "supportText": "Where we have managers, a manager settles conflicts about the work within written limits, and staff can take a grievance past them.",
"sections": { "sections": {
"corePrinciple": "We prioritize efficiency and clear lines of authority. In a hierarchy, the person with responsibility for the outcome must have the authority to resolve the blockers.", "corePrinciple": "Where we have managers, the person responsible for the work can settle conflicts that block it, quickly and close to the people involved. That speed puts power with someone the staff did not choose, so we limit it in writing. A manager decides only how the work is arranged, never a conflict they are part of and never a report of harassment or discrimination. Staff can see the policies a manager must follow. It breaks when raising a concern costs someone their shifts.",
"applicableScope": [ "applicableScope": [
"Operational disagreements", "Conflicts between staff",
"performance issues", "Tasks, schedules and workloads",
"or swift resolution of low-stakes conflicts." "Grievances about a manager’s decision"
], ],
"processProtocol": "The manager hears both sides of the conflict (often in 1:1 meetings) and consults organizational policy. They then make a unilateral decision based on what is best for the business or the team's goals. The manager holds 100% of the decision power.", "processProtocol": "Staff can use the first steps of our conflict process first. When those fail, or a staff member asks, the manager of the people involved hears each one separately within a week, then together if both agree. The manager decides in writing, with reasons and a review date, how the work changes, such as tasks, schedules or who works with whom. Managers hear conflicts about the work of the people they manage. A manager who is part of a conflict hands it to their own manager or to a mediator both people accept. Where staff have a union contract, its grievance steps come first.",
"restorationFallbacks": "The decision is communicated as a directive. Compliance is mandatory as a condition of employment. Failure to comply is treated as insubordination." "restorationFallbacks": "Any staff member can bring a written grievance about a manager’s decision within 14 days to the next person up, or to a person outside the chain the owners name for this, who answers in writing within 14 days. Harassment and discrimination reports skip the manager and follow our written procedure for them. A manager cannot cut pay or end someone’s work over a conflict alone. That follows our written discipline steps and employment law. Nobody loses pay, shifts or their job for raising a concern."
} }
}, },
{ {
"id": "internal-tribunal", "id": "outside-help",
"label": "Internal Tribunal", "label": "Outside help",
"supportText": "A formal hearing body within the organization.", "supportText": "We decide in advance who we call when someone is in danger or crisis, or when a conflict needs skills we lack.",
"sections": { "sections": {
"corePrinciple": "We provide rigorous due process for the most serious accusations. To protect members from unjust expulsion, we simulate a legal trial to ensure high standards of evidence.", "corePrinciple": "We are not a crisis service, and some moments need skills or authority we do not have. Deciding in calm times who we call keeps us from improvising in a panic and from calling someone who makes things worse for the people involved. Whether our list ever includes the police, and when, is ours to decide together, and we write the answer down. This pulls against handling everything ourselves, and it breaks when nobody can find the list.",
"applicableScope": [ "applicableScope": [
"High-stakes violations that could result in permanent expulsion", "Immediate danger",
"blacklisting", "A member in crisis",
"or significant financial penalty." "Mediators, lawyers and other help"
], ],
"processProtocol": "A panel of judges (often distinct from the leadership team) is convened. A 'Prosecutor' presents the case against the accused, and a 'Defender' advocates for them. Formal rules of evidence apply. The Tribunal weighs the facts against the organization's 'Constitution' or supreme laws.", "processProtocol": "As a decision of the whole group, we keep a short list of outside help in our records, seen only by members. It names who we call in immediate danger, in a medical or mental health crisis, for a mediator, for legal help and for support after harm, with a crisis line that answers at any hour. It also records whether and when we involve the police. Two members check every entry twice a year. Safety leads and anyone who hears a conflict for us carry the list, and every member learns where it is when they join.",
"restorationFallbacks": "The Tribunal issues a Verdict. If guilty, the sentence is executed immediately. Appeals are only allowed on procedural grounds (e.g., if the Tribunal failed to follow its own rules), not on the facts of the case." "restorationFallbacks": "Any member can always get help for themselves, whatever the list says, and nobody is blamed for a good faith call in immediate danger. When a member is in crisis, we respond with care and outside support, not discipline. We ask a person before sharing their situation outside the group, unless someone is in immediate danger or the law requires a report, as for child abuse. Paying for outside help needs one other member’s written agreement over $50 and a decision of the whole group over $500."
} }
}, },
{ {
"id": "consensus-building", "id": "conflict-workshops",
"label": "Consensus Building", "label": "Conflict workshops",
"supportText": "Collaborative work to reach a resolution that all parties can agree upon.", "supportText": "Twice a year, members practice the skills our conflict process needs, from hard conversations to being a fair friend.",
"sections": { "sections": {
"corePrinciple": "We believe that sustainable solutions come from the parties themselves finding common ground. Unlike a top-down ruling, a consensus agreement ensures that all stakeholders feel heard and invested in the outcome.", "corePrinciple": "Most of us learned conflict where someone above us ended the argument, so we arrive out of practice. Handling conflict as equals is a skill anyone can learn, and it is cheaper to learn in calm times than in the middle of a fight. We practice on situations from our own kind of work, so that everyone, not just a few, can talk directly, be a fair friend and calm a heated moment. It costs a few evenings a year, and it breaks when only the same few come.",
"applicableScope": [ "applicableScope": [
"Best for complex", "Calm times",
"multi-stakeholder issues where relationships must be preserved and no single rule was broken." "New members",
"Skills for our conflict process"
], ],
"processProtocol": "The process begins by convening all stakeholders in a shared space to establish shared goals and ground rules. Each party is invited to state their underlying needs rather than just their surface demands. The group then brainstorms multiple options for mutual gain without immediate judgment. Through dialogue, these options are refined until a single solution emerges that every stakeholder can support, maintaining full decision-making power within the group itself.", "processProtocol": "Twice a year we hold a workshop of two to three hours on conflict skills, and new members join the next one. It covers asking for a hard conversation, sticking to what happened, saying back the other person’s account, calming a heated moment, being someone’s friend in a conflict and what counts as a safety concern. We practice with made-up situations from our own kind of work, never with a real conflict between members. Anyone who hears conflicts for us also practices hearing a case and keeping notes private. Two members plan each workshop, or we invite someone from a local mediation center or a neighboring group.",
"restorationFallbacks": "The result is a shared agreement ratified by all parties. If consensus cannot be reached, the group may define a specific fallback mechanism, such as escalating to a Supermajority Vote (requiring 75% agreement) or bringing in an external Facilitator to unblock the dialogue." "restorationFallbacks": "Workshops are free for members and held at times and places most of us can make, with notes for anyone who cannot come. Nobody has to share anything personal, and nobody is penalized for missing one. If a year passes without a workshop, our next meeting names two members to plan one. Paying someone from outside needs one other member’s written agreement over $50 and a decision of the whole group over $500."
} }
}, },
{ {
"id": "binding-arbitration", "id": "regular-debriefs",
"label": "Binding Arbitration", "label": "Regular debriefs",
"supportText": "An external arbitrator makes a binding decision.", "supportText": "After big pieces of work, and monthly otherwise, we look back without blame and fix what keeps causing friction.",
"sections": { "sections": {
"corePrinciple": "We need finality and legal certainty. By outsourcing the decision to a professional judge, we remove internal bias and ensure the ruling will hold up in court.", "corePrinciple": "Many conflicts between two people are really about something the group never settled, such as who decides what, who covers a shift or how much one person carries. We look back at the work often and without blame, so these gaps surface as problems to fix rather than grudges against a person. A debrief is about how we work and never a hearing about anyone’s conduct. It costs half an hour, and it breaks when it turns into blame.",
"applicableScope": [ "applicableScope": [
"Commercial disputes", "After actions, events and releases",
"contract breaches between entities", "Unclear roles and workloads",
"or employment termination disputes." "Gaps a conflict showed"
], ],
"processProtocol": "The organization contracts a professional arbitration firm. The process follows strict legal procedures similar to a court trial but is private. The arbitrator hears evidence and legal arguments from both sides' lawyers. The arbitrator holds 100% of the decision power.", "processProtocol": "Within a week after each action, event or big piece of work, and once a month otherwise, the people who did the work meet for half an hour or answer in writing. A member who did not lead the work asks what worked, what got in the way and what we will change. We talk about steps and systems rather than people. Changes go in our records with who makes them and by when.",
"restorationFallbacks": "The award is final, binding, and enforceable in real-world courts. There is typically no right of appeal. This provides total closure to the dispute." "restorationFallbacks": "A concern about one person’s conduct does not belong in a debrief and goes to our conflict process. A change that needs a new rule goes to the way our Rule makes that decision. If debriefs keep finding the same problem, it goes on the agenda of our next meeting without naming who was involved. If a month passes without one, any member can call one, and anyone who cannot attend can add answers in writing."
}
},
{
"id": "safety-lead",
"label": "Safety lead",
"supportText": "At every event or shift, a named safety lead can ask anyone to leave for the day and knows who to call.",
"sections": {
"corePrinciple": "Danger at an event cannot wait for a meeting. So one person at every event, shift or distribution knows that watching for safety is their job, and nobody has to wonder who acts. Their power is short and narrow. They can calm things down, keep people apart and ask someone to leave for the day, and anything longer goes to our conflict process. It pulls against an open door, and it breaks when the lead is too tired to notice or is part of the trouble.",
"applicableScope": [
"Events, shifts and distributions",
"Immediate danger",
"People who are not members"
],
"processProtocol": "For each event, shift or distribution we name a safety lead and a backup, and everyone working knows who they are. Where our Rule names an on-site lead, one member can hold both roles. The safety lead stays sober, carries our list of outside help and checks in with the people working. If someone threatens, harms or frightens others, the safety lead first tries to calm things, then can separate people and ask a person to leave for the rest of the day. In immediate danger they get help from outside the group first. Within 24 hours they write a short note, without the names of the people we serve, for two members not involved, who take it into our conflict process if it needs more.",
"restorationFallbacks": "Asking someone to leave lasts only for that day, and anything longer goes to our conflict process. If the person receives aid from us, a member finds another safe way for them to get it that day. A safety lead who is part of the problem hands over to the backup. Nobody loses their place for raising a safety concern, and no safety lead is blamed for a good faith call."
}
},
{
"id": "legal-action",
"label": "Legal action",
"supportText": "Legal action in our name is a decision of the whole group, and legal papers reach two named members the same day.",
"sections": {
"corePrinciple": "Going to court costs money, time and trust, and a missed deadline can lose a case without a hearing. Legal action in our name comes after our other steps unless a deadline or harm cannot wait. Against a member, a former member or someone we serve, those steps include our conflict process. It breaks when papers sit unopened or one person answers alone.",
"applicableScope": [
"Suing or being sued",
"Lawyer’s letters and legal threats",
"Subpoenas, warrants and demands for records",
"Keeping records once trouble is likely"
],
"processProtocol": "Anyone handed legal papers for us notes when and from whom and sends a copy that day to the members who handle legal matters. Nobody else replies, signs, promises or hands over anything for us. Those members check the papers are real, tell any insurer in writing at once and find a lawyer through our list of outside help. If a legal deadline will pass before the group can decide, they act only to protect it, spending up to $500. That step lapses after 14 days unless the group confirms it, but nothing is withdrawn that would lose the case. Within a week they tell members what arrived, leaving out names and our lawyer’s advice. Suing, answering, settling or appealing is a decision of the whole group, without any member on the other side, after a lawyer’s advice in a meeting or call.",
"restorationFallbacks": "A legal hold covers anything the matter may need, since destroying records once trouble is likely can be a crime. A letter or request is not an order. We follow a judge’s warrant at once and give only what an order requires. Unless a judge forbids it, we tell the people named first, or once officers leave. We never take legal action over a concern, a report or organizing, and never lock anyone out of their home or cut its utilities. Nobody’s name or story goes into a court filing without their consent unless the law requires it. Nobody loses their place, roles, shifts, pay or aid for going in good faith to a lawyer, an agency, the police or a court."
}
},
{
"id": "police-and-immigration-visits",
"label": "Police and immigration visits",
"supportText": "When police or immigration officers arrive, members know what to say, who to call and what never to hand over.",
"sections": {
"corePrinciple": "What members say in the first minutes of a visit from police or immigration officers can put someone at risk. So we decide in calm times who speaks, what we ask for and what we never do. We never lie to or physically stop officers, which puts everyone in more danger and can be a crime. It breaks when nobody knows who speaks or where the legal help number is.",
"applicableScope": [
"Officers at a shift, an event or our space",
"Warrants and searches",
"Questions about members or the people we serve"
],
"processProtocol": "At each shift, event or space, one named member and a backup speak with officers while everyone else keeps working or steps back. Where we have a space, we mark its private areas and keep their doors closed. The member asks for the officers’ names, badge numbers, reason and any warrant, and reads it without opening a door. A warrant signed by a judge lets officers search the places it names. A paper signed only by an immigration officer is not a judge’s warrant and does not by itself open private areas. If officers go in anyway, we say we do not consent, never block a door or a person, and write down what happens. Anyone can decline to answer and ask for a lawyer, giving only their name where the law requires it. Nobody answers questions about other people or hands over any list. One member calls the members who handle legal matters and the legal help on our list of outside help.",
"restorationFallbacks": "Nobody resists, runs, lies, hides or deletes anything, or unlocks another member’s phone. Members who face added risk can calmly step away before officers stop them, and nobody asks why. Anyone filming keeps the faces of members and the people we serve out of the frame. Within 24 hours the member who spoke writes the members who handle legal matters a short note, naming none of the people we serve. Nobody loses their place, roles, shifts, pay or aid for acting in good faith during a visit. Once a year we practice and ask a lawyer whether these steps still fit."
}
},
{
"id": "legal-support-for-members",
"label": "Legal support for members",
"supportText": "When a member is cited, arrested or sued for doing our work, we back them with legal help, jail support and money we agreed.",
"sections": {
"corePrinciple": "Members who do our work in public, such as sharing food, giving first aid or protesting, carry risks the rest of us share. When one of them is cited, arrested or sued for that work, we stand with them instead of leaving them to find help alone. The member makes every legal choice, and our support never depends on what they are accused of. It costs money and pulls against privacy, and it breaks when nobody knows the legal hotline number.",
"applicableScope": [
"Citations and arrests during our work",
"Members sued or subpoenaed for our work",
"Jail support and bail"
],
"processProtocol": "Our list of outside help names a legal hotline, and members carry its number on paper before street work, since phones can be taken. Our work means a shift, action or task the group or a working group agreed to. Before an action where arrests are likely, we name two members and a backup for jail support. Each member can give one of them their legal name, birth date, medication needs and whether they want bail. When a member is held, the jail support members call the hotline, find the member and meet them on release with a ride and food. Bail is paid only when the member asks, from a bail fund or shared money, by two members who tell every member within 24 hours. More than $500 needs a decision of the whole group unless the member would stay held. Before paying, they ask the hotline whether the member may have an immigration hold. A member sued or subpoenaed for our work gets our help finding a lawyer.",
"restorationFallbacks": "The group hears only what the member agrees to share. Nobody discusses an arrest or charge in a group chat or online, or presses anyone about what to tell a court. Nobody loses their place, roles, shifts, pay or aid over a citation, arrest or charge from our work. Harm to a person still goes to our conflict process. Members who face added risk from an arrest can choose other work without saying why. Private details held for jail support are deleted once the member is safe."
}
},
{
"id": "disputes-with-partners",
"label": "Disputes with partners",
"supportText": "We write dispute steps into agreements with partners and settle disputes by talking, then mediation, before anything legal.",
"sections": {
"corePrinciple": "We work with other groups, funders, landlords and suppliers, and our Rule binds none of them. So we agree ahead of time, in writing, how a dispute with a partner will be handled, and keep each dispute with the few people who hold the relationship for as long as that works. Walking away from a partnership is a fair outcome. A dispute with a partner is never a reason to share private details about our members or the people we serve.",
"applicableScope": [
"Partner groups and funders",
"Landlords, suppliers and contractors",
"Who speaks for us"
],
"processProtocol": "Any written agreement in which we share money, space, equipment or work with a partner names one contact on each side and these steps. First the people who hold the relationship talk within a week of either side raising a problem. If that fails, the named contacts meet within two weeks. If that fails, we offer mediation by someone both sides accept, and arbitration only as our signed agreement with that partner says, or if both sides agree to it at the time. We ask a lawyer before signing an agreement that makes an arbitrator’s decision final. Only members our Rule names, or members the group sends with written limits, can speak for us or agree terms. A complaint from outside about one of our members goes to our conflict process.",
"restorationFallbacks": "A dispute that could cost more than $500, end a partnership or change what we do is a decision of the whole group. We ask a lawyer before signing anything that settles a claim and before any legal step. For a dispute on our list for binding arbitration, groups in our network that agreed to the list follow it instead of these steps. Notes on each dispute stay with those handling it. When a dispute ends, we tell members what changes in the work and note what to write differently in our next agreement."
}
},
{
"id": "working-with-children",
"label": "Working with children",
"supportText": "When we work with children or adults at risk, two adults are always present and anyone can report concerns outside the group.",
"sections": {
"corePrinciple": "Abuse happens where adults are trusted and alone with people who cannot easily speak up. We prevent it with simple habits everyone follows, not with trust in good people. These habits cover childcare at meetings, youth programs, school drives and work with adults who depend on others for care. They pull against convenience, and they break when a shortage of volunteers becomes a reason to skip them.",
"applicableScope": [
"Childcare and youth programs",
"Adults who depend on care",
"Concerns about abuse"
],
"processProtocol": "No member is alone with a child who is not their own or with an adult at risk, and two adults are present or within sight. Members who take on this work learn the signs of abuse and how to raise a concern. Where the law or an insurer requires a background check for a role with children, only that role gets one, and the results stay with two named members. We keep contact through group channels and never through private messages with a child.",
"restorationFallbacks": "Any member who suspects abuse can report it outside the group at any time, and our list of outside help names who to call. Members who must report child abuse by law do so. A member a concern is about steps away from this work at once while our conflict process looks into it, as a pause and not a finding. Nobody loses their place for raising a concern."
}
},
{
"id": "restorative-practices",
"label": "Restorative practices",
"supportText": "When someone owns the harm they caused and the person harmed wants it, a facilitator helps them agree how to repair it.",
"sections": {
"corePrinciple": "Some harm is best answered by the person who caused it making things right. A restorative meeting puts the person harmed at the center and asks the person who caused harm to understand what their actions did and take concrete steps to repair it. It is voluntary for both, never a condition of anyone’s place, and works alongside our conflict process rather than replacing it. It never delays a safety step. It breaks when anyone is pushed into it, so nobody is asked to forgive.",
"applicableScope": [
"Harm someone admits causing",
"When both people want to meet",
"Repair agreed in a plan"
],
"processProtocol": "Either person can ask for a restorative meeting. It goes ahead only if both agree and the person who caused harm accepts that it happened. A facilitator both accept, a member close to neither person or someone from outside, meets each alone first to check that it is safe and wanted. Then the two meet, each with a supporter if they wish. The person harmed says what happened to them and what they need. The person who caused harm listens and says it back without defending. Together they write a repair plan of specific actions, such as an apology, paying for a loss or changing a behavior, with dates and a check-in.",
"restorationFallbacks": "Either person can stop at any time, and stopping is never held against the person harmed. If the plan is not kept or no meeting happens, the case returns to our conflict process. What is said stays private and is not used against either person later, except to keep someone safe. Our records note only that a plan exists. Paying an outside facilitator needs one other member’s written agreement over $50 and a decision of the whole group over $500, and the request names nobody involved."
}
},
{
"id": "accountability-process",
"label": "Accountability process",
"supportText": "For serious harm, the person harmed says what they need, and a separate small group helps the person who caused it change.",
"sections": {
"corePrinciple": "Some harm, such as sexual harm, abuse or a pattern of harm, needs more than mediation, and the person harmed should never have to face the person who caused it. We follow what the person harmed needs and keep them apart from the work with the person who caused harm. We also help the person who caused harm take responsibility and change, because pushing someone out without that often carries the harm elsewhere. It breaks when change is only performed, so the person harmed says whether their needs are met.",
"applicableScope": [
"Sexual harm or abuse",
"Repeated harm",
"When meeting would not be safe"
],
"processProtocol": "When the person harmed asks for this process, they choose two or three supporters, inside or outside the group, and with them say what they need, such as distance from certain spaces, an acknowledgment or a change in behavior. Separately, two or three people the person who caused harm trusts, and who accept those needs, form an accountability group. It meets the person who caused harm regularly, for months if needed, to work through those needs and the changes they will make. The two groups share only what the person harmed agrees to share. The person harmed can ask for a pause or more distance at any point.",
"restorationFallbacks": "The person harmed can step out at any time, and we still act on their safety. If the person who caused harm will not take part, or the harm goes on, it returns to our conflict process, which can lead to removal. When we can, we bring in an outside facilitator experienced in this work. Paying them needs one other member’s written agreement over $50 and a decision of the whole group over $500, and the request names nobody involved. Nobody pressures the person harmed to report, take part or forgive. What is shared stays with the two groups, and our records note only that it exists."
}
},
{
"id": "circle-processes",
"label": "Circle processes",
"supportText": "When a conflict or loss has touched everyone, we meet in a circle and speak in turn so every voice is heard.",
"sections": {
"corePrinciple": "When a conflict, a removal or a hard loss has touched the whole group, people need a way to say how it landed before the work can go on well. In a circle everyone speaks in turn, without interruption or debate, so quiet members are heard as fully as loud ones. A circle builds understanding and costs time, so it is short and ends on practical next steps. It never decides anything about a person and never replaces or delays our conflict process or a safety step.",
"applicableScope": [
"After a conflict touched everyone",
"After a removal or a loss",
"Check-ins a few times a year"
],
"processProtocol": "Any member can ask for a circle, and we hold one when two members agree it would help. A keeper, a member practiced in circles or someone from outside, prepares two or three questions, such as what has been hardest about this and what would help us work well together. People sit in a circle or join a call. An object passes from person to person, and only the person holding it speaks. Anyone can pass. The keeper closes with a round on what each person will do next and notes only the agreements.",
"restorationFallbacks": "Nobody has to attend or share, and nobody is asked to forgive. The people involved in a case are asked first whether they want a circle held, and a circle never discusses who was right in a case our conflict process is handling. If a circle surfaces a specific harm, it goes to our conflict process. If it surfaces a gap in how we work, it goes to our next meeting. Agreements go in our records without who said what."
}
},
{
"id": "whistleblower-protection",
"label": "Whistleblower protection",
"supportText": "Anyone can report suspected fraud, misuse of money or law-breaking, even by leaders, and nobody is punished for an honest report.",
"sections": {
"corePrinciple": "People inside a group are often the first to see money going missing or rules being broken, and the last to say so if speaking up could cost them. We want to hear early, so anyone can report around whoever a concern is about, and we protect people who report honestly. A worry about shared money is never handled only by the people who control the money. It breaks when reports go to the people they concern, so we name more than one door.",
"applicableScope": [
"Fraud or misuse of money",
"Breaking the law",
"Concerns about leaders"
],
"processProtocol": "We name two members to receive reports, neither of whom handles our money, and can name a trusted person outside the group as a third. Anyone can report to any of them, in person, in writing or without giving their name, about fraud, misuse of money or property, breaking the law or hiding any of these, by anyone including leaders and managers. Within a week the person who receives a report tells the reporter it arrived, if they can reach them, and with someone close to neither side looks into it. Where money is involved, they can see every account and record, and so can any member who asks, without details of the people we serve.",
"restorationFallbacks": "A report about one of the people who receive reports goes to the others, and if nothing happens within two weeks, the reporter can take it to another. Findings about a person go to our conflict process, and findings about how we handle money go to a decision of the whole group. Nobody loses their place, shifts or pay for an honest report, even a mistaken one, or for helping look into it. Only those handling a report see it. Nothing here stops anyone from going to a government agency."
} }
}, },
{ {
"id": "non-binding-arbitration", "id": "non-binding-arbitration",
"label": "Non-Binding Arbitration", "label": "Non-binding arbitration",
"supportText": "An arbitrator gives a recommendation that is not binding.", "supportText": "When talking and mediation stall, a person close to neither side hears both and recommends an answer either side can turn down.",
"sections": { "sections": {
"corePrinciple": "We want an expert opinion to break a deadlock, but we aren't ready to hand over full control. A neutral expert's 'advisory opinion' can often shame or persuade parties into agreement.", "corePrinciple": "Some disputes stall because each side trusts its own reading of the facts or of a written agreement. A person close to neither side hears both and recommends an answer in writing, with reasons. Neither side has to accept it, so the way to court stays open, though legal deadlines keep running. It answers a question, such as what is owed, and never judges anyone’s conduct. It pulls against speed, and it breaks when the arbitrator is close to one side or one side uses it to stall.",
"applicableScope": [ "applicableScope": [
"Complex technical or valuation disputes where parties need a reality check on their positions." "Money owed and member shares, other than pay",
"What a written agreement means",
"Technical facts two sides read differently",
"Disputes between groups in our network"
], ],
"processProtocol": "Similar to binding arbitration, a neutral expert hears the case. However, instead of a ruling, they issue a 'Recommendation' explaining how a court would likely rule or what is fair. The parties then take this recommendation back to the negotiation table.", "processProtocol": "Either side can propose it once talking has not settled a dispute within 30 days and mediation has failed or been turned down. When a member proposes it to the group, the group takes part, and otherwise the other side answers within a week. The sides choose an arbitrator within 14 days, or the body that appoints for us does. When the group is a side, the arbitrator is never a member or anyone we used in the past two years. A member on the other side neither votes nor counts. The arbitrator discloses any tie, hears both under our fair hearing safeguards and recommends within 30 days.",
"restorationFallbacks": "The parties use the recommendation as a baseline for a final settlement. If they still cannot agree, they preserve the right to go to court or binding arbitration." "restorationFallbacks": "Each side says in writing within 14 days whether it accepts all, part or none, and after a reminder silence means no. The parts both accept become a written agreement that binds them, so a member can take advice first. If either side says no, a dispute on our list for binding arbitration goes there if both sides agreed to the list. Any other dispute returns to our conflict process. Saying no is never held against anyone. As far as the law allows, what either side says or offers is used later only to keep someone safe. It hears none of the matters outside arbitration. When a member is a side, the group pays the arbitrator. Our records note only that it took place, and the note is deleted after two years."
} }
}, },
{ {
"id": "binding-contracts", "id": "binding-arbitration",
"label": "Binding Contracts", "label": "Binding arbitration",
"supportText": "Legal agreements that define resolution methods.", "supportText": "For a few listed disputes that each member agrees to in writing, a person close to neither side decides instead of a court.",
"sections": { "sections": {
"corePrinciple": "We prioritize predictability and liability protection. By defining the rules of engagement in advance, we prevent ambiguity when things go wrong.", "corePrinciple": "Some disputes need a final answer, and court is slow, costly and public. The arbitrator’s decision is final, and a court will enforce it. Both sides give up a jury, most appeals and a public hearing, so we list few disputes and each member chooses whether to agree. It breaks when members agree without knowing what they gave up.",
"applicableScope": [ "applicableScope": [
"Vendor relationships", "Money owed and member shares, other than pay",
"employment terms", "Shared property other than anyone’s home",
"intellectual property ownership", "What a written agreement means",
"and liability waivers." "Disputes between groups in our network that agreed to it"
], ],
"processProtocol": "The process happens before conflict arises. Parties negotiate terms (using lawyers if necessary) and sign a document. When a conflict occurs, the contract is referenced. If the contract says 'X happens,' then X happens automatically without further debate.", "processProtocol": "Our list for binding arbitration binds nobody until a lawyer says it can, and adding a dispute needs our supermajority share. Each member hears the list in a language they understand, with what agreeing gives up, then decides separately in writing. Joining, aid and work never depend on it. A yes can be taken back for disputes that begin later. People under 18, partners and the people we serve, including members receiving aid, are never bound. The sides choose an arbitrator within 14 days, or the body that appoints for us does. When the group is a side, the arbitrator is never a member or anyone we used in the past two years. The arbitrator discloses any tie, hears both under our fair hearing safeguards and decides in writing within 30 days. The arbitrator decides without a side only after two written notices 14 days apart, sent every way we know.",
"restorationFallbacks": "The contract itself is the resolution mechanism. Breach of contract triggers specific penalties defined in the document (e.g., termination, fines)." "restorationFallbacks": "It hears none of the matters outside arbitration, and a court decides whether a dispute is on our list. Small claims court, urgent court orders and government agencies stay open, and legal deadlines keep running. If no arbitrator is in place after 60 days, either side can go to court. When a member is a side, the group pays the arbitrator within 30 days or the member can go to court. The list binds the group as it binds members. The decision is final after 14 days unless the body that appoints for us has granted a new hearing by then. Nothing owed is ever taken from anyone’s aid."
}
},
{
"id": "lottery-sortition",
"label": "Lottery/Sortition",
"supportText": "Random selection used to resolve low-stakes disputes or select juries.",
"sections": {
"corePrinciple": "We believe that when two valid options exist and neither is 'wrong,' a random decision is the fairest way to break the tie.",
"applicableScope": [
"Allocating scarce resources (e.g.",
"who gets the office)",
"scheduling conflicts",
"or low-stakes ties."
],
"processProtocol": "The group agrees that the issue is not worth further debate and commits to a random method (coin flip, drawing straws, RNG). The random event occurs, and the result is accepted immediately. This transfers 100% of the decision power to chance.",
"restorationFallbacks": "The resolution is immediate. Because the process is seen as inherently unbiased, no repair is usually needed. Refusal to accept the result is treated as acting in bad faith."
}
},
{
"id": "rotational-judging",
"label": "Rotational Judging",
"supportText": "A rotating set of members is assigned to handle conflicts.",
"sections": {
"corePrinciple": "We distribute the power of judgment. By taking turns being the 'judge,' members learn empathy for the difficulty of making decisions and prevent a permanent ruling class.",
"applicableScope": [
"Minor Code of Conduct infractions",
"disputes over shared space or noise."
],
"processProtocol": "A 'Jury Duty' roster is created including all eligible members. When a dispute arises, the next three members on the list are summoned to hear the case briefly. They issue a ruling based on common sense and community norms, holding 100% of the decision power for that specific instance. Afterward, they return to the pool.",
"restorationFallbacks": "The ruling is binding for that instance. This builds community capacity for governance. Repeated poor judgments by rotational judges may trigger a review of the 'Jury Duty' training process."
} }
} }
] ]
+398 -187
View File
@@ -1,17 +1,17 @@
{ {
"header": { "header": {
"title": "Choose up to five Core Values", "title": "Choose up to six core values",
"descriptionLead": "What does the community hold most dear? You can also combine or", "descriptionLead": "What does your group hold most dear? You can also combine or",
"addLink": "add", "addLink": "add",
"descriptionTrail": "new values to the list." "descriptionTrail": "new values to the list."
}, },
"multiSelect": { "multiSelect": {
"addButtonText": "Add value", "addButtonText": "Add value",
"selectionCount": "{count} of {max}", "selectionCount": "{count} of {max}",
"limitReached": "Five of five values selected. Remaining values are unavailable until one is removed." "limitReached": "Six of six values selected. Remaining values are unavailable until one is removed."
}, },
"detailModal": { "detailModal": {
"subtitle": "Edit or add to this description to describe what this value means to your community.", "subtitle": "Edit or add to this description to say what this value means to your group.",
"meaningLabel": "What does this value mean to your group?", "meaningLabel": "What does this value mean to your group?",
"signalsLabel": "Signals of Violation", "signalsLabel": "Signals of Violation",
"addValueButton": "Add Value", "addValueButton": "Add Value",
@@ -22,351 +22,562 @@
"discardProceed": "Discard", "discardProceed": "Discard",
"discardKeepEditing": "Keep editing" "discardKeepEditing": "Keep editing"
}, },
"families": [
{
"id": "treat-each-other",
"label": "How we treat each other"
},
{
"id": "share-power",
"label": "How we share power"
},
{
"id": "stay-free",
"label": "How we stay free"
},
{
"id": "make-room",
"label": "How we make room for everyone"
},
{
"id": "keep-safe",
"label": "How we keep each other safe"
},
{
"id": "share-work",
"label": "How we share work and what it makes"
},
{
"id": "stand-together",
"label": "How we stand with others"
},
{
"id": "share-knowledge",
"label": "How we share what we know"
},
{
"id": "learn-and-last",
"label": "How we learn and last"
}
],
"values": [ "values": [
{ {
"id": "accessibility",
"label": "Accessibility", "label": "Accessibility",
"meaning": "We design spaces and tools that everyone can use regardless of ability or background. To us, this means we provide alt-text for images, wheelchair ramps for events, and use plain language in documents. We acknowledge a tension with Aesthetics/Security (Simple design vs flashy easy access vs strict gates).", "family": "make-room",
"signals": "Hosting events in venues that are physically inaccessible to wheelchair users or publishing video content without captions which excludes the deaf community." "meaning": "We make our spaces, tools and words usable by everyone, whatever their body, language or schooling. To us, this means we meet in places people can reach and enter, caption what we record, offer translation when we can and write in plain words. We acknowledge a tension with Speed (Making it work for everyone vs getting it out now).",
"signals": "Meetings held up stairs or far from transit, videos shared without captions, or documents full of jargon that only longtime members can follow."
}, },
{ {
"id": "accountability",
"label": "Accountability", "label": "Accountability",
"meaning": "Members accept responsibility for their actions and their impact on the community. To us, this means we publish error logs and hold retrospective meetings to own our mistakes without shifting blame. We acknowledge a tension with Psychological Safety (Blame-free culture vs owning mistakes).", "family": "share-power",
"signals": "Leaders refusing to accept responsibility for failures and instead blaming external factors or junior team members for the lack of success." "meaning": "Every member and every role answers for what they do with the trust the group gives them. To us, this means each role has a written mandate, a term and a report back, members can recall it and we name our own mistakes at the next meeting without hunting for someone to blame. We acknowledge a tension with Trust (Checking the work vs taking each other at our word).",
"signals": "Role holders who never report back, terms that quietly never end, no way to recall anyone or mistakes that get blamed on whoever is newest."
}, },
{ {
"id": "adaptability",
"label": "Adaptability", "label": "Adaptability",
"meaning": "The group remains flexible and willing to change strategies when circumstances shift. To us, this means we pivot strategies quarterly based on feedback rather than sticking to a rigid 5-year plan. We acknowledge a tension with Consistency (Changing course vs staying the path).", "family": "learn-and-last",
"signals": "Persisting with a strategy that evidence shows is failing or penalizing members who suggest necessary changes based on new data." "meaning": "We change course when the situation changes instead of holding to a plan that has stopped working. To us, this means we set a date to look again at every big plan, ask the people doing the work what has changed and drop what no longer serves the goal. We acknowledge a tension with Reliability (Changing course vs keeping our word).",
"signals": "Sticking with a plan everyone can see is failing, or treating a member who suggests a change as disloyal."
}, },
{ {
"id": "agency",
"label": "Agency", "label": "Agency",
"meaning": "Every individual possesses the power and capacity to act on their own behalf. To us, this means Members choose their own tasks and define their own working hours. We acknowledge a tension with Alignment (Doing what you want vs pulling together).", "family": "stay-free",
"signals": "Managers micromanaging details that should be up to the individual or requiring approval for minor decisions that slows down work unnecessarily." "meaning": "Every member can act on their own judgment without waiting for permission. To us, this means members make small and reversible calls on their own and tell the group what they did, and anyone who takes on work gets the say and the tools to do it. We acknowledge a tension with Cooperation (Acting alone vs pulling together).",
}, "signals": "Members asking permission for every small task, work handed out without the say to do it, or one person second-guessing every choice others make."
{
"label": "Altruism",
"meaning": "We act out of a genuine concern for the well-being of others without expecting reward. To us, this means we offer services for free or pay-what-you-can, prioritizing need over ability to pay. We acknowledge a tension with Sustainability (Giving it all away vs keeping the lights on).",
"signals": "Only offering assistance when it provides a public relations benefit or expecting recipients of aid to perform public gratitude."
}, },
{ {
"id": "anti-oppression",
"label": "Anti-oppression", "label": "Anti-oppression",
"meaning": "We actively oppose all forms of systemic discrimination and hierarchy. To us, this means we have a dedicated diversity officer and mandatory bias training. We acknowledge a tension with Comfort (Challenging norms is uncomfortable).", "family": "make-room",
"signals": "Dismissing concerns about bias as irrelevant politics or refusing to examine how organizational structures replicate systemic inequalities." "meaning": "We work against racism, sexism, ableism and every other system that ranks people, inside the group as well as outside it. To us, this means we look each year at who holds roles, who gets heard and who leaves, and we change the practices that keep the same people on top. We acknowledge a tension with Comfort (Naming harm vs keeping everyone at ease).",
"signals": "Dismissing a concern about bias as politics, the same kind of people holding every role year after year, or members who raise harm being treated as the problem."
}, },
{ {
"id": "autonomy",
"label": "Autonomy", "label": "Autonomy",
"meaning": "The group maintains independence from external control by governments or corporations. To us, this means we refuse funding that comes with strings attached or veto power. We acknowledge a tension with Coordination (Independence vs standardization).", "family": "stay-free",
"signals": "Accepting funding that comes with strings attached such as donor veto power or allowing external partners to dictate internal strategy." "meaning": "Each part of the group runs its own work without asking the whole group for permission. To us, this means every working group, crew or chapter decides how to do the work it was given, within the limits our Rule sets, and the rest of us step in only when a decision reaches beyond it. We acknowledge a tension with Fairness (Each crew’s own way vs the same rules for all).",
"signals": "A working group that has to ask before every choice, or a central few who overrule the people closest to the work on matters that touch nobody else."
}, },
{ {
"label": "Capacity Building", "id": "capacity-building",
"meaning": "We focus on teaching skills so that members become stronger and more capable over time. To us, this means we dedicate 20% of the budget to training and mentorship programs. We acknowledge a tension with Efficiency (Training takes time away from doing).", "label": "Capacity building",
"signals": "Cutting the training budget immediately when funds are tight or expecting junior members to perform without providing any mentorship." "family": "learn-and-last",
"meaning": "We teach each other skills so that more of us can do every job. To us, this means each new member is paired with someone who has done the work, we set aside time to train, and no job depends on a skill only one person has. We acknowledge a tension with Speed (Teaching it vs doing it ourselves).",
"signals": "Training dropped the moment things get busy, new members left to guess, or the same one person always doing the hard job because nobody else was shown how."
}, },
{ {
"label": "Collaboration", "id": "common-ownership",
"meaning": "We work together to solve problems rather than competing against one another. To us, this means we default to 'Multi-player mode' on documents and credit all contributors. We acknowledge a tension with Speed (Group work is slower than solo work).", "label": "Common ownership",
"signals": "Individuals hoarding information to make themselves indispensable or taking sole credit for work that was the result of a team effort." "family": "share-work",
"meaning": "What we build together belongs to all of us, not to investors or to whoever started it. To us, this means shared money, tools and equipment are held for the group, every member has an equal share in deciding what happens to them, and nobody can sell them off alone. We acknowledge a tension with Growth (Owning it ourselves vs taking outside money to grow).",
"signals": "A few founders who keep the votes or the assets, shared accounts or equipment held by one person with nothing written down, or a sale of shared things members hear about afterward."
}, },
{ {
"label": "Common Ownership", "id": "community-care",
"meaning": "Resources and assets are owned collectively by the members rather than by private investors. To us, this means Assets are held in a trust or cooperative structure where every member has a share. We acknowledge a tension with Investment Speed (Harder to raise VC capital).", "label": "Community care",
"signals": "Structuring the organization so that a small group of founders retains all voting power and profits despite claims of being a collective." "family": "treat-each-other",
"meaning": "We put each other’s health and rest ahead of the work. To us, this means we ask at each meeting who has room this week and who needs cover, anyone can step back when they are sick or worn out without explaining, and we share out hard jobs so nobody carries them alone. We acknowledge a tension with Urgency (Rest now vs the next crisis).",
"signals": "Praise for working while sick, the same few people carrying every crisis, or members who burn out and leave without anyone asking how they were."
}, },
{ {
"label": "Community Care", "id": "competence",
"meaning": "We prioritize the physical and mental health of our members above productivity. To us, this means we start meetings with check-ins and fund mental health stipends. We acknowledge a tension with Productivity (Rest takes time away from output).", "label": "Competence",
"signals": "Praising individuals for working while sick or scheduling mandatory meetings during times that are ostensibly reserved for rest and recovery." "family": "learn-and-last",
"meaning": "We do the work well and take on only what we are trained to do. To us, this means anyone doing a job that carries risk, such as handling food, giving first aid, driving or using power tools, first learns how from someone who knows, and we say no rather than guess. We acknowledge a tension with Inclusion (Doing it right vs letting anyone help).",
"signals": "Members doing risky work nobody showed them how to do, care or advice given beyond anyone’s training, or a job taken on because saying no felt awkward."
}, },
{ {
"label": "Conflict Resolution", "id": "confidentiality",
"meaning": "We address disagreements openly and constructively to find a path forward. To us, this means we have a standing mediation committee and clear grievance forms. We acknowledge a tension with Harmony (Addressing conflict feels disruptive).", "label": "Confidentiality",
"signals": "Ignoring interpersonal disputes in the hope they will disappear or labeling members who raise valid concerns as troublemakers to silence them." "family": "keep-safe",
}, "meaning": "What someone tells us in confidence stays with the people who need to know it. To us, this means a sensitive matter about one person goes to the smallest circle that can handle it, and our records say a decision was made without the private details. We acknowledge a tension with Accountability (Keeping a confidence vs letting members check the decision).",
{ "signals": "A member’s health, money trouble or conflict discussed in the group chat, minutes that name who said what in a hard case, or private details passed on as gossip."
"label": "Consent",
"meaning": "Participation and interactions are always voluntary and based on clear permission. To us, this means we use 'opt-in' by default for all communications and initiatives. We acknowledge a tension with Speed (Asking everyone takes time).",
"signals": "Automatically subscribing members to lists without asking or pressuring individuals to take on tasks they have already explicitly declined."
}, },
{ {
"id": "consensus",
"label": "Consensus", "label": "Consensus",
"meaning": "We strive to make decisions that everyone can agree to or at least live with. To us, this means we do not move forward with a proposal until all blocking concerns are resolved. We acknowledge a tension with Action/Speed (Everyone agreeing takes forever).", "family": "share-power",
"signals": "Pushing a decision forward despite unresolved blocking concerns or using social pressure to force dissenters to conform to the majority." "meaning": "We make decisions that every member can live with. To us, this means we ask every member before a big decision and change a proposal until no member has a concern serious enough to block it. We acknowledge a tension with Speed (Everyone on board vs deciding in time).",
"signals": "Pushing a proposal through while a serious concern still stands, or pressing the members who object until they go quiet."
}, },
{ {
"label": "Constructive Feedback", "id": "consent",
"meaning": "Criticism is offered kindly to help improve the work rather than to tear people down. To us, this means we use the 'Praise-Improve-Praise' sandwich and focus on the work, not the person. We acknowledge a tension with Politeness (Honesty can hurt feelings).", "label": "Consent",
"signals": "Delivering harsh criticism without offering actionable advice for improvement or attacking a person's character rather than their work." "family": "keep-safe",
"meaning": "Nobody is touched, recorded, named or signed up for anything without saying yes first, except that we help someone in danger who cannot answer. To us, this means we ask before we touch anyone, take their photo, share their story or add them to a list, and a no needs no reason and costs nothing. We acknowledge a tension with Outreach (Asking first vs showing our work).",
"signals": "Photos of people at an event posted without asking, members added to lists or chats they never chose, or pressure on someone who already said no."
}, },
{ {
"id": "constructive-feedback",
"label": "Constructive feedback",
"family": "learn-and-last",
"meaning": "We tell each other how the work is going, plainly and kindly, so it gets better. To us, this means we give feedback soon, in private first, about the work and not the person, with one thing to try next, and we ask for it as often as we give it. We acknowledge a tension with Harmony (Saying it now vs keeping the peace).",
"signals": "Criticism saved up and dropped in a meeting, comments about someone’s character instead of their work, or feedback that only ever flows from longtime members to new ones."
},
{
"id": "contribution",
"label": "Contribution",
"family": "share-work",
"meaning": "Members who do the work have a say in how it gets done, and every kind of work counts. To us, this means the people doing a job shape it, we count care, cleanup and note-taking as much as visible work, and we check that say does not simply follow free time or confidence. We acknowledge a tension with Equality (Say from the work vs an equal say for all).",
"signals": "Influence that tracks confidence, seniority or free time rather than the work done, or cooking, cleaning and care that never count when the group decides who has a say."
},
{
"id": "cooperation",
"label": "Cooperation", "label": "Cooperation",
"meaning": "We believe working together produces better results than working in isolation. To us, this means we actively partner with similar organizations instead of competing for market share. We acknowledge a tension with Competition (Sharing the pie vs taking the pie).", "family": "stand-together",
"signals": "Viewing similar organizations as threats to be destroyed rather than partners or refusing to share resources that could help the broader ecosystem." "meaning": "We work with each other and with other groups instead of against them. To us, this means we share credit for work done together, share what we know with groups doing similar work and look for a partner before we compete with one. We acknowledge a tension with Independence (Joining forces vs going our own way).",
"signals": "Taking sole credit for shared work, hoarding contacts or supplies another group could use, or treating a group doing similar work as a rival to beat."
}, },
{ {
"id": "copyleft",
"label": "Copyleft", "label": "Copyleft",
"meaning": "We use licensing that ensures derived works remain free and open for everyone. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "family": "share-knowledge",
"signals": "Releasing modifications to open projects under restrictive licenses that prevent the community from benefiting from the improvements." "meaning": "We share what we make on the condition that anything built from it is shared the same way. To us, this means we release our code, writing and designs under share-alike licenses such as the GPL or CC BY-SA, and anyone who builds on them keeps their version open too. We acknowledge a tension with Adoption (Keeping it open vs more people using it).",
"signals": "Improvements to shared work released under closed terms, a share-alike license quietly swapped for one that lets others close it, or our work taken private without credit."
}, },
{ {
"id": "decentralization",
"label": "Decentralization", "label": "Decentralization",
"meaning": "Power is distributed across a network rather than concentrated in a central authority. To us, this means Decision-making authority is pushed to the edges no central HQ exists. We acknowledge a tension with Efficiency/Cohesion (Herding cats).", "family": "stay-free",
"signals": "Concentrating all critical decision-making power in a single founder or small inner circle while claiming to be a flat organization." "meaning": "Power and know-how are spread across many people and places so no center can control the group. To us, this means new crews or chapters can start and connect with each other without asking permission, and no one person, office or chat holds what everyone depends on. We acknowledge a tension with Coordination (Spreading power out vs acting as one).",
"signals": "One founder or inner circle making every real decision, every account and key held by one person, or a group that calls itself flat while a few people run it."
}, },
{ {
"label": "Direct Action", "id": "democracy",
"meaning": "We take immediate steps to solve problems ourselves instead of asking authorities to do it. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "label": "Democracy",
"signals": "Forming a committee to study a problem endlessly instead of taking immediate steps to address the urgent needs of the community." "family": "share-power",
"meaning": "Members govern the group together and each has an equal vote on what binds everyone. To us, this means an equal say for every member on the big questions, one member one vote when we vote and roles that answer to the members. Any member can see and question a decision made in our name and put a question to everyone. We acknowledge a tension with Leadership (Everyone deciding vs trusting the people we chose).",
"signals": "A few members making binding decisions and telling the rest afterward, a vote called when most members cannot attend, or no way for a member to put a question to everyone."
}, },
{ {
"id": "dignity",
"label": "Dignity",
"family": "treat-each-other",
"meaning": "Everyone deserves to be treated as a whole person, most of all when they need help. To us, this means nobody has to prove they deserve help or a fair hearing, people choose for themselves what they need, and no task is beneath any of us. We acknowledge a tension with Fairness (Taking people at their word vs making sure everyone gets a share).",
"signals": "Asking people to prove they need help, telling someone’s hardship to raise money without asking them, or handing the dull and dirty jobs to the same few people."
},
{
"id": "direct-action",
"label": "Direct action",
"family": "stand-together",
"meaning": "We meet needs and change conditions ourselves instead of waiting for those in power to act. To us, this means when people need food, a ride or a repair we organize to provide it now, and we fix what we can ourselves instead of waiting for permission. We acknowledge a tension with Safety (Acting now vs the risks we take on).",
"signals": "A problem studied for months while people go without, action taken without asking who carries the risk or every fix waiting on a landlord, agency or funder to approve it."
},
{
"id": "diversity",
"label": "Diversity", "label": "Diversity",
"meaning": "We value a wide range of perspectives and backgrounds within our membership. To us, this means we strictly enforce quotas or targets for representation in leadership. We acknowledge a tension with Homophily/Ease (It's easier to work with people like you).", "family": "make-room",
"signals": "Hiring primarily for culture fit which results in a homogenous team or having a leadership team that lacks representation from marginalized groups." "meaning": "We want people of many backgrounds, ages and experiences in the group and in its roles. To us, this means we notice who is missing, go where they already are to invite them, and invite newcomers of every background into roles, without keeping records of anyone’s identity. We acknowledge a tension with Ease (Reaching new people vs working with people like us).",
"signals": "A group that looks the same year after year, roles that only ever go to one kind of person, or new members chosen because they seem to fit in."
}, },
{ {
"id": "documentation",
"label": "Documentation", "label": "Documentation",
"meaning": "We record our processes clearly so that others can learn from and duplicate our work. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "family": "share-knowledge",
"signals": "Relying on oral tradition where only long-time members understand the systems or refusing to write down processes because it takes too much time." "meaning": "We write down what we know so nobody has to depend on one person’s memory. To us, this means every decision, role and how-to has a short written note in our records, and anyone who learns how something works writes it down for the next person. We acknowledge a tension with Privacy (Writing it down vs leaving no trail).",
"signals": "Systems only longtime members understand, decisions nobody can find a month later, or one person who is the only one who knows how something works."
}, },
{ {
"id": "education",
"label": "Education", "label": "Education",
"meaning": "We are committed to the continuous learning and growth of all members. To us, this means we publish open curriculums and host free workshops for the public. We acknowledge a tension with Action (Learning vs Doing).", "family": "learn-and-last",
"signals": "Treating questions from newcomers as annoyances or creating an environment where members are afraid to admit they do not know something." "meaning": "We learn together, from study and from our own mistakes, and share what we learn. To us, this means we make time to study the conditions behind our work, look back after each big effort to ask what we would do differently and share what we learn with members and other groups. We acknowledge a tension with Action (Studying it vs doing it).",
"signals": "Questions from new members treated as a nuisance, mistakes hidden instead of talked through, or no time ever set aside to read, reflect or pass on what the group has learned."
}, },
{ {
"id": "empathy",
"label": "Empathy", "label": "Empathy",
"meaning": "We strive to understand and share the feelings of others in our community. To us, this means we prioritize user research and listening tours before building solutions. We acknowledge a tension with Objectivity (Feeling with users vs analyzing data).", "family": "treat-each-other",
"signals": "Dismissing user feedback that conflicts with internal assumptions or prioritizing data metrics over the actual lived experience of people." "meaning": "We try to understand how things feel for the people a decision touches. To us, this means we listen to the people a decision affects before we make it, ask how a change lands on members with the least time or money, and take their answer seriously. We acknowledge a tension with Objectivity (Feeling with people vs weighing the facts).",
"signals": "Decisions made about people without asking them, feelings dismissed as drama, or a change that lands hardest on the members with the least time or money while nobody notices."
}, },
{ {
"label": "Empowerment", "id": "equality",
"meaning": "We give members the authority and resources to take charge of their own work. To us, this means we provide resources and authority to those closest to the problem. We acknowledge a tension with Control (Letting go means risking mistakes).", "label": "Equality",
"signals": "Delegating responsibility without giving the corresponding authority to make decisions or second-guessing every choice made by the team." "family": "share-power",
"meaning": "Every member stands on equal footing whatever their money, title or time in the group. To us, this means no member’s word or vote counts for more because of seniority, wealth or a role, every member gets the same news about decisions and money and every role is open to any member willing to learn it. We acknowledge a tension with Equity (The same for everyone vs more for those who need it).",
"signals": "Longtime or well-off members whose words carry more weight in practice, roles that only some members are ever offered, or information that only some members get to see."
}, },
{ {
"id": "equity",
"label": "Equity", "label": "Equity",
"meaning": "We distribute resources based on need to ensure fair outcomes for everyone. To us, this means we use sliding scale pricing and offer scholarships to level the playing field. We acknowledge a tension with Equality (Different treatment vs same treatment).", "family": "make-room",
"signals": "Treating everyone exactly the same despite different starting points or refusing to accommodate specific needs under the guise of fairness." "meaning": "We share out resources and effort by need so everyone can take part on fair terms. To us, this means sliding scales and waivers for anything that costs money, rides and shared child care so people can attend, and more support for members who start with less. We acknowledge a tension with Equality (Giving by need vs treating everyone alike).",
"signals": "Treating everyone exactly the same despite very different starting points, fees with no waiver, or meetings that only people with a car and free evenings can reach."
}, },
{ {
"label": "Experimentation", "id": "fair-pay",
"meaning": "We encourage trying new things and learning from failure rather than sticking to the safe path. To us, this means we celebrate 'smart failures' as learning opportunities. We acknowledge a tension with Reliability (New things break).", "label": "Fair pay",
"signals": "Punishing members for experiments that do not succeed or requiring guaranteed success before approving any new initiative." "family": "share-work",
"meaning": "Anyone we pay for work is paid fairly, and nobody is left out of pocket for doing the group’s work. To us, this means we pay at least a living wage when we pay anyone, members can see how pay is set, the gap between the highest and lowest pay stays small, and we pay back what members spend on the group’s work. We acknowledge a tension with Sustainability (Paying fairly vs a budget that lasts).",
"signals": "Expecting people to work for exposure or passion, pay nobody can explain, or members quietly paying for supplies and gas out of their own pockets."
}, },
{ {
"id": "fairness",
"label": "Fairness", "label": "Fairness",
"meaning": "We apply rules and distribute resources in a just and impartial manner. To us, this means we use transparent salary bands and objective criteria for promotions. We acknowledge a tension with Favoritism (Treating everyone same vs helping friends).", "family": "share-work",
"signals": "Creating special rules for friends of the leadership or applying policies inconsistently depending on who is involved." "meaning": "We apply our rules the same way to everyone and share out work and resources by terms anyone can see. To us, this means the same rule binds a founder and a newcomer, shifts and shared things are handed out by a method we agreed in advance, and any member can ask how a choice was made. We acknowledge a tension with Friendship (Same rules for all vs looking after our own).",
}, "signals": "Special treatment for friends of longtime members, rules enforced on some people and not others, or shifts and shared things handed out with no method anyone can explain."
{
"label": "Fair Remuneration",
"meaning": "Workers are paid a fair wage that reflects the true value of their labor. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Expecting members to work for exposure or passion instead of paying a living wage for their labor."
}, },
{ {
"id": "flexibility",
"label": "Flexibility", "label": "Flexibility",
"meaning": "We accommodate the different needs and schedules of our members. To us, this means we allow remote work and asynchronous schedules. We acknowledge a tension with Predictability (Asynch work makes scheduling hard).", "family": "make-room",
"signals": "Penalizing members for using flexible work arrangements or judging commitment based on the number of hours physically present in the office." "meaning": "We make room for members’ different schedules, bodies and ways of taking part. To us, this means members can join in person, by phone or in writing, take on work that fits the time they have and change how much they give as their lives change. We acknowledge a tension with Reliability (Fitting the work to our lives vs counting on each other).",
"signals": "Judging commitment by hours in the room, decisions made only at meetings some members can never reach, or members penalized for doing less during a hard season."
}, },
{ {
"id": "forkability",
"label": "Forkability", "label": "Forkability",
"meaning": "Anyone has the right to take the work in a new direction if the current path fails. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "family": "stay-free",
"signals": "Legally threatening or socially shunning a group that decides to take the project in a different direction due to disagreements." "meaning": "Anyone can take our shared work in a new direction if they disagree with where it is going. To us, this means our code, documents and how-tos stay under open licenses and in formats others can open, and anyone who leaves to start something new can take a copy of that work with our goodwill, though never members’ personal details. We acknowledge a tension with Solidarity (Freedom to split vs staying together).",
"signals": "Threats or shunning aimed at people who start their own version, shared work locked in a tool only one person controls, or records kept in formats nobody else can open."
}, },
{ {
"id": "freedom",
"label": "Freedom", "label": "Freedom",
"meaning": "We protect the liberty of individuals to express themselves and act as they choose. To us, this means we release our work under copyleft licenses. We acknowledge a tension with Security (Total freedom includes freedom to harm).", "family": "stay-free",
"signals": "Censoring speech that is critical of the leadership or implementing surveillance tools to monitor member activity." "meaning": "Members are free to speak, disagree and live as they choose, inside the group and out. To us, this means anyone can criticize a decision or a role holder in the open, nobody watches or polices members’ lives outside the work, and anyone can leave without penalty. We acknowledge a tension with Safety (Freedom to act vs protecting people from harm).",
"signals": "Criticism of role holders shut down or punished, members watched or judged for their lives outside the group, or tests of ideological purity for joining."
}, },
{ {
"label": "Freedom of Information", "id": "freedom-of-information",
"meaning": "Information is a public good that should not be hidden behind paywalls. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "label": "Freedom of information",
"signals": "Locking important organizational knowledge behind paywalls or restricting access to documents based on arbitrary hierarchy." "family": "share-knowledge",
"meaning": "Knowledge that helps people meet their needs should be free to read and use. To us, this means the guides and how-tos we choose to share are free for anyone, and any member who asks for a record that affects them gets it with personal details left out. We acknowledge a tension with Safety (Sharing what we know vs who else could use it).",
"signals": "Useful guides kept behind a fee or a login, members refused the record of a decision that affects them, or knowledge rationed to keep some members on top."
}, },
{ {
"id": "generosity",
"label": "Generosity", "label": "Generosity",
"meaning": "We give freely of our time and resources to support the collective goal. To us, this means we share our surplus resources with aligned groups in need. We acknowledge a tension with Self-Preservation (Giving until it hurts).", "family": "stand-together",
"signals": "Hoarding a surplus of resources while allied groups are struggling to survive or viewing every interaction as a transaction to be maximized." "meaning": "We give our time, skills and anything we have to spare freely and without keeping score. To us, this means we lend our tools and skills, share surplus with groups that need it and help without asking for thanks or a return. We acknowledge a tension with Sustainability (Giving freely vs keeping enough to last).",
"signals": "Surplus hoarded while allied groups struggle, help offered only when it brings good press, or people expected to show gratitude for what they receive."
}, },
{ {
"label": "Harm Reduction", "id": "harm-reduction",
"meaning": "We focus on minimizing negative consequences associated with risky behaviors or systemic issues. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "label": "Harm reduction",
"signals": "Insisting on abstinence-only approaches to risk or refusing to provide safety equipment because it might encourage risky behavior." "family": "keep-safe",
}, "meaning": "We meet people where they are and make risky situations safer without judging anyone. To us, this means we share safety supplies and accurate information freely, never make help depend on someone changing first and learn how to respond in an emergency. We acknowledge a tension with Caution (Meeting people where they are vs protecting the group from risk).",
{ "signals": "Help that depends on someone quitting first, lectures handed out in place of supplies, or people turned away because of how they live or what they use."
"label": "Holism",
"meaning": "We consider the whole person and the whole system rather than just isolated parts. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Ignoring the personal context of a member and treating them solely as a unit of production or optimizing one department at the expense of others."
},
{
"label": "Holocracy",
"meaning": "We organize around roles and circles rather than top-down managers. To us, this means we use circles and roles instead of managers and job titles. We acknowledge a tension with Clarity (Roles can be confusing compared to titles).",
"signals": "Reintroducing hidden hierarchies where influence is based on social capital rather than clear roles or ignoring governance processes."
}, },
{ {
"id": "honesty",
"label": "Honesty", "label": "Honesty",
"meaning": "We tell the truth even when it is difficult or uncomfortable. To us, this means we refuse to spin bad news and admit when we don't know the answer. We acknowledge a tension with Diplomacy (Blunt truth vs tact).", "family": "treat-each-other",
"signals": "Hiding bad news from the membership to protect morale or giving vague non-answers when asked direct questions about challenges." "meaning": "We tell each other the truth, especially when it is hard to hear. To us, this means we share bad news early, admit when we do not know or got it wrong and give a straight answer to a direct question. We acknowledge a tension with Kindness (Saying it plainly vs saying it gently).",
"signals": "Bad news kept from members to protect morale, vague answers to direct questions about money or problems, or mistakes quietly covered up and never mentioned again."
}, },
{ {
"id": "horizontalism",
"label": "Horizontalism", "label": "Horizontalism",
"meaning": "We organize as equals without bosses or top-down management structures. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "family": "share-power",
"signals": "Creating informal power structures where a shadow board makes the real decisions outside of the collective process." "meaning": "We organize as equals with no bosses, and power stays with the members. To us, this means decisions are made by the people they affect, roles rotate, and we notice when someone becomes an informal leader and hand their influence back to the group. We acknowledge a tension with Speed (Shared power vs quick calls).",
"signals": "A few people who make the real decisions outside the group’s process, roles held for years with nobody else trained to take them, or one voice the room always waits for."
}, },
{ {
"id": "humility",
"label": "Humility", "label": "Humility",
"meaning": "We recognize that we do not have all the answers and are open to learning from others. To us, this means Leaders eat last and credit the team for successes. We acknowledge a tension with Marketing (Selling yourself vs being modest).", "family": "treat-each-other",
"signals": "Leaders taking credit for the team's success while blaming the team for failures or refusing to admit when they are wrong." "meaning": "We know we do not have all the answers and learn from the people and groups who came before us. To us, this means we credit the work of others, ask for advice before we act on what we do not know and say plainly when we were wrong. We acknowledge a tension with Confidence (Staying open vs standing firm).",
"signals": "Role holders who take credit for shared work, advice from outside the group dismissed out of hand, or nobody ever admitting a mistake."
}, },
{ {
"id": "inclusion",
"label": "Inclusion", "label": "Inclusion",
"meaning": "We proactively welcome people who are often marginalized or excluded. To us, this means we proactively invite marginalized voices to the planning table. We acknowledge a tension with Exclusivity/Focus (Big tent vs niche group).", "family": "make-room",
"signals": "Holding meetings in spaces or at times that are inaccessible to working parents or people with different schedules." "meaning": "We make sure people who are often left out belong here and have a real say. To us, this means we ask who is missing and what keeps them away, change our times, places and habits to fit them and make sure new and quiet members are heard before we decide. We acknowledge a tension with Focus (A big tent vs a clear purpose).",
}, "signals": "Inside jokes and jargon that leave newcomers lost, the same voices filling every meeting, or people who came once and never came back with nobody asking why."
{
"label": "Inclusivity",
"meaning": "We actively welcome people who have been historically marginalized or excluded. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Using jargon that alienates newcomers or failing to provide translation for members who speak different languages."
}, },
{ {
"id": "independence",
"label": "Independence", "label": "Independence",
"meaning": "We avoid reliance on any single funding source or authority figure. To us, this means we maintain a diversified funding base to avoid dependency on one donor. We acknowledge a tension with Resources (Going it alone means less help).", "family": "stay-free",
"signals": "Relying on a single large donor who threatens to pull funding if the organization takes a political stance they dislike." "meaning": "We avoid depending on any one funder, platform, landlord or person so that nobody outside the group can steer it. To us, this means we draw our money from more than one source, keep a backup for every tool and space we rely on, and turn down offers that come with a say over our choices. We acknowledge a tension with Interdependence (Standing on our own vs leaning on others).",
"signals": "One funder or platform the group could not survive losing, a partner who sets our priorities, or plans quietly dropped because someone outside might object."
}, },
{ {
"id": "innovation",
"label": "Innovation", "label": "Innovation",
"meaning": "We value new ideas and creative solutions over tradition and established methods. To us, this means we reward novel approaches even if they disrupt current workflows. We acknowledge a tension with Tradition (New vs Old).", "family": "learn-and-last",
"signals": "Shutting down new ideas with the excuse that things have always been done a certain way or refusing to allocate resources to test new concepts." "meaning": "We try new ways of working and learn from what does not work. To us, this means any member can propose a trial for a set time with a date to look again, and a trial that fails counts as a lesson rather than a mark against anyone. We acknowledge a tension with Maintenance (Building something new vs tending what we have).",
"signals": "New ideas shut down because it has always been done another way, members blamed when a trial does not work, or nothing ever tried without a guarantee it will succeed."
}, },
{ {
"id": "integrity",
"label": "Integrity", "label": "Integrity",
"meaning": "We align our actions with our stated values even when no one is watching. To us, this means we refuse contracts that violate our ethical standards, regardless of profit. We acknowledge a tension with Profit/Opportunity (Saying no to easy money).", "family": "treat-each-other",
"signals": "Accepting a partnership that violates the group's core values because the financial offer is too good to refuse." "meaning": "We act on our values even when it costs us and nobody is watching. To us, this means we turn down money, partners and shortcuts that would have us break our own Rule, and we say so openly when we fall short of it. We acknowledge a tension with Sustainability (Holding to our values vs keeping the group afloat).",
"signals": "A grant or partnership accepted that goes against what the group says it stands for, or values written in our Rule that appear nowhere in how we act."
}, },
{ {
"id": "interdependence",
"label": "Interdependence", "label": "Interdependence",
"meaning": "We recognize that our survival and success are tied to the well-being of others. To us, this means we map our supply chain to ensure we support local ecosystems. We acknowledge a tension with Self-Sufficiency (Relying on others vs prepping).", "family": "stand-together",
"signals": "Acting as if the organization exists in a vacuum and ignoring the impact of its actions on the surrounding community or ecosystem." "meaning": "Our well-being is bound up with the people, groups and land around us. To us, this means before a big decision we ask how it will land on members’ lives outside the work, on neighboring groups and on the place we share, and we build ties we can lean on in hard times. We acknowledge a tension with Independence (Leaning on others vs standing on our own).",
"signals": "Acting as if the group stands alone, fixing one problem by pushing it onto members’ families or a neighboring group, or ignoring what our work does to the place we share."
}, },
{ {
"id": "interoperability",
"label": "Interoperability", "label": "Interoperability",
"meaning": "Our tools and systems are designed to work seamlessly with other open systems. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "family": "share-knowledge",
"signals": "Building walled gardens that trap users or deliberately breaking compatibility with other open tools to lock people in." "meaning": "Our tools and records work with other people’s tools so nobody gets locked in. To us, this means we choose software that exports in common formats and connects with what our partners already use, so members and allied groups can join in without buying anything new. We acknowledge a tension with Simplicity (Open tools vs the apps members already know).",
"signals": "Records trapped in an app nobody can export from, tools that shut out partner groups, or members forced onto a new platform just to keep taking part."
}, },
{ {
"id": "intersectionality",
"label": "Intersectionality", "label": "Intersectionality",
"meaning": "We consider how different forms of discrimination overlap to affect individuals. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "family": "make-room",
"signals": "Focusing on a single issue like class while ignoring how race and gender compound the experience of oppression for some members." "meaning": "We pay attention to how racism, sexism, class, disability and other kinds of discrimination stack up in one person’s life. To us, this means before we decide we ask who is missing from the decision and who it will burden most, and we listen first to members who face more than one of them. We acknowledge a tension with Simplicity (Many different needs vs one rule for all).",
"signals": "Treating one issue such as class or race as the only one that matters, or decisions that help most members while landing hardest on those who already face the most."
}, },
{ {
"id": "joy",
"label": "Joy",
"family": "treat-each-other",
"meaning": "We make room for fun, food and celebration in the work. To us, this means we share meals, mark wins and the end of someone’s term in a role, and keep meetings human enough that people want to come back. We acknowledge a tension with Seriousness (Time for fun vs the weight of the work).",
"signals": "Every meeting run as a crisis, wins that pass without anyone noticing, or members who describe the work only as a duty they cannot put down."
},
{
"id": "justice",
"label": "Justice", "label": "Justice",
"meaning": "We seek to repair past wrongs and create a fair system for the future. To us, this means we prioritize restorative processes over punitive ones. We acknowledge a tension with Mercy (Strict rules vs forgiveness).", "family": "stand-together",
"signals": "Focusing on punishment and exclusion rather than healing and restoration or applying rules more strictly to marginalized members." "meaning": "We work for a world where everyone has what they need and nobody’s needs count for less. To us, this means we name the causes behind the needs we meet, side with the people most harmed by them and judge our work by whether those conditions change. We acknowledge a tension with Inclusion (Taking a side vs welcoming everyone).",
"signals": "Meeting the same needs year after year without asking why they exist, staying quiet about the causes to keep a funder happy, or rules applied hardest to members with the least."
}, },
{ {
"label": "Knowledge Sharing", "id": "labor-rights",
"meaning": "We freely distribute information so that everyone can benefit from it. To us, this means we document everything in a public wiki. We acknowledge a tension with Intellectual Property (Open source vs patent).", "label": "Labor rights",
"signals": "Creating silos where information is hoarded to maintain power or refusing to document work so that others become dependent on a single expert." "family": "share-work",
}, "meaning": "We stand for every worker’s right to organize and speak up about their conditions, inside our group and beyond it. To us, this means anyone who works with us can talk openly about pay, hours and safety, nobody is punished for organizing and we honor strikes and picket lines that workers call. We acknowledge a tension with Reliability (Honoring a strike vs keeping our commitments).",
{ "signals": "Members discouraged from comparing pay or conditions, anyone who raises a workplace concern losing shifts or standing, or the group crossing a picket line to save time or money."
"label": "Labor Rights",
"meaning": "We uphold the right of all workers to organize and advocate for their conditions. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.",
"signals": "Discouraging members from discussing their pay or working conditions or retaliating against those who try to organize."
}, },
{ {
"id": "leadership",
"label": "Leadership", "label": "Leadership",
"meaning": "We encourage everyone to step up and take initiative when needed. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "family": "share-power",
"signals": "Waiting for permission to solve an obvious problem or complaining about an issue without offering to help fix it." "meaning": "Anyone can step up to lead, and every lead is a job the group hands out for a time. To us, this means whoever leads a task does it with a mandate from the group, shares the work as they go and trains someone to take over when their term ends. We acknowledge a tension with Horizontalism (Stepping up vs staying equal).",
}, "signals": "Waiting for someone else to fix an obvious problem, the same person leading for years with nobody ready to take over, or a lead who decides beyond what the group asked of them."
{
"label": "Learning",
"meaning": "We embrace mistakes as opportunities to grow rather than failures to be punished. To us, this means we fund conference tickets and book clubs for members. We acknowledge a tension with Performance (Study time vs work time).",
"signals": "Viewing mistakes as a sign of incompetence rather than a learning opportunity or cutting the budget for personal development."
},
{
"label": "Liberty",
"meaning": "We oppose authoritarian control and support the freedom of the individual. To us, this means we defend the right of members to fork the project if they disagree. We acknowledge a tension with Order (Chaos vs rules).",
"signals": "Imposing strict rules on personal behavior that has no impact on the work or demanding ideological purity tests for membership."
}, },
{ {
"id": "localism",
"label": "Localism", "label": "Localism",
"meaning": "We prioritize local needs and resources over global or distant ones. To us, this means we source supplies from within a 50-mile radius. We acknowledge a tension with Globalism/Scale (Sourcing near vs sourcing cheap).", "family": "stand-together",
"signals": "Importing goods that could be sourced locally to save a small amount of money or ignoring the needs of the immediate neighborhood." "meaning": "We put the needs, people and resources of the place we live first. To us, this means we organize where we live and work, buy from and partner with nearby people and small businesses when we can and keep our money moving close to home. We acknowledge a tension with Interdependence (Rooted here vs connected to groups far away).",
"signals": "Buying from far away to save a little when a neighbor could supply it, or plans made for the region that skip what the people next door actually need."
}, },
{ {
"id": "maintenance",
"label": "Maintenance", "label": "Maintenance",
"meaning": "We value the unglamorous work of sustaining and fixing what we have already built. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "family": "learn-and-last",
"signals": "Celebrating only new feature launches while neglecting the critical infrastructure that keeps the organization running." "meaning": "We value the unglamorous work that keeps things running, and we share it. To us, this means repairs, updates, cleaning and admin are planned, shared out and thanked like any other work, and we fix what we have before we start something new. We acknowledge a tension with Innovation (Tending what exists vs building what is new).",
"signals": "Celebrating new projects while the tools, records and spaces everyone relies on fall apart, or upkeep left to the same one or two people without thanks."
}, },
{ {
"label": "Mentorship", "id": "managing-conflict",
"meaning": "Experienced members actively guide and support newcomers. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "label": "Managing conflict",
"signals": "Senior members refusing to spend time training juniors or creating a culture where asking for help is seen as weakness." "family": "treat-each-other",
"meaning": "We expect conflict and handle it early, in the smallest room that can hold it. To us, this means we go to the person rather than around them unless that is unsafe, the concern is discrimination or shared money, or one of us holds power over the other, use our conflict process when talking is not enough and never pretend a conflict is over for good. We acknowledge a tension with Transparency (Keeping it in a small room vs letting everyone know).",
"signals": "Conflicts aired in the group chat before the two people have talked, quarrels left to fester until someone quits, or members who raise a concern treated as troublemakers."
}, },
{ {
"label": "Meritocracy", "id": "mutual-aid",
"meaning": "Influence is earned through active contribution and skill rather than status or wealth. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "label": "Mutual aid",
"signals": "Rewarding people based on their confidence and social status rather than the actual quality and impact of their contributions." "family": "stand-together",
}, "meaning": "We meet each other’s needs directly as equals, in solidarity and never as charity. To us, this means everyone gives what they can and takes what they need, people who receive help can help run the work and decide how it is done, and we take people at their word about what they need. We acknowledge a tension with Sustainability (Meeting every need vs not burning out).",
{ "signals": "Help that depends on proving need, a line between members who give and people who receive, or people who get help shut out of deciding how the work runs."
"label": "Mutual Aid",
"meaning": "We engage in a reciprocal exchange of resources where everyone is both a giver and a receiver. To us, this means we practice 'solidarity, not charity' by exchanging resources directly. We acknowledge a tension with Charity (Solidarity vs Tax Write-offs).",
"signals": "Creating a transactional system where help is only given with the expectation of an immediate equivalent return."
}, },
{ {
"id": "non-violence",
"label": "Non-violence", "label": "Non-violence",
"meaning": "We reject physical and emotional violence as a means to achieve our goals. To us, this means we de-escalate conflicts and refuse to engage with law enforcement where possible. We acknowledge a tension with Self-Defense (Turning cheek vs fighting back).", "family": "keep-safe",
"signals": "Using aggressive language or intimidation tactics to silence opposition or justifying harm as necessary for the greater good." "meaning": "We do not use threats, force or intimidation against each other or against people who disagree with us. To us, this means members learn to calm a tense moment, we settle disputes by talking rather than by fear, and we decide together ahead of time how we will respond if someone threatens us. We acknowledge a tension with Safety (Holding back vs defending people under threat).",
"signals": "Shouting down or threatening people who disagree, harm excused as necessary for the cause, or a tense moment made worse because nobody knew how to calm it."
}, },
{ {
"label": "Open Source", "id": "open-source",
"meaning": "We make our work freely available for others to use; study; and modify. To us, this means Observable actions that demonstrate this value. We recognize that Values often trade off with Efficiency or Speed.", "label": "Open source",
"signals": "Releasing code without documentation or using a license that claims to be open but actually restricts commercial use or modification." "family": "share-knowledge",
"meaning": "We make our work free for anyone to use, study, change and share. To us, this means we release our code, designs and guides under open licenses with what others need to use them, and we welcome contributions from outside the group and credit them. We acknowledge a tension with Sustainability (Free to use vs paid to make).",
"signals": "Code released without the source or the notes needed to use it, a license that calls itself open but blocks changes, or outside contributions ignored or taken without credit."
}, },
{ {
"id": "openness",
"label": "Openness", "label": "Openness",
"meaning": "We operate transparently and invite participation from the outside world. To us, this means All meetings are open to the public unless safety is at risk. We acknowledge a tension with Privacy/Security (Open doors let bugs in).", "family": "make-room",
"signals": "Conducting all real business in private messages and only using public meetings to rubber-stamp decisions that have already been made." "meaning": "Our door is open to anyone who can take part in our work, shares our aims and will take on the responsibilities of membership. To us, this means we write down how to join, ask every newcomer the same questions about shared commitments and never turn people away for who they are. We acknowledge a tension with Safety (An open door vs knowing who is in the room).",
"signals": "Joining that depends on knowing the right person, newcomers asked different questions depending on who they are, or no written way in at all."
}, },
{ {
"id": "participation",
"label": "Participation", "label": "Participation",
"meaning": "Democracy requires the active engagement of all members in the decision-making process. To us, this means we require active contribution to maintain voting rights. We acknowledge a tension with Efficiency (Too many cooks).", "family": "share-power",
"signals": "Creating barriers to entry that make it difficult for new members to have a voice or ignoring the input of the broader membership." "meaning": "Every member can take part in the decisions that shape the group. To us, this means we give notice before big decisions, hold them at times and in ways most members can join, and take written input from members who cannot come. We acknowledge a tension with Efficiency (Hearing from everyone vs getting things done).",
"signals": "Big decisions made at short notice or when most members cannot attend, input asked for and then ignored, or new members left out of discussions that affect them."
}, },
{ {
"id": "peer-review",
"label": "Peer review",
"family": "share-knowledge",
"meaning": "Members check each other’s work before it goes out in the group’s name. To us, this means a second member looks over code, guides, flyers and money reports before they are shared, and reviews look at the work, never the person, and go both ways. We acknowledge a tension with Agency (A second pair of eyes vs trusting members to act).",
"signals": "Work that goes out in the group’s name with nobody else having seen it, reviews used to keep newcomers out, or the same one person approving everything."
},
{
"id": "prefiguration",
"label": "Prefiguration",
"family": "stand-together",
"meaning": "We build inside the group now the world we are working toward. To us, this means how we decide, share work and treat each other matches what we say we want, so we choose shared power and care inside the group even when a shortcut would be faster. We acknowledge a tension with Effectiveness (Living our values now vs what works fastest).",
"signals": "A group that fights bosses while running itself with one, means excused because the cause is urgent, or decisions made for members that the group says people should make for themselves."
},
{
"id": "privacy",
"label": "Privacy",
"family": "keep-safe",
"meaning": "We collect as little about people as we can and protect what we keep. To us, this means we ask only for the details a task needs, keep rosters, addresses and meeting places out of shared and public channels, and use encrypted tools for anything sensitive. We acknowledge a tension with Transparency (Keeping it private vs letting members see the records).",
"signals": "Sign-in sheets that ask for names nobody needs, a roster or address posted in a public chat, or personal details kept for years in tools anyone can open."
},
{
"id": "reliability",
"label": "Reliability", "label": "Reliability",
"meaning": "Members can count on one another to follow through on what they commit to. To us, this means we do what we say we will do and notify others early when plans must change. We acknowledge a tension with Flexibility (Sticking to the plan vs adapting).", "family": "learn-and-last",
"signals": "Missing deadlines without notice or making ambitious commitments the group knows it cannot keep." "meaning": "Members can count on one another to follow through on what they commit to. To us, this means we take on only what we can carry, do what we said we would and tell the group early when plans must change. We acknowledge a tension with Flexibility (Keeping our word vs adapting to what comes up).",
"signals": "Missing shifts or deadlines without telling anyone, or making commitments the group already knows it cannot keep."
}, },
{ {
"id": "resilience",
"label": "Resilience", "label": "Resilience",
"meaning": "The group can absorb shocks and keep functioning when conditions get hard. To us, this means we build redundancy into critical roles and document institutional knowledge so no single departure sinks the work. We acknowledge a tension with Efficiency (Backup capacity vs lean operations).", "family": "learn-and-last",
"signals": "Concentrating all critical knowledge in one person or ignoring warning signs of burnout until key members quit." "meaning": "The group can take a hard blow and keep going. To us, this means at least two people can do every vital job and reach every account and key, and we keep a little in reserve so that one loss, departure or bad month does not sink the work. We acknowledge a tension with Efficiency (Keeping spares vs using everything we have).",
"signals": "Everything vital known or held by one person, nothing kept in reserve, or warning signs of burnout ignored until key members quit."
}, },
{ {
"id": "respect",
"label": "Respect",
"family": "treat-each-other",
"meaning": "We treat every member as someone worth hearing, most of all when we disagree. To us, this means we listen before we answer, let people finish, keep criticism about the idea and not the person, and use the names and pronouns people ask us to use. We acknowledge a tension with Freedom (Care in how we speak vs speaking freely).",
"signals": "Talking over people, mocking a question, arguing about the person instead of the idea, or getting someone’s name or pronouns wrong after being told."
},
{
"id": "restorative-justice",
"label": "Restorative justice",
"family": "keep-safe",
"meaning": "When someone causes harm we focus on repair and change rather than punishment. To us, this means we ask first what the person harmed needs, support the person who caused harm to take responsibility and make amends, and never push the person harmed to meet them. We acknowledge a tension with Safety (Working toward repair vs keeping people apart).",
"signals": "Punishment decided before anyone asks what the person harmed needs, pressure on them to forgive or meet, or removal used as the only answer to every harm."
},
{
"id": "right-to-repair",
"label": "Right to repair",
"family": "share-knowledge",
"meaning": "Everyone should be able to fix, change and keep using the things they depend on. To us, this means we choose tools, equipment and software we can repair or change ourselves, keep manuals and spare parts where members can find them and teach each other to fix what breaks. We acknowledge a tension with Convenience (Fixing it ourselves vs buying new).",
"signals": "Equipment that only one paid service can fix, software the group cannot change or move away from, or broken tools thrown out because nobody kept the manual."
},
{
"id": "safety",
"label": "Safety",
"family": "keep-safe",
"meaning": "Everyone should be able to take part without fear of harm. To us, this means we plan for the risks of our work, act at once when someone may be unsafe, get help from outside the group when danger is immediate and treat a pause in someone’s access as a precaution rather than a finding. We acknowledge a tension with Fairness (Acting at once vs hearing everyone out first).",
"signals": "Known risks nobody plans for, a member who reports a threat and is told to work it out alone, or someone who raised a safety concern losing their place or shifts."
},
{
"id": "self-governance",
"label": "Self-governance",
"family": "share-power",
"meaning": "We write our own rules and change them together, and nobody outside the group writes them for us. To us, this means members agree to our Rule, any member can propose a change to it and we change it only by the method it names, so our rules fit how we really work. We acknowledge a tension with Cooperation (Our own rules vs what partners and the law expect).",
"signals": "Rules handed down by a founder, funder or parent group that members never agreed to, a Rule nobody can change, or written rules nobody follows in practice."
},
{
"id": "simplicity",
"label": "Simplicity",
"family": "learn-and-last",
"meaning": "We keep our rules, tools and processes as light as the work allows. To us, this means we keep our Rule short enough that every member reads it, choose tools most members already use, and drop steps that slow the work without protecting anyone. We acknowledge a tension with Documentation (Keeping it light vs writing everything down).",
"signals": "Approval steps nobody can explain, a Rule so long that nobody reads it, or new members who need a guide just to find where decisions happen."
},
{
"id": "solidarity",
"label": "Solidarity", "label": "Solidarity",
"meaning": "We stand with one another, especially when members face pressure from outside forces. To us, this means we defend members against unjust treatment and share risk collectively rather than leaving anyone exposed. We acknowledge a tension with Individualism (Collective defense vs personal autonomy).", "family": "stand-together",
"signals": "Staying silent when a member faces mistreatment or leaving vulnerable members to handle external pressure alone." "meaning": "We stand with each other and with people in struggle beyond our group, because our futures are bound together. To us, this means we defend members who face unjust treatment, share risk rather than leave anyone exposed and show up for allied groups when they ask. We acknowledge a tension with Freedom (Acting together vs each member’s own choice).",
"signals": "Staying silent when a member is mistreated, leaving members to face a landlord, boss or the law alone or helping others only when it costs us nothing."
}, },
{ {
"id": "stewardship",
"label": "Stewardship", "label": "Stewardship",
"meaning": "We take responsibility for tending the shared resources and relationships entrusted to us. To us, this means we make decisions with future members in mind and reinvest in the infrastructure we depend on. We acknowledge a tension with Efficiency (Long-term care vs short-term output).", "family": "share-work",
"signals": "Extracting short-term value from shared resources without replenishing them or deferring maintenance until systems fail." "meaning": "We look after the money, tools, spaces and relationships we hold for the group and for those who come after us. To us, this means we decide with future members in mind, put money and time back into what we depend on and hand things over in better shape than we found them. We acknowledge a tension with Generosity (Saving for the long run vs giving it away now).",
"signals": "Using up shared money, tools or goodwill without putting anything back, putting off upkeep until something breaks or handing a role over with nothing written down."
}, },
{ {
"id": "sustainability",
"label": "Sustainability",
"family": "learn-and-last",
"meaning": "We work at a pace that people, money and the land can keep up for years. To us, this means we size what we take on to the people we have, rotate the hardest jobs, grow only as fast as we can bring new people in well and choose materials and practices that do the least harm to the land. We acknowledge a tension with Urgency (Pacing ourselves vs meeting every need now).",
"signals": "Plans bigger than the people available to carry them, the same few members running on empty, or waste and harm to the land treated as someone else’s problem."
},
{
"id": "transparency",
"label": "Transparency", "label": "Transparency",
"meaning": "Decisions, finances, and processes are visible to the membership by default. To us, this means we publish meeting notes, budgets, and decision logs where all members can read them. We acknowledge a tension with Privacy (Open books vs personal disclosure).", "family": "share-power",
"signals": "Making major decisions in private channels and only announcing outcomes, or hiding finances behind vague summaries." "meaning": "Members can see how decisions are made, where the money goes and who holds which role. To us, this means our minutes, budgets and decision log sit in our records for every member to read, with personal details left out, and each entry says why a decision was made as well as what it was. We acknowledge a tension with Privacy (Open books vs personal details).",
"signals": "Big decisions made in private chats and announced afterward, money reports too vague to check, or members who have to ask a friend to learn who decided something."
}, },
{ {
"id": "trust",
"label": "Trust", "label": "Trust",
"meaning": "We extend good faith to one another and build relationships that can withstand disagreement. To us, this means we interpret others charitably and repair quickly when trust is strained. We acknowledge a tension with Accountability (Assuming good intent vs naming harm).", "family": "treat-each-other",
"signals": "Jumping to worst-case interpretations of ambiguous behavior or withholding repair when a breach could be addressed directly." "meaning": "We give each other good faith and build relationships strong enough to hold disagreement. To us, this means we assume people mean well until we learn otherwise, ask before we judge, and repair quickly when trust is strained. We acknowledge a tension with Accountability (Assuming good intent vs naming harm).",
"signals": "Assuming the worst about a member’s motives, talking about people instead of to them, or leaving a strained relationship unrepaired when one conversation could mend it."
}, },
{ {
"id": "voluntarism",
"label": "Voluntarism", "label": "Voluntarism",
"meaning": "Participation is freely chosen rather than coerced, and contribution is recognized on its own terms. To us, this means we don't pressure members into tasks they haven't consented to and we honor their right to step back. We acknowledge a tension with Reliability (Free participation vs guaranteed coverage).", "family": "stay-free",
"signals": "Using guilt or social pressure to extract labor from members or shaming people who decline additional responsibilities." "meaning": "Everyone takes part by choice, and every contribution counts on its own terms. To us, this means members choose what they take on, nobody is pushed or guilted into a task, and anyone can step back without explaining or losing their place. We acknowledge a tension with Reliability (Free choice vs knowing the work gets covered).",
"signals": "Guilt or pressure used to fill shifts, members shamed for saying no to more work, or people who step back treated as if they had quit."
} }
] ]
} }
File diff suppressed because it is too large. Load diff
+254 -124
View File
@@ -1,14 +1,14 @@
{ {
"_comment": "Create flow — membership / how members join (compact card stack)", "_comment": "Create flow — membership / how members join (compact card stack)",
"page": { "page": {
"compactTitle": "How do new members join\nand get connected?", "compactTitle": "Who belongs to your group\nand how do people join and leave?",
"compactDescriptionBefore": "You can select multiple methods for different needs or ", "compactDescriptionBefore": "Pick a card for each part of membership, from joining to leaving. Combine them or ",
"compactDescriptionLinkLabel": "add", "compactDescriptionLinkLabel": "add",
"compactDescriptionAfter": " your own.", "compactDescriptionAfter": " your own.",
"expandedTitle": "How should new members join and get connected?", "expandedTitle": "Who belongs to your group and how do people join and leave?",
"expandedDescriptionBefore": "You can select multiple methods for different needs or ", "expandedDescriptionBefore": "Pick a card for each part of membership, from joining to leaving. Combine them or ",
"expandedDescriptionAfter": " your own", "expandedDescriptionAfter": " your own.",
"seeAllLink": "See all membership approaches" "seeAllLink": "See all membership cards"
}, },
"confirmModal": { "confirmModal": {
"title": "Confirm selection", "title": "Confirm selection",
@@ -16,205 +16,335 @@
"nextButtonText": "Confirm" "nextButtonText": "Confirm"
}, },
"addPlatform": { "addPlatform": {
"nextButtonText": "Add Platform" "nextButtonText": "Add card"
}, },
"removePlatform": { "removePlatform": {
"nextButtonText": "Remove" "nextButtonText": "Remove"
}, },
"sectionHeadings": { "sectionHeadings": {
"eligibility": "Eligibility & Philosophy", "eligibility": "Who it covers and why",
"joiningProcess": "Joining Process", "joiningProcess": "How it works",
"expectations": "Expectations & Removal" "expectations": "Expectations and limits"
}, },
"methods": [ "methods": [
{ {
"id": "open-access", "id": "open-admission",
"label": "Open Access", "label": "Open admission",
"supportText": "Maximum inclusion. Anyone can join immediately by simply showing up.", "supportText": "Anyone who agrees to follow this Rule can join and start on the work the same day.",
"sections": { "sections": {
"eligibility": "Membership is open to any individual who lives in [Region/Context] and supports our mission. We believe that gatekeeping creates unnecessary hierarchy, so we prioritize radical inclusivity and low barriers to entry. Our only requirement is a commitment to mutual respect and adherence to the Community Code of Conduct.", "eligibility": "Anyone who agrees to follow this Rule can be a member. We keep the door wide because the people who come to us, for help or to help, can run this group with us. Aid never depends on joining, and everyone who gets aid is welcome to join. If our Rule has levels of membership, people who join here start at the first level. If our Rule has another way to admit voting members, that decides who votes and our work stays open to everyone.",
"joiningProcess": "There is no formal application or waiting period. A person becomes a member instantly by joining our [Digital Platform/Meeting], introducing themselves to the group, and explicitly agreeing to the community standards. Access to all channels and working groups is granted immediately upon sign-up.", "joiningProcess": "A person joins by coming to a shift, a meeting or one of our channels and telling a member they want to take part. That member explains what we do, shares this Rule and asks if they agree to follow it. Once they say yes they are a member and can start on the work that day. The member adds them to our roster with only a name or nickname and one way to reach them, kept where only two members the group names to keep it can see it.",
"expectations": "Members are expected to participate constructively. We view removal as a last resort. Barring immediate safety threats, no member will be banned without first going through the steps outlined in our Conflict Management Policy. We prioritize the restorative measures defined in that section over punitive expulsion. However, refusal to engage in that process may result in removal." "expectations": "Members follow this Rule and can leave at any time. Nobody is turned away for who they are. Open doors pull against safety and against keeping our culture as we grow, and they break when newcomers outnumber the members who can explain how we work. Then we slow down and welcome people in crews, or add a second door for deciding, rather than closing this one. Someone who may make others unsafe can be paused at once under our conflict process."
} }
}, },
{ {
"id": "orientation-required", "id": "worker-owner-candidacy",
"label": "Orientation Required", "label": "Worker-owner candidacy",
"supportText": "Newcomers must attend a training or orientation session.", "supportText": "Every worker has a dated path to ownership, from candidate to a member vote to a member share.",
"sections": { "sections": {
"eligibility": "We welcome all new members who are willing to align with our specific values and operational methods. We believe that shared knowledge is power. Requiring education before decision-making prevents confusion and ensures that all members are empowered to participate fully in our governance.", "eligibility": "Every worker who stays with the co-op has a dated path to becoming a worker-owner. Ownership means one member one vote, a share of any surplus by hours worked and the duties that come with both. Candidacy gives the candidate and the members time to see if it fits. We judge every candidate against the same written criteria and never by whether they are like us.",
"joiningProcess": "Prospective members enter as \"Observers.\" To become a voting \"Member,\" an individual must attend a mandatory [2-hour] \"New Member Orientation\" session. This session covers our history, decision-making tools, and safety protocols. Upon completion, they sign the membership agreement and are granted full voting rights.", "joiningProcess": "Founding members are worker-owners once they adopt this Rule. Every later hire becomes a candidate, with the books, the bylaws and this Rule open to them from the first week. Candidacy lasts nine months. A worker-owner meets the candidate every month to answer questions and give feedback. At six months the candidate and two worker-owners review the work in writing against our written criteria. At nine months the worker-owners vote, and the candidate joins with more than half of those who vote, if our quorum votes. The new worker-owner buys one member share, the same price for everyone, paid over a year or longer if asked, and by payroll only with written consent. Candidates are not yet members, and only worker-owners vote on the co-op’s business.",
"expectations": "Members must remain active to retain voting status. Inactivity for [6 months] reverts a member to \"Alumni\" status. Regarding behavior, we follow the graduated sanctions described in our Conflict Management section. Membership revocation is the final step in that process and is reserved for cases where mediation and accountability attempts have failed." "expectations": "Worker-owners attend member meetings and vote. A candidate who is not approved hears why in writing and can stand again once after three months. After a second no, our bylaws say whether their job continues. Candidacy pulls against equality, so it never runs past twelve months, and a vote not held by then is taken at the next member meeting. The co-op is an employer, so candidates have the same written protections against harassment and discrimination as members. Removing a worker-owner follows our conflict process and our bylaws."
} }
}, },
{ {
"id": "invitation-only", "id": "join-by-contributing",
"label": "Invitation Only", "label": "Join by contributing",
"supportText": "New members can only join if they are 'vouched for' by existing members.", "supportText": "Anyone can start on the work, and after two shifts or two accepted contributions they become a member by asking.",
"sections": { "sections": {
"eligibility": "To ensure the physical and digital safety of our community, we operate on a strict \"web of trust\" model. We prioritize the security of our vulnerable members over rapid expansion. Membership is restricted to those who are known and trusted by the existing network.", "eligibility": "We use this when doing the work together is the best way to know each other, as in a crew that runs shifts or an open source project that reviews contributions. Anyone can start without asking. Every kind of work counts, including shifts, admin, care for other members, writing, design, code and working group meetings. If our Rule also keeps our work open to everyone, this decides who becomes a member.",
"joiningProcess": "An applicant must be \"vouched for\" by at least [2] existing members in good standing. The vouchers must submit a statement confirming they know the applicant in real life and attest to their integrity. The Membership Working Group reviews these requests. The wider group then has a [72-hour] window to raise safety concerns before approval.", "joiningProcess": "A newcomer picks up an open task, a shift or a first contribution marked for newcomers, with a member nearby to answer questions. After two shifts, or two contributions a member has reviewed and accepted, within three months, the newcomer can ask any member to add them as a member. That member checks the work happened, adds them to our roster and tells the members where every member will see it. A member with a safety concern raises it within three days, and the person waits while two members look into it. The person hears the concern and can answer it. If it still stands, it is a decision of the whole group.",
"expectations": "Members are responsible for the integrity of those they vouch for. If trust is broken, we do not act unilaterally. Instead we engage the specific accountability mechanisms detailed in our Conflict Management Policy. Removal is a consensus decision made only after those specific mediation steps have been exhausted or rejected by the offending party." "expectations": "Members keep taking part in some way. A member who has taken no part for six months and does not answer a friendly message within a month moves to former members and can come back by asking. Contributing earns membership, and more say comes only through the levels our Rule sets, on the same terms for everyone. Joining by work favors people with free time, so we count every kind of work and keep tasks small enough for people with jobs, children or disabilities. Removal follows our conflict process."
} }
}, },
{ {
"id": "contribution-based", "id": "membership-by-token",
"label": "Contribution Based", "label": "Membership by token",
"supportText": "Membership is reserved for people contributing their labor.", "supportText": "Anyone who holds our token is a member, with written limits so that money alone cannot buy control of the group.",
"sections": { "sections": {
"eligibility": "We practice \"Do-ocracy\" which means the people doing the work make the decisions. Membership is not a static title but a dynamic status earned through contribution. This ensures that our governance reflects the reality of labor and prevents \"armchair\" participation from stalling our progress.", "eligibility": "Our members are the people who hold our token, a record on a public blockchain that shows who belongs. Anyone who holds at least one token is a member. Tokens can be bought and sold, so money can buy say in the group, and we accept that only within the written limits below. Work for the group earns tokens without paying, and our Rule’s methods for deciding set how much say each token gives. Aid never depends on a token.",
"joiningProcess": "Participants enter as \"Volunteers.\" A Volunteer becomes a voting \"Member\" only after documenting [10 hours] of labor or completing [3 shifts] within a [30-day] period. \"Labor\" is defined broadly to include administrative tasks, emotional support, and physical work to ensure accessibility.", "joiningProcess": "A person becomes a member when they hold a token in a wallet, an account on the blockchain. They can buy one where tokens are offered or earn tokens for work the members approve. We ask a lawyer before offering tokens for sale, since selling tokens can bring them under investment law. A member we name sends each new holder a short welcome and this Rule. Holdings are visible to anyone, so members who need privacy use a wallet that is not tied to their name. If our work is open to everyone, anyone can take part in our work and discussions, and only holders vote. Where our Rule lets people join by contributing, that work earns a newcomer their first token.",
"expectations": "To maintain voting rights, a member must log at least [5 hours] of activity per month. However, high activity does not grant immunity from community standards. Behavioral violations are handled separate from labor tracking by utilizing the workflows defined in the Conflict Management section. A member may be removed for hampering the group's safety regardless of their work output." "expectations": "No holder can cast more than one tenth of the votes on any decision, and wallets we can tie to one person count as one holder. Every member can see every holding, vote and payment from shared funds and can raise a concern through our conflict process. Decisions can be undone the way they were made. Members can leave at any time by giving up their tokens. A holder removed under our conflict process loses roles, spaces and votes where our token terms allow and can sell us their tokens."
} }
}, },
{ {
"id": "mentorship", "id": "volunteers-and-core-members",
"label": "Mentorship", "label": "Volunteers and core members",
"supportText": "New members are paired with 'Mentors' to guide them through a probationary period.", "supportText": "Volunteers do the work, and core members who take part every month also hold keys and make the big decisions.",
"sections": { "sections": {
"eligibility": "We believe that the best way to integrate new members is through personal guidance. Eligibility is open to those who are willing to learn and build relationships. We value inter-generational knowledge transfer and community cohesion over rapid growth.", "eligibility": "We have two kinds of members. Volunteers are everyone who takes part, and they can do any open task, join working groups and speak at any meeting. Core members are volunteers who have taken on more. They hold keys, logins and private channels and make decisions of the whole group. Core is a job, not a status, and every core member has one vote. In this Rule active members are core members who are not on leave.",
"joiningProcess": "Upon applying, a prospective member is assigned a Mentor. They undergo a [3-month] mentorship period involving regular check-ins and shared tasks. After the mentor validates the newcomer's readiness, full membership is granted.", "joiningProcess": "A volunteer who has taken part for one season of three months can ask to join the core. The request is posted where every core member will see it. The volunteer joins if no core member raises a reasoned concern within fifteen days, and a concern that stands goes to a vote of the core by our supermajority share. If our Rule sets another way to approve members, that governs this step instead. Core members do at least one shift or task for the group each month, and admin, care for members and working group meetings count. At the start of each month two members who keep our roster check who did, looking only at the month just ended.",
"expectations": "Mentees are expected to be proactive in their learning. Mentors must provide support, not just judgment. If the relationship fails, a new mentor may be assigned. Persistent lack of engagement or violation of values leads to offboarding." "expectations": "The roster keepers message any core member who missed the month, asking how they are and offering leave. One who does not take leave within two weeks, backdated if they wish, returns to volunteer for now and hands back keys, so quiet months never leave us short of a quorum. They rejoin the core after one month at the floor by asking, with no new approval. Tiers can harden into a ladder, so the core is open to every volunteer on the same terms. Removal follows our conflict process."
} }
}, },
{ {
"id": "peer-sponsorship", "id": "contributors-and-maintainers",
"label": "Peer Sponsorship", "label": "Contributors and maintainers",
"supportText": "New member must be vouched for by an existing member.", "supportText": "Contributors become reviewers and then maintainers through steady work and a nomination, on the same written terms for everyone.",
"sections": { "sections": {
"eligibility": "Trust is the foundation of our group. We rely on the judgment of our existing members to bring in trustworthy individuals. Eligibility is restricted to those who have a personal connection with at least one current member.", "eligibility": "Our project has three steps. Contributors are anyone who has had work accepted. Reviewers can approve work in the areas they know. Maintainers accept work into the main project, publish releases, hold the project’s accounts and decide changes to this Rule, so in this Rule active members are maintainers not on leave. Each step is a job open to everyone on the same written terms, and every maintainer has one vote. If our Rule also has core members, maintainers are the core.",
"joiningProcess": "An existing member must formally sponsor the applicant, submitting a statement of support. The sponsorship is reviewed by the membership committee, followed by a [notification period] for the wider group to raise objections before confirmation.", "joiningProcess": "A contributor who has had at least five pieces of work accepted over three months can be nominated as a reviewer by any maintainer or can nominate themselves. A reviewer who has reviewed work steadily for six months can be nominated as a maintainer the same way. A nomination is posted where every maintainer will see it and passes if no maintainer raises a reasoned concern within fifteen days. A concern that stands goes to a vote of maintainers by our supermajority share. Our records say what each step can do and who holds each step.",
"expectations": "Sponsors are partially accountable for the behavior of those they bring in during the onboarding phase. If a new member violates norms, the sponsor is expected to participate in the conflict resolution process." "expectations": "Maintainers review work within two weeks or say they cannot. A reviewer or maintainer who has not taken part for six months and does not answer a message within a month moves to former maintainers, keeps their credit, hands back access and can return by asking. The ladder pulls against equality and favors people paid to work on the project, so every kind of work counts, including docs, design, support and sorting reports, and we look at who climbs each year. Removal follows our conflict process."
} }
}, },
{ {
"id": "consensus-or-vote-based-approval", "id": "companions",
"label": "Consensus or Vote-Based Approval", "label": "Companions",
"supportText": "Group votes on whether to admit a new member.", "supportText": "Each new member is paired for their first season with a companion who explains how we work and why.",
"sections": { "sections": {
"eligibility": "We function as a democratic collective where every voice matters. We believe that adding a new member changes the group dynamic, so the group must consent to the addition. Eligibility implies a willingness to abide by collective decisions.", "eligibility": "Every new member gets a companion for their first season of three months. A companion is a member who joined at least one season ago. They are a peer, never a boss or a judge. Their job is the why behind how we work, so our culture passes on along with our methods. Whoever started the group is the first companion.",
"joiningProcess": "Candidates introduce themselves at a meeting or submit a profile. After a Q&A period, the existing membership votes. Approval requires [Consensus / Supermajority / Simple Majority].", "joiningProcess": "When someone joins, a member pairs them with a companion who has time that season. A companion has at most two newcomers at once. They meet in the first week and do at least one shift or task together each month. They talk about how we decide, who holds which keys and why our rules say what they say. In the second month the companion asks what the newcomer sees that we have stopped seeing and brings it to the next meeting, without names if the newcomer prefers. Either one can ask for a new pairing at any time without giving a reason.",
"expectations": "Members must respect the democratic process. Removal follows the same voting threshold as admission, ensuring that exclusion is a collective decision, not an individual one." "expectations": "Companions answer questions, explain and introduce. They never decide whether a newcomer stays, and a newcomer never needs a companion’s approval to take part or to speak. Being a companion counts as work for the group. Pairing pulls against growth because some newcomers will wait for a companion, though never to join or take part. When no companion is free we say so and give a date. Concerns about a pairing go to our conflict process."
} }
}, },
{ {
"id": "trial-period-provisional-membership", "id": "joining-in-crews",
"label": "Trial Period / Provisional Membership", "label": "Joining in crews",
"supportText": "New members have a probationary period before full rights.", "supportText": "New members learn together in crews that start on set dates, and each crew is only as big as we can teach.",
"sections": { "sections": {
"eligibility": "Commitment is proven through action, not just words. We believe in a 'try before you commit' approach for both the group and the individual. Anyone can start the trial, but full membership is earned.", "eligibility": "We bring new members in by crews so that our culture keeps pace with our growth. Drift wins when new people outnumber the members who can say why a rule is there. A crew is a few newcomers who start together, learn together and lean on each other. Anyone our Rule admits can join the next crew, and newcomers who arrive between crews can do the work right away.",
"joiningProcess": "Newcomers start as 'Provisional Members' with limited rights (e.g., no voting on strategy). After [3 months] or [X contributions], a review is conducted. Successful completion upgrades them to full 'Member' status.", "joiningProcess": "We name the start date of the next crew at least a month ahead and start a crew every three months, or sooner when five or more newcomers are waiting. A crew takes only as many newcomers as we can pair with experienced members, two newcomers to each. The crew meets weekly for its first month to learn this Rule and the work, and hears what we tried before and what it cost. At every shift one member welcomes anyone who drops in, gives them one task and tells them the date the next crew starts.",
"expectations": "Provisional members must demonstrate activity and cultural alignment. If the trial period expires without meeting criteria, the provisional status is revoked or extended once." "expectations": "Newcomers waiting for a crew can still do the work and are welcome at every meeting. Crews pull against reach because some eager people wait. When more people want in than we can teach, we start a second crew or help newcomers start a group of their own rather than cutting what each crew learns. After each crew’s first season we ask how it went and change the plan for the next crew. Concerns about a crew go to our conflict process."
} }
}, },
{ {
"id": "referral-system-with-screening", "id": "orientation",
"label": "Referral System with Screening", "label": "Orientation",
"supportText": "Members refer new people, with structured screening.", "supportText": "New members are invited to a short orientation that reads this Rule aloud and gives the reason behind each rule.",
"sections": { "sections": {
"eligibility": "We combine the trust of referrals with the fairness of objective screening. This ensures we grow through trusted networks while maintaining quality standards and reducing nepotism.", "eligibility": "Every new member is invited to an orientation in their first month. It is a welcome, not a test, and nobody waits for it to start on the work or to speak at a meeting. Orientation passes on what is written down. It reads this Rule aloud, explains how we decide, who holds which keys and how to raise a concern, and tells how our rules came to be. Online projects can run it as a written guide with a monthly call.",
"joiningProcess": "Candidates must be referred by a member. Once referred, they complete a standard screening interview or questionnaire with the Membership Team to ensure they meet the baseline requirements independently of their referrer.", "joiningProcess": "Two members run orientation once a month, or whenever three newcomers are waiting, for about an hour and a half. They read this Rule aloud and pause after each part for questions. For each rule they give the reason, what we tried before and what it cost, and they leave the gossip out. They show where our records live and how to reach the members who hold each role. Newcomers who cannot come get the same walkthrough one on one or on a call. When a newcomer asks a question this Rule cannot answer, the members running orientation bring it to the next meeting.",
"expectations": "Referred members are treated as individuals, not extensions of their referrer. They are subject to the standard Code of Conduct and removal processes." "expectations": "Doing the work and speaking at meetings never depend on orientation. Running orientation counts as work for the group and rotates among members each season. Orientation pulls against time, and it goes stale when nobody updates it, so the members running it update the notes each season. A rule with no reason given is the first thing a capable newcomer will want to fix, so we give the reason with the rule. Concerns about orientation go to our conflict process."
} }
}, },
{ {
"id": "membership-agreement-or-pledge", "id": "rest-and-workload-limits",
"label": "Membership Agreement or Pledge", "label": "Rest and workload limits",
"supportText": "New members must agree to specific values, responsibilities, or rules.", "supportText": "We cap what any one member carries, set hours off and make the work smaller when the crew cannot cover it.",
"sections": { "sections": {
"eligibility": "Our unity comes from shared agreement. Eligibility is based on the willingness to explicitly commit to our [Manifesto/Constitution/Code of Conduct]. We prioritize alignment of values over specific skills.", "eligibility": "Burnout is the group’s to fix, not the person’s. It comes from a design nobody chose, where work flows to whoever can least say no. These limits apply to every member, including whoever started the group, and we set them together while everyone is rested. People burn out fastest when they cannot afford to stop, so a group that pays its members pays a wage they can live on and keeps a reserve for leave.",
"joiningProcess": "Prospective members review the Membership Agreement. They must sign (digitally or physically) or publicly recite the pledge. Membership is effective immediately upon this affirmation.", "joiningProcess": "No member holds more than two roles at once, and every role has a second person learning it. A group too small for this lists who holds each role and who takes it next. We keep hours when the group’s phones are off and a closed week each season, and in off hours only members on call need to answer urgent messages. Every six months we count the hours members can give and the hours our work takes. When the work takes more, we grow only as fast as we can teach, or we make the work smaller and say who that leaves out. Members are paid back for what they spend on the work. Spending over $50 needs one other member’s written agreement first, and over $500 a decision of the whole group.",
"expectations": "The Agreement acts as the contract for behavior. Violation of the specific terms agreed to is grounds for immediate review and potential removal." "expectations": "Any member can say a load is too much without giving a reason, and nobody loses their place, shifts or pay for saying it. A member who takes on more than these limits allow is asked to hand some of it over, as care and not as a penalty. Limits pull against speed and reach, and some work will wait. A smaller mission is not a defeat. We change these limits by a decision of the whole group, never in a crisis. Concerns go to our conflict process."
} }
}, },
{ {
"id": "weighted-or-tiered-membership", "id": "leave-and-return",
"label": "Weighted or Tiered Membership", "label": "Leave and return",
"supportText": "Different levels of membership with increasing rights over time.", "supportText": "Members can step back for up to six months without losing their place, and come back without starting over.",
"sections": { "sections": {
"eligibility": "We recognize that commitment levels vary. By offering tiers (e.g., Supporter, Contributor, Core), we allow people to engage at their capacity. Higher tiers require deeper accountability and grant more governance power.", "eligibility": "Every member can take leave for any reason and does not have to give one. People get sick, burn out, care for family, move or need rest. A clear leave lets them rest without guilt and keeps our counts honest, because members on leave are not active members and do not count toward quorum or any threshold. In a worker co-op, pay during leave follows our bylaws and the law.",
"joiningProcess": "Everyone joins at the 'Basic' tier. Moving to higher tiers requires meeting specific criteria (e.g., hours worked, dues paid, tenure) and applying for an upgrade, which is verified by the Governance Circle.", "joiningProcess": "A member tells the roster keepers they are taking leave and for how long, up to six months. They hand keys, logins and roles to another member before they go, and keep announcements if they wish. Leave can be extended once by telling the roster keepers. When burnout is hurting a member or the work, two members not involved can offer rest leave. Pausing their shifts and roles for up to a month needs the member’s agreement or a decision of the whole group. The member keeps their vote and can ask two other members to review it. Disagreement is never a reason, and paid work follows employment law. To return, they say so and get a catch-up from a member within two weeks.",
"expectations": "Members must maintain the requirements of their tier. Falling below requirements results in a downgrade to a lower tier rather than removal from the group." "expectations": "A member on leave keeps their place, their level and their history, and returns with the rights they had. Nobody is pressed to explain a leave or to come back early. Leave pulls against coverage, so a member who holds a role hands it over before leaving. A member away for six months without taking leave gets a friendly message asking how they are. If they do not reply within a month they move to former members and can return by asking. This is not removal."
} }
}, },
{ {
"id": "hybrid-approval-process", "id": "leaving-the-group",
"label": "Hybrid Approval Process", "label": "Leaving the group",
"supportText": "Combines multiple approval steps like voting, screening, and referrals.", "supportText": "Members can leave at any time, hand back what they hold, say what they saw and come back later.",
"sections": { "sections": {
"eligibility": "We require a rigorous vetting process to ensure high alignment. By combining methods, we filter for competence, culture, and trust. Eligibility is for those dedicated enough to navigate a multi-step process.", "eligibility": "Any member can leave at any time for any reason, and nobody is pressed to stay or to explain. A group that is easy to leave should still hear why people go, and a group that is hard to leave, like a worker co-op, should make leaving fair. This covers leaving by choice, not removal or members who drift away.",
"joiningProcess": "Step 1: Application/Referral. Step 2: Screening Interview. Step 3: Trial Task or Period. Step 4: Final Ratification Vote by the core team.", "joiningProcess": "A member who leaves tells any member, who passes it to the members who keep our roster. The departing member hands roles to another member and returns keys, equipment and shared money within two weeks, and their access to private channels and accounts is closed. A member they choose offers a short talk about what they saw, what worked and what made them go, and brings what they agree to share to the next meeting without their name. A departing worker-owner gives the notice our bylaws set and is repaid their share as our bylaws say.",
"expectations": "Due to the high barrier to entry, members are expected to be highly active leaders. Removal is a formal process involving the same bodies that approved the member." "expectations": "Former members are welcome back and return the way our Rule says people join, with no penalty for having left. Their details leave our roster after two years unless they ask us to keep them. We do not gossip about people who leave. Easy leaving pulls against keeping what people know, so we ask for notes on any role first. If several people leave in a short time, the members look at why at the next meeting. Concerns go to our conflict process."
} }
}, },
{ {
"id": "skill-based-contribution", "id": "handing-over-roles",
"label": "Skill-Based Contribution", "label": "Handing over roles",
"supportText": "Joining by submitting work, such as a pull request.", "supportText": "A member who steps down from a role gives notice, trains the next person and hands over keys, logins and notes.",
"sections": { "sections": {
"eligibility": "Code (or work) speaks louder than words. We operate as a meritocracy where the ability to contribute is the only barrier to entry. Anyone capable of doing the work is eligible.", "eligibility": "Any member can step down from any role at any time, and leaving a role is ordinary and never a betrayal. Knowledge that lives in one head leaves with that head, so every role has written notes and every key and login is held by at least two members. The group owns each role and its accounts, never the person who holds them. This covers stepping down, the end of a term and handing over before leave.",
"joiningProcess": "A contributor picks up a 'Good First Issue' or task. Once their contribution (e.g., Pull Request) is reviewed and merged/accepted by a Maintainer, they are invited to the organization roster.", "joiningProcess": "A member stepping down tells the members two weeks ahead when they can, or right away when they cannot. They update the role’s notes, a page on how the work is done, and give them to the next holder or the second person already learning it. They do the role together at least once. Keys, logins, passwords and bank access move to the new holder, and two members check that the old holder’s access is closed within a week. When no one takes the role, the members decide at the next meeting whether to pause that work or share it out.",
"expectations": "Continued membership depends on active contribution. Members who go dormant for [6 months] may be moved to 'Alumni'. Malicious contributions result in an immediate ban." "expectations": "Members who step down are thanked in the open, and we mark each handover so that changing hands is something we celebrate. The old holder answers questions for a month if they can and owes nothing more. Handing over pulls against speed because teaching is slower than doing. A role that nobody can hand over has become one person’s job, and we fix that at the next meeting. Concerns about a handover go to our conflict process."
} }
}, },
{ {
"id": "pay-to-join", "id": "ending-a-membership",
"label": "Pay-to-Join", "label": "Ending a membership",
"supportText": "Requires financial contribution or dues.", "supportText": "We end a membership against someone’s will only as the last step of our conflict process, by our supermajority share.",
"sections": { "sections": {
"eligibility": "We value financial sustainability and commitment. Paying dues demonstrates a tangible investment in the group's longevity. Eligibility is open to anyone willing and able to support the organization financially.", "eligibility": "This covers ending a membership the member does not want to end. We remove a member only when they have done serious harm or repeatedly broken our commitments, our conflict process has run, and repair has failed or would not keep people safe. Disagreeing, raising concerns, being inactive and who someone is are never grounds. Removal pulls against our open door, so it is the last step and never the first. Our conflict process can pause someone’s access at once, and a pause is not a finding.",
"joiningProcess": "The prospective member selects a membership tier and sets up a payment method. Upon successful transaction, access to member-only spaces is automatically granted.", "joiningProcess": "A proposal to remove a member comes only from our conflict process, with a short summary of what happened and what is disputed, without private details. The member hears the reasons and can answer in writing or in person with a friend present. The person harmed never has to attend or speak. The active members vote within two weeks, by secret ballot if anyone asks, and removal passes with our supermajority share of the active members who vote, if our quorum votes. The member in question neither votes nor counts. If our Rule gives this decision to a named role, that role decides after the same steps. We record only that it was decided, its date and the method.",
"expectations": "Membership is contingent on active payment. Failure to pay dues results in a grace period followed by a lapse in membership status/access." "expectations": "A removed member hands back keys and equipment, their access closes within a day and any member share is repaid as our bylaws say. Removal never cuts anyone off from aid, and if distance is needed we find another way to get them aid. They can ask the members once to reconsider with new facts, and can ask to return after a year. If the vote fails, the case goes back to our conflict process. A group with staff follows employment law and asks a lawyer first. Ending a membership never ends anyone’s right to live in our housing without the court process the law requires. Nobody is locked out or has utilities cut."
} }
}, },
{ {
"id": "application-review", "id": "equal-membership",
"label": "Application & Review", "label": "Equal membership",
"supportText": "Application reviewed by an onboarding team.", "supportText": "Everyone who belongs is a full member with the same rights, the same duties and one vote.",
"sections": { "sections": {
"eligibility": "We seek specific qualities to balance our team. A formal application allows us to assess skills, experience, and diversity needs objectively. Eligibility is open, but selection is competitive.", "eligibility": "We have one kind of member. Every member can do the work, has the same say in how it is run and in this Rule, and has one vote, whatever they put in and however long they have been here. People who help without joining are supporters, welcome in our work and at our events, and they do not vote.",
"joiningProcess": "Candidates submit a detailed written application. The Membership Committee scores applications against a rubric. Selected candidates are notified and onboarded.", "joiningProcess": "A person becomes a full member the way our Rule says people join, after any trial period or candidacy our Rule sets, and from then on has every right in this Rule. Roles, keys and logins are jobs the members hand out for a term. They are not ranks, and holding one never adds to a member’s vote. Whoever started the group, longtime members and members who put in more money or hours have one vote like everyone else. A supporter who wants a say becomes a member the same way anyone else does.",
"expectations": "Members obtained through this process are expected to fulfill the roles they applied for. Significant deviation from the stated intent in the application may trigger a review." "expectations": "Every member takes on a share of the work they can manage and joins decisions of the whole group when they can. Equal say pulls against speed as we grow, and it breaks when a few members do most of the work while everyone decides. When that happens we consider a core of members who take on more, or limits on what any one member carries, rather than giving anyone a bigger vote. Removal follows our conflict process."
} }
}, },
{ {
"id": "identity-verification", "id": "starting-with-one-person",
"label": "Identity Verification", "label": "Starting with one person",
"supportText": "Requires verification of identity, credentials, or background.", "supportText": "One person starts the group, writes the first draft of this Rule and hands their sole say to the first members.",
"sections": { "sections": {
"eligibility": "Security and accountability are paramount. We require Sybil resistance (one person, one account). Eligibility is restricted to real, unique humans who can prove their identity.", "eligibility": "Our group starts with one person who writes this Rule as a draft for the people who join. Their first job is to make room for others. Anyone they invite, and anyone who agrees to this draft and wants to do the work, can be a founding member. The person who started the group gets no title, extra vote or veto once others join. Later members join the way our Rule says.",
"joiningProcess": "User submits proof of identity (ID, social media link, or cryptographic proof like Proof of Humanity). A verifier or automated system confirms the identity. Access is granted upon verification.", "joiningProcess": "The person who started the group invites the first members, shares this draft and asks each to agree to follow it while it is a draft. They decide alone only as long as our Rule’s methods for deciding allow, and if those name no end, until a second member joins. They record each decision, why they made it and how it can be undone. When the group has three members, or after three months with two, the founding members read the draft together, change what they want and adopt it by a decision of the whole group.",
"expectations": "Members must maintain a single identity. Validated bad actors are permanently blacklisted. Privacy is respected, but accountability is enforced." "expectations": "Once the Rule is adopted, the person who started the group is one member among equals. Every key, account and login they set up is shared with at least one other member within a month. If they want to keep a role, such as holding the bank account, they ask the members, and the role gets a term and a recall like any other. Keeping control of what you started is tempting, so the founding members review this arrangement after their first season. Removal follows our conflict process."
} }
}, },
{ {
"id": "collective-interviews", "id": "owners-and-staff",
"label": "Collective Interviews", "label": "Owners and staff",
"supportText": "Group collectively interviews new members for cultural fit.", "supportText": "Owners have the final say on the business, and staff have written rights to see decisions, raise concerns and appeal.",
"sections": { "sections": {
"eligibility": "Culture is co-created. We believe the existing community should interact with potential members to ensure resonance. Eligibility requires the social skills to engage with the group.", "eligibility": "Our business has owners and staff. The owners, named in our records, have the final say on what this Rule leaves to them, such as hiring, pay, the budget and ending someone’s work. Staff are everyone else who works here, and they are members with every right this part of our Rule does not keep for owners. Because this gives owners power, it limits that power in writing. In a worker co-op, worker-owners are the owners and candidates for ownership are staff.",
"joiningProcess": "Applicants attend a 'Ask Me Anything' or group interview session. After the session, the group debriefs and decides on admission based on the interaction.", "joiningProcess": "Staff join the way our Rule says and become owners only as our ownership agreement or bylaws say. Owners write down each decision that affects staff as a group, with the reason, where every member will see it. Decisions about one person stay between the owners and that person. Staff can see this Rule, the decisions that affect their work and the pay scale, and can talk about pay and organize, including in a union. Any staff member can ask the owners to explain or change a decision, and the owners answer in writing within two weeks. If they do not answer in time, the question goes to the next meeting of all members.",
"expectations": "Members are expected to maintain the cultural vibe established in the interview. 'Vibe checks' may continue, and dissonance can lead to conflict resolution." "expectations": "A staff member who disagrees with an answer can appeal through our conflict process, and a decision that breaks this Rule is undone when the appeal finds it does. Nobody loses their place, shifts or pay for raising a concern. We are an employer, so we keep written harassment and discrimination procedures and follow employment law. Before ending anyone’s work, owners give the reasons in writing and hear the person’s side. Owner power pulls against equality, so owners and staff review this arrangement together every year."
} }
}, },
{ {
"id": "skill-based-evaluation", "id": "paid-staff",
"label": "Skill-Based Evaluation", "label": "Paid staff",
"supportText": "Requires passing a skills assessment or portfolio review.", "supportText": "Staff we pay have a written role, report to a named member or working group and never decide their own pay.",
"sections": { "sections": {
"eligibility": "We are a guild of professionals. Quality assurance is our priority. Eligibility is limited to those who can demonstrate a specific level of proficiency in our domain.", "eligibility": "Some member-run groups hire a coordinator or a few workers so the work does not rest on volunteers alone. A paid role is a job, not a rank. Staff carry out what the members decide and do not become the boss nobody chose. They keep every right the law gives employees, including talking about pay and organizing. Staff who are also members keep their vote, except on their own pay, role or review.",
"joiningProcess": "Candidate submits a portfolio or takes a standardized test. Experts within the group review the submission. A passing score grants membership.", "joiningProcess": "We create a paid role as a decision of the whole group, with a written description of the work, the hours, the pay and who the staff member reports to, which is one named member or a working group. Hiring follows written criteria and a fair process open to anyone who could do the job. Staff take direction only through the person or working group they report to. Pay follows a scale every member can read.",
"expectations": "Members must maintain professional standards. Plagiarism or gross negligence in work leads to revocation of credentials/membership." "expectations": "Reviews happen once a year in the smallest circle that can do them well, and the members hear only the outcome. Concerns about staff go to the person they report to and then to our conflict process, and a group with staff keeps written harassment and discrimination procedures. Ending a paid role follows employment law and our written discipline steps. Paid staff pulls against shared work, so each year we check that members still do the work that matters."
} }
}, },
{ {
"id": "lottery-sortition", "id": "one-person-one-account",
"label": "Lottery / Sortition", "label": "One person one account",
"supportText": "Members are selected randomly from a pool of eligible applicants.", "supportText": "Each member has one account and one vote, and we check this without collecting documents or legal names.",
"sections": { "sections": {
"eligibility": "We believe in fairness and equality. By removing human bias from selection, we ensure a representative sample of the community and prevent power consolidation. Eligibility is open to all who meet basic criteria.", "eligibility": "We use this when we meet and vote mostly online and an account could be faked or copied, so that one person might vote many times. Any person can hold one membership account. We never ask for ID, legal names, addresses, photos of documents or biometric data, because collecting them puts undocumented members and the people we work with at risk. Aid and access to our work never depend on an account.",
"joiningProcess": "Interested individuals submit their name to the pool. At regular intervals, a random selection (using a cryptographic or transparent physical randomizer) occurs. Selected individuals are invited to join.", "joiningProcess": "A new account becomes a member account once two members confirm they have met its holder live, on a video call or in person, and the holder says this is their only account. In a large network the confirming members can come from the holder’s local group. They record only that they confirmed the account and the date. Accounts that are not yet confirmed can take part in discussion but do not vote on decisions of the whole group.",
"expectations": "Selected members are expected to serve their term faithfully. Removal is rare and reserved for gross misconduct, as the legitimacy of the system relies on the randomness of the selection." "expectations": "Members keep one account and tell the members who keep accounts if they lose access, so the old account can be closed and a new one confirmed. A member who knowingly holds two voting accounts loses the extra one, and repeated cases go to our conflict process. Live checks favor people who can meet on video in a shared language, so we offer other times, languages and in-person checks. Removal follows our conflict process."
}
},
{
"id": "trial-period",
"label": "Trial period",
"supportText": "New members do the work and help run it from day one, and gain a say in this Rule after one season.",
"sections": {
"eligibility": "Every new member has a first season of three months that opens three doors in order. The work opens on the first day, and so does a say in how the work is run. A say in changes to this Rule, in who joins and in spending over $500 opens when the season ends, on the same terms everyone has. A group that makes people wait longer to speak gets members who have learned to be quiet. If our Rule has core members, the core opens after this season.",
"joiningProcess": "During the first season the new member works beside other members and has a companion if our Rule names one. They do not count toward thresholds for the decisions still closed to them. Near the end of the season the new member and two members talk about how it has gone, what the new member sees that we have stopped seeing and what they still want to learn. The season then ends and the member has every right members have, unless a member raises a reasoned concern. The new member hears the concern and can answer it, and the season can be extended once by three months.",
"expectations": "New members are never on trial for who they are. A concern must be about our commitments or safety. If one still stands at the end of an extension, the active members decide by our supermajority share whether the person gets a say in this Rule. Either way they keep doing the work, and if turned down they can ask again after three months. Waiting pulls against equality, so the season is extended once at most. Removal follows our conflict process."
}
},
{
"id": "members-approve-new-members",
"label": "Members approve new members",
"supportText": "A person who wants to join meets members and joins when no member raises a reasoned concern within fifteen days.",
"sections": {
"eligibility": "We use this when membership carries real power, such as a vote on this Rule, keys or access to shared money, and every member should have a say in who joins. Anyone who agrees to this Rule can ask. We judge everyone against the same written commitments and never by whether they are like us. If our Rule also keeps our work open to everyone, this decides who becomes a member.",
"joiningProcess": "The person tells any member they want to join and meets members at a meeting or on a call any member can join. Members ask only the questions we agreed in advance about our commitments and the work, the same for everyone, and the person asks theirs. A member then posts the request where every member will see it and raises it at the next meeting. If no member raises a concern within fifteen days, the person is a member. A concern must give a reason tied to our commitments or to safety. The person hears it without private details and can answer it. If it still stands, the active members vote and the person joins with our supermajority share.",
"expectations": "A person who is not approved hears why in plain words and can ask again after three months. Nobody can block a new member over dislike, a disagreement outside our commitments or who the person is. Approving members pulls against speed and can let a few longtime members guard the door, so once a year we review who asked to join and who was turned away. New members join on the same terms as everyone else. Removal follows our conflict process."
}
},
{
"id": "application-and-review",
"label": "Application and review",
"supportText": "People apply, and a small working group reviews every application against the same written criteria and recommends a decision.",
"sections": {
"eligibility": "We use this when places are limited, roles need particular skills or more people ask to join than members can meet, as in housing co-ops, fellowships and hiring. Anyone can apply. We say what we look for before anyone applies, judge everyone against the same criteria and ask only for what the decision needs. A referral from a member never replaces the review. In a worker co-op this governs hiring, and new hires then follow our path to ownership.",
"joiningProcess": "The members name a working group of two to five members for a one-year term. It writes a short application and the criteria, and every application gets a reply within a month. Two of its members read each application and talk with the applicant, asking everyone the same questions. They score it against the criteria and recommend a decision. If our Rule has the members approve new members, the recommendation goes to them. Otherwise it takes effect when no member raises a concern within fifteen days, and a concern that stands goes to a decision of the whole group. An applicant who is turned down hears why and can apply again after six months.",
"expectations": "The working group keeps applications private, shares only its recommendation and reasons with the members and deletes the applications of people who did not join after a year. It cannot add criteria on its own. The members change the criteria by a decision of the whole group. Written applications favor people who write well, so we accept spoken applications and help with forms. If replies fall behind a month, the members name new people to the working group."
}
},
{
"id": "vouching",
"label": "Vouching",
"supportText": "A new member joins when two members who know them in person vouch for them and nobody raises a safety concern.",
"sections": {
"eligibility": "We use vouching when our work puts members or the people we work with at risk, such as care at protests, immigration defense or organizing a workplace. Trust is built in person, so a member vouches only for someone they know off the internet. Vouching is a door to membership and never a judgment of anyone’s worth. If our work is open to everyone, it stays open and vouching decides who becomes a member. If the members also approve new members, vouching comes first and their fifteen days follow.",
"joiningProcess": "Two members who know the person in person each tell the members who keep our roster that they vouch for them. In a group of five or fewer one voucher is enough. Members hear of it by first name in a channel only members can read and have three days to raise a safety concern with the vouchers. If none is raised the person is a member. A concern must name a safety reason, and dislike is not one. The vouchers and the member with the concern then meet within a week with a member both trust. If they cannot settle it, the person does not join this time and can be vouched for again after three months.",
"expectations": "Vouchers check in with the new member through their first season and answer their questions. A voucher is not to blame for what a new member later does, and nobody loses their place for vouching in good faith. We never ask for documents, legal names or addresses. Vouching favors people who already have friends inside, so once a year we look at who we have not reached and invite people from there. Removal follows our conflict process."
}
},
{
"id": "membership-agreement",
"label": "Membership agreement",
"supportText": "Every member agrees in writing to a short list of shared commitments, and the agreement is the same for everyone.",
"sections": {
"eligibility": "Anyone who accepts our commitments can agree to them. The agreement is one page that says what we are for, what members commit to and what we will not do, and it points to this Rule for everything else. Whoever starts the group can write it before anyone joins, and the members can change it later. Our other ways of joining decide when a person is asked to agree.",
"joiningProcess": "A member reads the agreement with the new person, explains the reason behind each commitment and answers questions. The person agrees by signing on paper or by saying yes where a member records it. Both are recorded with a first name and the date only. Members read the agreement aloud together once a year and at the first meeting of each new crew, and anyone can ask what a line means. Changing the agreement is a decision of the whole group, announced where every member will see it at least fifteen days ahead.",
"expectations": "Members keep the commitments they agreed to. When someone may have broken one, we start with our conflict process and never with removal alone. A commitment nobody can explain is the first thing a new member will want to fix, so every commitment carries its reason. The agreement breaks when it grows into a rulebook nobody reads, so we keep it to one page. Removal follows our conflict process."
}
},
{
"id": "member-share-or-dues",
"label": "Member share or dues",
"supportText": "Members put in a share or dues they can afford, with a waiver on request, and money never decides who gets aid.",
"sections": {
"eligibility": "Members put in money because they own and run the group together, not to buy a service. A co-op asks each member for one member share at one price. A volunteer group may ask for dues to cover rent and supplies. Money never decides who gets aid, and a member who cannot pay asks for a waiver with no questions asked. One member has one vote however much they put in.",
"joiningProcess": "A new member pays their share or dues when they join. Dues run on a sliding scale the member chooses. A share can be paid in installments over a year, and a co-op can take it from payroll with the member’s written consent, never taking pay below minimum wage. Two members who keep the books record each payment with a first name and the amount, and a co-op keeps its legal register of shares apart. They never record why a member chose a level or asked for a waiver. The members set the share price, the dues and the scale by a decision of the whole group.",
"expectations": "A member behind on dues gets a friendly reminder and three months to catch up or ask for a waiver, then moves to former members and can come back by paying or asking. An unpaid share comes out of the member’s future surplus instead. The books are open to every member. A share is returned within a year of leaving unless our bylaws say otherwise, and dues are not refunded. Money pulls against open doors, so we review the scale and how many members use waivers every year."
}
},
{
"id": "roster-upkeep",
"label": "Roster upkeep",
"supportText": "Once a month two members update who is active, check in kindly with anyone who has drifted and keep the roster private.",
"sections": {
"eligibility": "Our counts of active members work only if our roster is current. Two members keep it for a term of one year. The roster holds only what the work needs, a name or nickname, one way to reach each member, their tier and any leave. It never holds addresses, legal names or why someone stepped back. It lives where only the roster keepers can see it, never in shared text or a public channel.",
"joiningProcess": "In the first week of each month the roster keepers look only at the month just ended. They add new members, mark leaves and returns, and note who took no part. If our Rule sets a monthly floor for a tier, they apply it from this look. A member who has taken no part for six months and is not on leave gets a friendly message saying we miss them, they are welcome back and they can take leave. With no reply within a month, the member moves to former members. The roster keepers post how many members are active, never who is not, where every member will see it.",
"expectations": "Moving someone to former members is not removal and carries no judgment. Former members can come back by asking and pick up where they left off. Roster keepers never share the roster outside the group and delete former members’ details after two years. Upkeep pulls against privacy, so we keep only what the count needs. If the roster keepers fall behind two months running, the members name new ones. Disputes about the roster go to our conflict process."
}
},
{
"id": "requirements-for-some-roles",
"label": "Requirements for some roles",
"supportText": "Roles that carry real risk, such as giving medical care, have written requirements for skills, training or age.",
"sections": {
"eligibility": "Belonging to the group never depends on skills, credentials or age. Some roles do, because doing them without the right skill can hurt someone. Those roles, and only those, have written requirements, and a member who does not meet them can do every other job. Until we list our own, the only such roles are medical care beyond basic first aid and any role the law limits to adults, and members under eighteen do not take them.",
"joiningProcess": "For each role on the list we write down the requirement and how a member shows they meet it. A skill is shown by doing the role at least three times beside members who hold it, until two of them agree the member is ready. A course is shown by finishing it, and a member can show a certificate instead. The member who keeps the list records only that it was seen and when it ends, never a copy or a legal name. We take members at their word about their age and never ask for ID. We pay for or find free courses for any member who wants one of these roles.",
"expectations": "Members in these roles keep their skills current. A member whose training lapses steps back from the role until it is renewed and keeps their place in the group. Requirements pull against open doors, so we list only roles where the risk is real and check the list every year. A member who thinks a requirement shuts people out for no good reason can ask the members to change it. Anyone can stop a role being done unsafely on the spot. Removal follows our conflict process."
}
},
{
"id": "member-groups",
"label": "Member groups",
"supportText": "Our members are groups that keep their own Rules, and each sends delegates to decide network matters together.",
"sections": {
"eligibility": "We are a network, and our members are groups rather than individuals. Any group that shares our aims, agrees to our network agreement and keeps its own Rule can join, whatever its size or legal form. A member group decides everything about itself. The network decides only what members agreed to share, such as supplies, spaces, tools or a common voice. If we also have individual members, the rest of our Rule on joining covers them.",
"joiningProcess": "A group that wants to join meets two delegates from existing member groups and shares a short description of its work and two ways to reach it. One existing member group sponsors it. The delegates post the request where every member will see it, and the group joins when no member group raises a reasoned concern within fifteen days. If a concern stands, member groups decide by our supermajority share, one vote per member group. Each member group names one or two delegates and tells the network when they change.",
"expectations": "Each member group sends a delegate to network meetings, follows the network agreement when using shared resources and keeps what it learns about other groups private. A member group can leave at any time and hands back shared keys and equipment within two weeks. The network never overrides a member group’s own decisions. One vote per group favors small groups, so we review the balance every year. A member group that breaks the agreement can be paused from shared resources and removed only as our conflict process says."
}
},
{
"id": "common-bond",
"label": "Common bond",
"supportText": "Membership is open to people who share a tie we name, such as living in our area or working in our trade.",
"sections": {
"eligibility": "Our members share a common bond, such as living in our neighborhood or building, working in our trade or using our shop. Until we name it, the bond is living or working in the area where we do most of our work. The bond draws a clear edge around who decides and never limits who we help. This says who can join, and the rest of our Rule says how. Where our Rule lets anyone join, anyone who shares our bond can join that way.",
"joiningProcess": "A person who wants to join tells a member that they share our bond. We take their word for it and never ask for proof such as ID, a lease or a pay stub, unless the law requires it, as for a credit union account or income-limited housing. They then join the way our Rule says. A member who moves away or leaves the trade can stay a member for a year and then becomes a former member unless the members agree to widen the bond. The members change the bond by a decision of the whole group, announced where every member will see it at least fifteen days ahead.",
"expectations": "Members tell us when they no longer share the bond. A bond pulls against reach and can quietly shut out people we mean to include, such as neighbors without a fixed address, so we name it in plain words and write it to include them. Every year we check who the bond leaves out. Doubts about whether someone shares the bond are settled by two members who know the person, never by documents. Removal follows our conflict process."
}
},
{
"id": "lottery-for-limited-places",
"label": "Lottery for limited places",
"supportText": "When more eligible people ask to join than we have places, we choose among them by a lottery members can watch.",
"sections": {
"eligibility": "We use a lottery when places are limited, such as homes in a housing co-op, seats in a program or spots in a crew, and more people qualify than we can take. A lottery is fairer than first come first served, which favors people with free time, and fairer than choosing by who we know. Everyone who meets our written criteria has the same chance. It never replaces our process for approving members.",
"joiningProcess": "We announce how many places are open, the criteria and the date of the draw at least two weeks ahead. People who meet the criteria add their names by that date, and two members check each entry against the criteria only. On the draw date two members with no one close to them in the pool draw names, by numbered slips or a random number tool any member can check, while other members watch. Names drawn are offered places in order, and the rest form a waiting list in the order drawn.",
"expectations": "People who get a place join on the same terms as every other member. The waiting list holds only names and one way to reach each person, is seen only by the two members who keep it and is deleted after a year. A lottery pulls against choosing for particular needs, so we can hold a share of places for needs we name before the draw, such as an accessible home. If a draw goes wrong we redo it in the open. Removal follows our conflict process."
}
},
{
"id": "member-classes",
"label": "Member classes",
"supportText": "We have more than one kind of member, such as workers and shoppers, and each class has written rights and seats.",
"sections": {
"eligibility": "We use this when different kinds of people own the group together, as in a co-op owned by its workers and the people who shop there, or a group with worker and supporter members. Each class has written rights and duties. Every member has one vote, and each class elects a set share of the seats on any board or council we have.",
"joiningProcess": "A person joins the class that fits how they take part, the way our Rule says people join, and someone who fits two classes chooses one. Until we set our own shares, each class elects an equal share of the seats. Each class meets on its own when it wants and can bring a proposal to the whole group. Changing what a class can do or its share of seats is a decision of the whole group, announced at least fifteen days ahead, and it also needs our supermajority share of each class it affects.",
"expectations": "Members move to another class when how they take part changes, by telling the members who keep our roster. Every member can see the books and every decision whatever their class. Classes pull against unity and can harden into blocs, so we review the classes and the seats every two years. A class with fewer than three members has its seats filled by all members until it grows. Removal follows our conflict process."
}
},
{
"id": "a-voice-for-people-without-a-vote",
"label": "A voice for people without a vote",
"supportText": "People who do our work without a vote are heard before we decide anything that touches them.",
"sections": {
"eligibility": "This card covers volunteers who are not members, staff who are not owners, contractors and candidates, and the people we serve where they choose to take part. Keeping the vote to members pulls against hearing the people closest to the work. It breaks when they learn of a decision after it is made, or when a voice becomes a reason never to open the vote. A voice never replaces a path to membership.",
"joiningProcess": "Anyone who has worked with us for a month, and anyone we serve who wants to, can come to an open forum. We hold it at least every three months and before any decision about pay, hours, safety, aid or who may join, except one made alone to prevent immediate harm, which we report to the forum afterward. Those at the forum choose one or two speakers among themselves. Speakers serve six months, come to our meetings and speak on any item, and only the people they speak for can replace them. Anyone this card covers can put an item on a meeting’s agenda by asking any member, and it is heard within two meetings. A proposal that touches their work, pay, aid or safety goes where they will see it at least seven days before we decide, unless waiting would cause immediate harm. Their comments go into our records beside the decision, without names unless the person asks to be named.",
"expectations": "Within one meeting, members answer each concern from the forum or the speakers in writing, saying what changed or why not. Nobody loses work, shifts, pay or aid for speaking up. Retaliation goes straight to our conflict process and skips its first steps. Once a year the forum hears how to become a member, and anyone can ask a member to walk them through it. Everyone here, with or without a vote, treats others with respect and is treated the same way in every channel, meeting and space. Only our conflict process can end someone’s place in the forum."
} }
} }
] ]
@@ -0,0 +1,51 @@
{
"_comment": "Create flow, recommendation lines on each deck (Figma CR-291, deck seven board 24716:27074 and card treatments 24717:27255). Draft copy. Dani signs off the why lines and the alternative row.",
"deckLine": "Recommended for your group",
"tags": {
"recommended": "RECOMMENDED",
"everyRuleHas": "Every Rule has"
},
"why": {
"fit": "Fits {group}",
"fitWithAnswer": "Fits {group} that {answer}",
"opened": "Suggested because you said {answer}",
"added": "Added because you said {answer}"
},
"group": {
"kindAndSize": "{kind} of {size}",
"kindOnly": "{kind}",
"sizeOnly": "a group of {size}",
"kind": {
"workersCoop": "a worker co-op",
"mutualAid": "a mutual aid group",
"nonprofit": "a nonprofit",
"forProfit": "a business",
"openSource": "an open source project",
"dao": "a DAO"
},
"size": {
"oneMember": "one person",
"twoToFive": "2 to 5 people",
"sixToTwelve": "6 to 12 people",
"thirteenToOneHundred": "13 to 100 people",
"oneHundredToOneHundredK": "more than 100 people"
}
},
"answerThirdPerson": {
"network": "has other groups among its members or partners",
"nonvoters": "has people doing the work without a vote",
"employer": "pays some people to do its work",
"sponsor": "relies on another organization for its money or legal status",
"records": "keeps information about the people it serves",
"risky": "does risky or licensed work"
},
"answerYouSaid": {
"network": "some of your members or partners are groups",
"nonvoters": "some people who do your work do not vote",
"employer": "you pay some people to do your work",
"sponsor": "another organization holds your money or legal status",
"records": "you keep information about the people you serve",
"risky": "your work carries physical risk, or needs a license, permit or inspection"
},
"alternative": "Another way groups do this"
}
@@ -20,6 +20,10 @@
"description": "Update the name shown on your public CommunityRule.", "description": "Update the name shown on your public CommunityRule.",
"ariaEditTitle": "Edit community name" "ariaEditTitle": "Edit community name"
}, },
"bindingArbitrationWarning": {
"title": "Binding arbitration needs each member’s own yes",
"description": "Open admission and membership by token do not record that yes. Each member still has to agree separately before binding arbitration applies to them."
},
"categories": [ "categories": [
{ {
"name": "Values", "name": "Values",
+60
View File
@@ -0,0 +1,60 @@
{
"_comment": "Create flow, the setup questions screen after the four setup questions (Figma CR-291, frame 24715:27505) and the setup copy the real-group review changed. Copy agreed 24 Oct. The question ids are the trait keys in _jobs.json.",
"title": "A few more things about your group",
"description": "Tick any that are true. Each one adds or changes a suggested card. We keep your answers with your draft and never publish them.",
"continueButton": "Continue",
"skipButton": "Skip",
"questions": [
{
"id": "network",
"label": "Some of our members or partners are groups",
"help": "Such as crews in a network, organizations in a coalition or businesses in a co-op"
},
{
"id": "nonvoters",
"label": "Some people who do our work do not vote",
"help": "Such as volunteers who are not members, staff who are not owners or contractors"
},
{
"id": "employer",
"label": "We pay some people to do our work",
"help": "Wages or salary, including worker-owners on payroll. Leave it unticked if everyone is a volunteer or a contractor"
},
{
"id": "sponsor",
"label": "Another organization holds our money or legal status",
"help": "Such as a fiscal sponsor, a parent nonprofit or a church that takes donations for us"
},
{
"id": "records",
"label": "We keep information about the people we serve",
"help": "Such as names, addresses, needs, health details or intake notes. A list of our own members does not count"
},
{
"id": "risky",
"label": "Our work carries physical risk, or needs a license, permit or inspection",
"help": "Such as medical care, building, driving, cooking for the public or caring for children"
}
],
"sizeHelp": "Count the people who have a say in your big decisions. If others take part without a vote, you can say so later in setup.",
"kind": {
"title": "What kind of group are you, mainly?",
"hint": "Pick one.",
"othersTitle": "Anything else that fits?",
"othersHelp": "These help us choose between cards and never add cards on their own."
},
"supermajorityShare": {
"_comment": "Draft copy, waits on Dani’s sign-off. The question itself was agreed 27 Oct, calls 3 and 14. Nothing is preselected. A group that starts from a template answers it at final review.",
"title": "What share of your members must agree to your biggest decisions?",
"help": "This is your supermajority share. Your Rule uses it for votes such as changing the Rule, ending a membership and emergencies. Cards that set their own share keep it.",
"options": {
"twoThirds": "Two thirds",
"threeQuarters": "Three quarters",
"everyone": "Everyone",
"custom": "Another share"
},
"customLabel": "Share of active members who vote, as a percent",
"warnEveryone": "With everyone, one member can block a change to your Rule or a way out of a deadlock.",
"warnHalfOrBelow": "At half or below, a change to your Rule could pass without most of the members who vote."
}
}
+132
View File
@@ -0,0 +1,132 @@
{
"sectionTitle": "Shared terms",
"about": "Shared terms print with every finished Rule. Only the meaning prints. setBy and whenNoCardCoversIt are notes for writers and do not print. The launch supermajority share is three quarters.",
"terms": [
{
"term": "active members",
"meaning": "Members in good standing who are not on leave. Every share, quorum and threshold in this Rule counts active members, never everyone who ever joined. A levels card can narrow this to core members or maintainers.",
"setBy": "Roster upkeep, Leave and return, Volunteers and core members, Contributors and maintainers",
"whenNoCardCoversIt": null
},
{
"term": "our supermajority share",
"meaning": "Three quarters of active members who vote, for decisions that need more than a majority.",
"setBy": "The launch setting, fixed at three quarters for every group (29 October)",
"whenNoCardCoversIt": "Three quarters of active members who vote."
},
{
"term": "our quorum",
"meaning": "At least half of active members, or a tenth when we have more than a hundred, unless a card sets its own.",
"setBy": null,
"whenNoCardCoversIt": null
},
{
"term": "a decision of the whole group",
"meaning": "A decision every active member is asked to take part in, made the way this Rule makes its big decisions.",
"setBy": "The card for big decisions",
"whenNoCardCoversIt": "Our supermajority share of active members who vote, with our quorum voting."
},
{
"term": "our records",
"meaning": "The one place the group keeps this Rule, its decisions, minutes and how-tos.",
"setBy": "Shared records, or GitHub / GitLab for open source projects",
"whenNoCardCoversIt": "A shared folder every member can open, named at our next meeting."
},
{
"term": "where every member will see it",
"meaning": "Posted where our members read announcements, plus email, text or a call for any member who is not on it.",
"setBy": "Announcements channel, Email distribution list, Phone and text tree",
"whenNoCardCoversIt": "Our main group chat, plus a direct message or call to any member who is not on it."
},
{
"term": "our conflict process",
"meaning": "The steps this Rule sets for what happens when we disagree.",
"setBy": "The conflict cards",
"whenNoCardCoversIt": "The two people talk first, and if that fails, two people close to neither side, whom both sides accept, hear both and propose what happens next."
},
{
"term": "shared money",
"meaning": "Money, accounts and things of value the group holds together.",
"setBy": "Shared money",
"whenNoCardCoversIt": "Kept where at least two members can see it, with every payment written in our records."
},
{
"term": "a working group",
"meaning": "Two or more members with a written mandate from the group.",
"setBy": "Delegated decision-making",
"whenNoCardCoversIt": "The group creates each working group as a decision of the whole group, with a written aim, area, term and date to report back."
},
{
"term": "our roster",
"meaning": "The private list of members, kept by two members, with only a name or nickname, one way to reach each person, their level and any leave.",
"setBy": "Roster upkeep, Open admission",
"whenNoCardCoversIt": "Two members the group names keep it, and only they can see it."
},
{
"term": "our bylaws",
"meaning": "The legal documents of our organization, if it has any. Where the law or our bylaws require something different from this Rule, they govern and we change this Rule to match.",
"setBy": "Changing this Rule",
"whenNoCardCoversIt": "A group with no bylaws has this Rule as its only governing document."
},
{
"term": "the person harmed",
"meaning": "The person a harm happened to. This Rule names people by what happened, never by a label.",
"setBy": null,
"whenNoCardCoversIt": null
},
{
"term": "the person who caused harm",
"meaning": "The person whose actions caused a harm. This Rule names people by what they did, never by a label, because a person is more than one act.",
"setBy": null,
"whenNoCardCoversIt": null
},
{
"term": "our list for binding arbitration",
"meaning": "The few disputes written in this Rule that members each agreed in writing to settle by an arbitrator’s final decision instead of in court. Each member is bound only by the list as it stood when they agreed.",
"setBy": "Binding arbitration",
"whenNoCardCoversIt": "Nothing goes to binding arbitration unless both sides agree in writing after the dispute begins."
},
{
"term": "matters outside arbitration",
"meaning": "Threats, violence, stalking, abuse, sexual assault, harassment, discrimination, a privacy breach and a child’s safety, and anything that grows out of a case of harm. Retaliation for raising a concern, a report of fraud, misuse of shared money or law-breaking, and what someone said when reporting harm. A pause, removal, anyone’s aid, and what this Rule, our bylaws or a membership agreement means. Pay, hours, safety at work, organizing, a union or a job ending for anyone who works for us. Anyone’s home, rent, deposit, repairs or right to stay in our housing. Any claim the law will not let anyone give up in advance. These follow our conflict process and any route the law allows. An arbitrator hears a claim about harm, pay or a home only if the person who raised it, or the person it harmed, asks for that in writing. They ask after the dispute begins, with time to get advice. A pause, removal, anyone’s aid and what this Rule means never go to an arbitrator.",
"setBy": "Binding arbitration, Non-binding arbitration",
"whenNoCardCoversIt": null
},
{
"term": "our fair hearing safeguards",
"meaning": "Each side can bring a friend, an advocate, an interpreter or a lawyer, and take part by phone or video. We provide interpreting, captions and a step-free room when anyone asks, and hold hearings at times both sides can make. Each side sees everything the other gives the person hearing it, without other people’s private details, and that person never discusses it with one side alone. The person hearing it can ask to see any of our records that bear on the question, without details of the people we serve. Records from our conflict process are used only with the consent of the person they are about. Anyone who fears the other side can give their account apart from them. A side in hospital, in custody, without a home or in crisis can pause the deadlines in our steps for 30 days at a time, though legal deadlines keep running. Nobody has to give a legal name or show ID unless a court requires it. Nobody loses their place, shifts, pay or aid for asking for a hearing, taking part, declining or being a witness. Nobody is asked to keep a dispute secret from family, an adviser, a lawyer, a union or a government agency.",
"setBy": "Binding arbitration, Non-binding arbitration",
"whenNoCardCoversIt": null
},
{
"term": "the body that appoints for us",
"meaning": "The co-op federation, dispute resolution office or arbitration service we name in our records to appoint a person close to neither side when two sides cannot agree on one. It sends both sides the same three names, each side strikes one and it appoints from the rest. Until we name one, it is a public dispute resolution office or bar association program near the side that did not start the arbitration.",
"setBy": "Binding arbitration, Non-binding arbitration",
"whenNoCardCoversIt": "A public dispute resolution office or bar association program near the side that did not start the arbitration. For binding arbitration only, a court can appoint."
},
{
"term": "legal deadlines",
"meaning": "The time the law gives anyone to file a claim, a charge or an answer. Some are only days, such as answering an eviction, and some claims against a city need written notice within months. Talking, mediation, arbitration and our conflict process never stop them, though the group can agree not to count that time against a member in a claim against the group. Anyone can file to protect a deadline at any point and ask for the case to wait, and nobody loses their place for it.",
"setBy": "Legal action, Binding arbitration, Non-binding arbitration",
"whenNoCardCoversIt": null
},
{
"term": "the members who handle legal matters",
"meaning": "The two members, and a backup, who receive legal papers for us, find a lawyer and keep a legal hold. They serve for one year. A member involved in a matter steps aside for it, and the backup acts.",
"setBy": "Legal action",
"whenNoCardCoversIt": "Any two members not involved in the matter, until a decision of the whole group names two. In a group of two or three, the one member not involved acts with a trusted person from outside."
},
{
"term": "a legal hold",
"meaning": "A pause on deleting anything a legal matter we expect may need, such as chats, email, texts, shared files and paper. It starts when legal papers, a threat of a lawsuit, a demand for records or news of an investigation arrives. It also starts when something happens that we can expect to end in a claim, such as a serious injury during our work or a police or immigration visit. Disappearing messages are turned off in chats about the matter, and we make no new lists or files about people for it beyond the short notes the matter needs. It ends when a lawyer says the matter is over, and deletion then returns to its usual dates. If no lawyer is involved, the members who handle legal matters review it every six months with the legal help on our list of outside help. They end it once no claim, deadline or investigation is still open.",
"setBy": "Legal action, Records about people, Security plan",
"whenNoCardCoversIt": "Whoever receives legal papers or a threat tells everyone who holds records about it the same day, and nothing about it is deleted until a lawyer says so."
},
{
"term": "our list of outside help",
"meaning": "The short list in our records, seen only by members, of who we call in immediate danger or in a medical or mental health crisis. It also names a mediator, legal help and support after harm.",
"setBy": "Outside help",
"whenNoCardCoversIt": "A crisis line that answers at any hour and a free legal help line, named at our next meeting."
}
]
}
+59 -57
View File
@@ -104,8 +104,10 @@ function bodyFromXlsxComposition(row: {
} }
/** /**
* Curated template chip rows — sourced from product Template Composition.xlsx * Curated template chip rows. Chip titles are card labels from
* (Governance Template × category columns). * `messages/en/create/customRule/*.json` (October 2026 deck remake), so
* `methodSlugFromTitle` resolves each one to a live `methods[].id`.
* Values match `coreValues.json` labels (case-insensitive).
*/ */
const COMPOSITION_BY_SLUG: Record< const COMPOSITION_BY_SLUG: Record<
string, string,
@@ -118,95 +120,95 @@ const COMPOSITION_BY_SLUG: Record<
} }
> = { > = {
consensus: { consensus: {
decisionMaking: "Consensus Decision-Making, Modified Consensus", decisionMaking: "Decision speeds, Consensus decision-making, Modified consensus",
membership: "Consensus or Vote-Based Approval, Peer Sponsorship", membership: "Members approve new members, Vouching",
values: "Consensus, Community Care, Horizontalism", values: "Consensus, Community care, Horizontalism",
communication: "In-Person Meetings, Loomio", communication: "How we run meetings, In-person meetings, Loomio",
conflict: "Consensus Building, Facilitated Negotiation", conflict: "Conflict ladder, Mediation",
}, },
"consensus-clusters": { "consensus-clusters": {
decisionMaking: "Sociocracy, Holacracy", decisionMaking: "Consent (sociocracy), Delegated decision-making",
membership: "Contribution Based, Orientation Required", membership: "Join by contributing, Orientation",
values: "Decentralization, Adaptability, Autonomy", values: "Decentralization, Adaptability, Autonomy",
communication: "Slack, Matrix / Element", communication: "How we run meetings, Shared records, Slack",
conflict: "Circle Processes, Restorative Practices", conflict: "Conflict ladder, Circle processes, Restorative practices",
}, },
"solidarity-network": { "solidarity-network": {
decisionMaking: "Do-ocracy, Modified Consensus", decisionMaking: "Do-ocracy, Modified consensus, Shared money",
membership: "Open Access, Peer Sponsorship", membership: "Open admission, Vouching, Member groups",
values: "Solidarity, Mutual Aid, Anti-oppression", values: "Solidarity, Mutual aid, Anti-oppression",
communication: "Signal, Matrix / Element", communication: "Signal, Matrix / Element",
conflict: "Peer Mediation, Restorative Practices", conflict: "Conflict ladder, Restorative practices",
}, },
"sortition-jury": { "sortition-jury": {
decisionMaking: "Lottery/Sortition, Deliberative Polling", decisionMaking: "Panel drawn by lot, Rotating roles",
membership: "Lottery / Sortition", membership: "Open admission, Roster upkeep",
values: "Fairness, Equity, Transparency", values: "Fairness, Equity, Transparency",
communication: "In-Person Meetings, Video Meetings", communication: "In-person meetings, Video meetings, Shared records",
conflict: "Lottery/Sortition, Rotational Judging", conflict: "Conflict ladder, Hearing panel",
}, },
"liquid-democracy": { "liquid-democracy": {
decisionMaking: "Delegated Decision-Making, Continuous Voting", decisionMaking: "Liquid democracy, Continuous voting",
membership: "Identity Verification, Open Access", membership: "Open admission, One person one account",
values: "Agency, Flexibility, Transparency", values: "Agency, Flexibility, Transparency",
communication: "Loomio, Discourse (Forum)", communication: "Loomio, Discourse (forum)",
conflict: "Ad Hoc Arbitration, Peer Mediation", conflict: "Conflict ladder, Non-binding arbitration",
}, },
"do-ocracy": { "do-ocracy": {
decisionMaking: "Do-ocracy, Lazy Consensus", decisionMaking: "Do-ocracy, Lazy consensus, Two-person sign-off",
membership: "Contribution Based, Skill-Based Contribution", membership: "Join by contributing, Rest and workload limits",
values: "Agency, Autonomy, Voluntarism", values: "Agency, Autonomy, Voluntarism",
communication: "GitHub / GitLab, Discord", communication: "GitHub / GitLab, Discord",
conflict: "Peer Mediation, Facilitated Negotiation", conflict: "Conflict ladder, Mediation",
}, },
"quadratic-governance": { "quadratic-governance": {
decisionMaking: "Quadratic Voting", decisionMaking: "Quadratic voting, Two-person sign-off",
membership: "Identity Verification, Pay-to-Join", membership: "One person one account, Member share or dues",
values: "Fairness, Innovation, Independence", values: "Fairness, Innovation, Independence",
communication: "Discourse (Forum), Discord", communication: "Discourse (forum), Discord",
conflict: "Supermajority Vote, Conflict Resolution Council", conflict: "Conflict ladder, Conflict working group",
}, },
"federated-clusters": { "federated-clusters": {
decisionMaking: "Consensus Seeking with Delegates", decisionMaking: "Consensus seeking with delegates",
membership: "Hybrid Approval Process, Membership Agreement or Pledge", membership: "Member groups, Membership agreement",
values: "Interoperability, Localism, Interdependence", values: "Interoperability, Localism, Interdependence",
communication: "Matrix / Element, Discourse (Forum)", communication: "Matrix / Element, Discourse (forum)",
conflict: "Internal Tribunal, Ad Hoc Arbitration", conflict: "Conflict ladder, Hearing panel, Non-binding arbitration",
}, },
devolution: { devolution: {
decisionMaking: "Autocratic Decision-Making, Delegated Decision-Making", decisionMaking: "One person decides, Delegated decision-making",
membership: "Invitation Only, Open Access", membership: "Starting with one person, Open admission",
values: "Capacity Building, Education, Maintenance", values: "Capacity building, Education, Maintenance",
communication: "Discord, GitHub / GitLab", communication: "Discord, GitHub / GitLab",
conflict: "Conflict Workshops, Managerial Decision", conflict: "Conflict ladder, Conflict workshops, Managerial decision",
}, },
"benevolent-dictator": { "benevolent-dictator": {
decisionMaking: "Autocratic Decision-Making, Hierarchical Decision-Making", decisionMaking: "One person decides, Hierarchical decision-making",
membership: "Invitation Only, Mentorship", membership: "Join by contributing, Contributors and maintainers",
values: "Reliability, Stewardship, Leadership", values: "Reliability, Stewardship, Leadership",
communication: "Email Distribution List, GitHub / GitLab", communication: "Email distribution list, GitHub / GitLab",
conflict: "Managerial Decision, Binding Contracts", conflict: "Conflict ladder, Managerial decision",
}, },
petition: { petition: {
decisionMaking: "Ranked Choice Voting, Majority Rule", decisionMaking: "Petition and referendum, Majority rule, Ranked choice voting",
membership: "Open Access, Identity Verification", membership: "Open admission, One person one account",
values: "Freedom of Information, Accountability, Participation", values: "Freedom of information, Accountability, Participation",
communication: "Loomio, Discourse (Forum)", communication: "Loomio, Discourse (forum)",
conflict: "Supermajority Vote, Binding Arbitration", conflict: "Conflict ladder, Non-binding arbitration, Legal action",
}, },
"self-appointed-board": { "self-appointed-board": {
decisionMaking: "Advisory Committees, Executive Committees", decisionMaking: "Appointed board, Advisory committees, Coordinating committee",
membership: "Invitation Only, Application & Review", membership: "Application and review, Paid staff",
values: "Stewardship, Resilience, Reliability", values: "Stewardship, Resilience, Reliability",
communication: "Video Meetings, Email Distribution List", communication: "Video meetings, Email distribution list",
conflict: "Judicial Committees, Internal Tribunal", conflict: "Conflict ladder, Harassment and discrimination complaints, Hearing panel",
}, },
"elected-board": { "elected-board": {
decisionMaking: "Elected Board of Directors, Majority Rule", decisionMaking: "Elected board of directors, General meeting, Majority rule",
membership: "Application & Review, Membership Agreement or Pledge", membership: "Application and review, Membership agreement",
values: "Accountability, Transparency, Trust", values: "Accountability, Transparency, Trust",
communication: "Email Distribution List, Slack", communication: "Email distribution list, Slack",
conflict: "Conflict Resolution Council, Mediation", conflict: "Conflict ladder, Mediation, Conflict working group",
}, },
}; };
@@ -274,7 +276,7 @@ const TEMPLATES: {
description: description:
'Power is held by autonomous "cells." A central hub acts as a switchboard for resources but cannot dictate cell activities.', 'Power is held by autonomous "cells." A central hub acts as a switchboard for resources but cannot dictate cell activities.',
sortOrder: 4, sortOrder: 4,
featured: false, featured: true,
body: bodyFromSlugComposition("solidarity-network"), body: bodyFromSlugComposition("solidarity-network"),
}, },
{ {
@@ -314,7 +316,7 @@ const TEMPLATES: {
description: description:
"Voting cost is squared (V²), preventing a majority from steamrolling a passionate minority.", "Voting cost is squared (V²), preventing a majority from steamrolling a passionate minority.",
sortOrder: 8, sortOrder: 8,
featured: true, featured: false,
body: bodyFromSlugComposition("quadratic-governance"), body: bodyFromSlugComposition("quadratic-governance"),
}, },
{ {
@@ -35,7 +35,7 @@ async function declineDiscardCustomizeEdits() {
/** /**
* Mounts the screen with optional starting state and exposes the latest * Mounts the screen with optional starting state and exposes the latest
* `state` to the test harness so we can assert the persistence side of * `state` to the test harness so we can assert the persistence side of
* the Add Platform flow without driving the wizard's Next chain. * the Add card flow without driving the wizard's Next chain.
*/ */
const EMPTY_STATE: CreateFlowState = {}; const EMPTY_STATE: CreateFlowState = {};
@@ -62,7 +62,7 @@ function ScreenWithStateProbe({
* snapping back to preset copy. See {@link CommunicationMethodEditFields} * snapping back to preset copy. See {@link CommunicationMethodEditFields}
* and `buildPublishPayload` for the read side. * and `buildPublishPayload` for the read side.
*/ */
describe("CommunicationMethodsScreen — Add Platform persistence", () => { describe("CommunicationMethodsScreen — Add card persistence", () => {
it("starts empty and Generate draft inserts the catalog preset", async () => { it("starts empty and Generate draft inserts the catalog preset", async () => {
let latest: CreateFlowState = {}; let latest: CreateFlowState = {};
render( render(
@@ -74,7 +74,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
@@ -96,7 +96,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } }); fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
fireEvent.click( fireEvent.click(
within(dialog).getByRole("button", { within(dialog).getByRole("button", {
name: "Add Platform", name: "Add card",
}), }),
); );
@@ -122,7 +122,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
void dialog; void dialog;
@@ -157,7 +157,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
void latest; void latest;
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const textareas = within(dialog).getAllByRole( const textareas = within(dialog).getAllByRole(
@@ -190,7 +190,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const textareas = within(dialog).getAllByRole( const textareas = within(dialog).getAllByRole(
@@ -259,7 +259,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const textareas = within(dialog).getAllByRole( const textareas = within(dialog).getAllByRole(
@@ -283,7 +283,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "More options" })); fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
@@ -312,7 +312,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "More options" })); fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
@@ -344,7 +344,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const textboxes = within(dialog).getAllByRole( const textboxes = within(dialog).getAllByRole(
@@ -387,7 +387,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const textboxes = within(dialog).getAllByRole( const textboxes = within(dialog).getAllByRole(
@@ -448,7 +448,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
await screen.findByText("Custom policy details"), await screen.findByText("Custom policy details"),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "Core Principle & Scope" }), screen.getByRole("button", { name: "Core principle and scope" }),
).toBeInTheDocument(); ).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Finalize policy" })); fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
@@ -470,7 +470,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
for (const button of within(dialog).getAllByRole("button", { for (const button of within(dialog).getAllByRole("button", {
@@ -490,11 +490,11 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
await screen.findByText("Custom policy details"), await screen.findByText("Custom policy details"),
).toBeInTheDocument(); ).toBeInTheDocument();
fireEvent.click( fireEvent.click(
screen.getByRole("button", { name: "Core Principle & Scope" }), screen.getByRole("button", { name: "Core principle and scope" }),
); );
expect(await screen.findByText("Add text block")).toBeInTheDocument(); expect(await screen.findByText("Add text block")).toBeInTheDocument();
expect( expect(
screen.getByDisplayValue(/We prioritize privacy and security/i), screen.getByDisplayValue(/Signal encrypts every message/i),
).toBeInTheDocument(); ).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Back" })); fireEvent.click(screen.getByRole("button", { name: "Back" }));
fireEvent.click(screen.getByRole("button", { name: "Back" })); fireEvent.click(screen.getByRole("button", { name: "Back" }));
@@ -526,7 +526,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const cardDialog = await screen.findByRole("dialog"); const cardDialog = await screen.findByRole("dialog");
fireEvent.click( fireEvent.click(
@@ -585,9 +585,9 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
? (document.getElementById(labelledby)?.textContent ?? "").trim() ? (document.getElementById(labelledby)?.textContent ?? "").trim()
: ""; : "";
}); });
expect(labels[0]).toMatch(/Logistics, Admin/); expect(labels[0]).toMatch(/How it works/);
expect(labels[1]).toMatch(/Code of Conduct/); expect(labels[1]).toMatch(/Norms/);
expect(labels[2]).toMatch(/Core Principle/); expect(labels[2]).toMatch(/Core principle/);
}); });
it("does not block tab close when the create modal is unchanged", async () => { it("does not block tab close when the create modal is unchanged", async () => {
@@ -600,7 +600,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
await screen.findByRole("dialog"); await screen.findByRole("dialog");
expect(dispatchBeforeUnload()).toBe(false); expect(dispatchBeforeUnload()).toBe(false);
@@ -616,7 +616,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const textboxes = within(dialog).getAllByRole( const textboxes = within(dialog).getAllByRole(
@@ -636,7 +636,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
); );
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "More options" })); fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
@@ -21,7 +21,7 @@ describe("CommunityStructureSelectScreen", () => {
renderWithProviders(<CommunityStructureSelectScreen />); renderWithProviders(<CommunityStructureSelectScreen />);
expect( expect(
screen.getByRole("button", { name: "Select Worker cooperative" }), screen.getByRole("button", { name: "Select Worker co-op" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "Select Local" }), screen.getByRole("button", { name: "Select Local" }),
@@ -18,7 +18,7 @@ describe("ConflictManagementScreen", () => {
render(<ConflictManagementScreen />); render(<ConflictManagementScreen />);
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { screen.getAllByRole("button", {
name: /Peer Mediation: Trained members/, name: /Conflict ladder: We talk one-on-one/,
})[0], })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
@@ -58,7 +58,7 @@ describe("CoreValuesSelectScreen", () => {
fireEvent.click( fireEvent.click(
within(dialog).getAllByRole("button", { name: "Generate draft" })[0], within(dialog).getAllByRole("button", { name: "Generate draft" })[0],
); );
expect(fields[0].value).toMatch(/everyone can use/i); expect(fields[0].value).toMatch(/usable by everyone/i);
}); });
it("closes a pending value without confirm when fields are unchanged", async () => { it("closes a pending value without confirm when fields are unchanged", async () => {
@@ -408,7 +408,7 @@ describe("CoreValuesSelectScreen", () => {
}); });
}); });
describe("five-value limit", () => { describe("six-value limit", () => {
async function addPresetValue(label: string) { async function addPresetValue(label: string) {
fireEvent.click(screen.getByText(label)); fireEvent.click(screen.getByText(label));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
@@ -420,22 +420,23 @@ describe("CoreValuesSelectScreen", () => {
it("shows a counter, disables remaining chips, and announces the limit", async () => { it("shows a counter, disables remaining chips, and announces the limit", async () => {
renderWithProviders(<CoreValuesSelectScreen />); renderWithProviders(<CoreValuesSelectScreen />);
expect(screen.getByText("0 of 5")).toBeInTheDocument(); expect(screen.getByText("0 of 6")).toBeInTheDocument();
await addPresetValue("Accessibility"); await addPresetValue("Accessibility");
await addPresetValue("Accountability"); await addPresetValue("Accountability");
await addPresetValue("Adaptability"); await addPresetValue("Adaptability");
await addPresetValue("Agency"); await addPresetValue("Agency");
await addPresetValue("Altruism"); await addPresetValue("Anti-oppression");
await addPresetValue("Autonomy");
expect(screen.getByText("5 of 5")).toBeInTheDocument(); expect(screen.getByText("6 of 6")).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
"Five of five values selected. Remaining values are unavailable until one is removed.", "Six of six values selected. Remaining values are unavailable until one is removed.",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "Select Anti-oppression" }), screen.getByRole("button", { name: "Select Capacity building" }),
).toBeDisabled(); ).toBeDisabled();
expect(screen.getByRole("button", { name: "Add value" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Add value" })).toBeDisabled();
expect(screen.getByRole("button", { name: /^add$/i })).toBeDisabled(); expect(screen.getByRole("button", { name: /^add$/i })).toBeDisabled();
+35 -12
View File
@@ -205,9 +205,9 @@ function FinalReviewWithCustomizeSelections() {
{ id: "2", label: "Accountability", state: "unselected" }, { id: "2", label: "Accountability", state: "unselected" },
], ],
selectedCommunicationMethodIds: ["signal"], selectedCommunicationMethodIds: ["signal"],
selectedMembershipMethodIds: ["open-access"], selectedMembershipMethodIds: ["open-admission"],
selectedDecisionApproachIds: ["lazy-consensus"], selectedDecisionApproachIds: ["lazy-consensus"],
selectedConflictManagementIds: ["peer-mediation"], selectedConflictManagementIds: ["conflict-ladder"],
}); });
}, [replaceState]); }, [replaceState]);
return <FinalReviewScreen />; return <FinalReviewScreen />;
@@ -220,16 +220,39 @@ describe("FinalReviewScreen — prefilled selections", () => {
expect(screen.getByText("Accessibility")).toBeInTheDocument(); expect(screen.getByText("Accessibility")).toBeInTheDocument();
}); });
expect(screen.getByText("Signal")).toBeInTheDocument(); expect(screen.getByText("Signal")).toBeInTheDocument();
expect(screen.getByText("Open Access")).toBeInTheDocument(); expect(screen.getByText("Open admission")).toBeInTheDocument();
expect(screen.getByText("Lazy Consensus")).toBeInTheDocument(); expect(screen.getByText("Lazy consensus")).toBeInTheDocument();
expect(screen.getByText("Peer Mediation")).toBeInTheDocument(); expect(screen.getByText("Conflict ladder")).toBeInTheDocument();
// Demo chips from `finalReview.json` must not leak through once the // Demo chips from `finalReview.json` must not leak through once the
// user has real selections: "Open Admission" is shipped as fallback, // user has real selections. The fallback still says "Open Admission".
// while the customize flow resolves to "Open Access".
expect(screen.queryByText("Open Admission")).not.toBeInTheDocument(); expect(screen.queryByText("Open Admission")).not.toBeInTheDocument();
expect(screen.queryByText("Consciousness")).not.toBeInTheDocument(); expect(screen.queryByText("Consciousness")).not.toBeInTheDocument();
}); });
it("warns when binding arbitration sits next to a join that records no yes", async () => {
function ReviewWithOpenJoinAndArbitration() {
const { replaceState } = useCreateFlow();
useLayoutEffect(() => {
replaceState({
title: "Oak Park Commons",
selectedMembershipMethodIds: ["open-admission"],
selectedConflictManagementIds: ["binding-arbitration"],
});
}, [replaceState]);
return <FinalReviewScreen />;
}
render(<ReviewWithOpenJoinAndArbitration />);
expect(
await screen.findByText(/Binding arbitration needs each member.s own yes/),
).toBeInTheDocument();
expect(
screen.getByText(
/Each member still has to agree separately before binding arbitration applies/,
),
).toBeInTheDocument();
});
}); });
describe("FinalReviewScreen — chip detail modal", () => { describe("FinalReviewScreen — chip detail modal", () => {
@@ -245,7 +268,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText( screen.getByText(
/Encrypted messaging for high-security, private coordination\./i, /encrypted Signal groups/i,
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -277,7 +300,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
); );
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: catalogChipName("Video Meetings") }), await screen.findByRole("button", { name: catalogChipName("Video meetings") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect(within(dialog).getAllByRole("textbox")).toHaveLength(3); expect(within(dialog).getAllByRole("textbox")).toHaveLength(3);
@@ -1021,9 +1044,9 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
? (document.getElementById(labelledby)?.textContent ?? "").trim() ? (document.getElementById(labelledby)?.textContent ?? "").trim()
: ""; : "";
}); });
expect(labels[0]).toMatch(/Logistics, Admin/); expect(labels[0]).toMatch(/How it works/);
expect(labels[1]).toMatch(/Code of Conduct/); expect(labels[1]).toMatch(/Norms/);
expect(labels[2]).toMatch(/Core Principle/); expect(labels[2]).toMatch(/Core principle/);
}); });
}); });
@@ -18,7 +18,7 @@ describe("MembershipMethodsScreen", () => {
render(<MembershipMethodsScreen />); render(<MembershipMethodsScreen />);
fireEvent.click( fireEvent.click(
screen.getAllByRole("button", { screen.getAllByRole("button", {
name: /Open Access: Maximum inclusion/, name: /Open admission: Anyone who agrees/,
})[0], })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
+1 -1
View File
@@ -48,7 +48,7 @@ test.describe("Critical User Journeys", () => {
await page.locator("text=Consensus").first().click(); await page.locator("text=Consensus").first().click();
await page.locator("text=Do-ocracy").first().click(); await page.locator("text=Do-ocracy").first().click();
await page.locator("text=Devolution").first().click(); await page.locator("text=Devolution").first().click();
await page.locator("text=Quadratic Governance").first().click(); await page.locator("text=Solidarity Network").first().click();
// 7. User checks out features // 7. User checks out features
const features = [ const features = [
+27880
View File
File diff suppressed because it is too large. Load diff
+194
View File
@@ -0,0 +1,194 @@
# How far each card reaches
Made by engine/run-profiles.ts. Counts are out of the 92 real-group profiles as given, 43 mutual aid groups and 49 worker co-ops and close relatives. Recommended counts every recommended card, which has no cap, and Shown counts the cards in the seven. With fixes gives both counts after the marks in _marksToConfirm.json, only where they change.
## Decisions
| Job | Tier | Card | Recommended | Shown | With fixes |
| --- | --- | --- | --- | --- | --- |
| Everyday decisions | frame | Decision speeds (every Rule) | 92 | 92 | |
| Everyday decisions | frame | Two-person sign-off | 0 | 0 | |
| Everyday decisions | frame | Do-ocracy | 0 | 0 | |
| Everyday decisions | frame | Advice process | 0 | 0 | |
| Big decisions | fitted | Lazy consensus | 31 | 31 | |
| Big decisions | fitted | Modified consensus | 13 | 13 | |
| Big decisions | fitted | Weighted voting | 0 | 0 | |
| Big decisions | fitted | General meeting | 5 | 5 | |
| Big decisions | fitted | Hierarchical decision-making | 2 | 2 | |
| Big decisions | fitted | One person decides | 7 | 7 | |
| Big decisions | fitted | Consensus decision-making | 4 | 4 | |
| Big decisions | fitted | Consent (sociocracy) | 28 | 28 | |
| Big decisions | fitted | Majority rule | 1 | 1 | |
| Big decisions | fitted | Supermajority rule | 0 | 0 | |
| Big decisions | fitted | Consensus seeking with delegates | 1 | 1 | |
| Changing this Rule | floor | Changing this Rule (every Rule) | 92 | 92 | |
| Changing this Rule | floor | Petition and referendum | 0 | 0 | |
| Changing this Rule | floor | Closing the group | 0 | 0 | |
| Emergencies | safety | Emergency decisions (every Rule) | 92 | 92 | |
| Emergencies | safety | On-site lead (added) | 43 | 43 | |
| Money | floor | Shared money (every Rule) | 75 | 56 | |
| Money | floor | Annual budget | 17 | 12 | |
| Money | floor | Pay and hiring (added) | 39 | 37 | |
| Money | floor | Decide by formula | 0 | 0 | |
| Money | floor | Participatory budgeting | 0 | 0 | |
| Money | floor | How we are covered (added) | 14 | 14 | |
| Spending | floor | Spending limits | 75 | 17 | |
| Spending | floor | No card | 17 | | |
| Roles and delegation | fitted | Elected board of directors | 12 | 12 | |
| Roles and delegation | fitted | Delegated decision-making | 69 | 69 | |
| Roles and delegation | fitted | Rotating roles | 7 | 7 | |
| Roles and delegation | fitted | Decisions about one person | 4 | 4 | |
| Roles and delegation | fitted | Follow-through | 0 | 0 | |
| Roles and delegation | fitted | Coordinating committee | 0 | 0 | |
| Roles and delegation | fitted | Appointed board | 0 | 0 | |
| Roles and delegation | fitted | Conflicts of interest | 0 | 0 | |
| Roles and delegation | fitted | Ranked choice voting | 0 | 0 | |
| Roles and delegation | fitted | Panel drawn by lot | 0 | 0 | |
63 of 92 groups have more than seven recommended in this deck. Recommended cards per group run from 6 to 10.
## Membership
| Job | Tier | Card | Recommended | Shown | With fixes |
| --- | --- | --- | --- | --- | --- |
| Joining | frame | Open admission | 43 | 43 | |
| Joining | frame | Worker-owner candidacy | 47 | 47 | |
| Joining | frame | Join by contributing | 0 | 0 | |
| Joining | frame | Membership by token | 0 | 0 | |
| Joining | frame | One person one account | 0 | 0 | |
| Joining | frame | Members approve new members | 0 | 0 | |
| Joining | frame | Application and review | 2 | 2 | |
| Joining | frame | Vouching | 0 | 0 | |
| Joining | frame | Membership agreement | 0 | 0 | |
| Joining | frame | Member share or dues | 0 | 0 | |
| Joining | frame | Member groups (added) | 18 | 18 | |
| Joining | frame | Common bond | 0 | 0 | |
| Joining | frame | Lottery for limited places | 0 | 0 | |
| Levels of membership | fitted | Volunteers and core members | 24 | 24 | |
| Levels of membership | fitted | Contributors and maintainers | 0 | 0 | |
| Levels of membership | fitted | Equal membership | 56 | 56 | 16 and 16 |
| Levels of membership | fitted | Starting with one person | 0 | 0 | |
| Levels of membership | fitted | Owners and staff | 2 | 2 | 42 and 42 |
| Levels of membership | fitted | Paid staff | 4 | 4 | |
| Levels of membership | fitted | Trial period | 6 | 6 | |
| Levels of membership | fitted | Requirements for some roles | 0 | 0 | |
| Levels of membership | fitted | Member classes | 0 | 0 | |
| Levels of membership | fitted | A voice for people without a vote (added) | 31 | 31 | |
| Learning how we work | fitted | Companions | 39 | 39 | |
| Learning how we work | fitted | Joining in crews | 46 | 46 | |
| Learning how we work | fitted | Orientation | 7 | 7 | |
| Staying and leaving | fitted | Rest and workload limits | 23 | 23 | |
| Staying and leaving | fitted | Leave and return | 32 | 32 | |
| Staying and leaving | fitted | Leaving the group | 35 | 35 | |
| Staying and leaving | fitted | Handing over roles | 2 | 2 | |
| Staying and leaving | fitted | Roster upkeep | 0 | 0 | |
| Removal | safety | Ending a membership (every Rule) | 92 | 92 | |
0 of 92 groups have more than seven recommended in this deck. Recommended cards per group run from 5 to 7.
## Conflict
| Job | Tier | Card | Recommended | Shown | With fixes |
| --- | --- | --- | --- | --- | --- |
| First steps | frame | Conflict ladder (every Rule) | 92 | 92 | |
| Safety first | safety | Precautionary suspension (every Rule) | 92 | 92 | |
| Safety first | safety | Safety lead | 0 | 0 | |
| Safety first | safety | Working with children | 0 | 0 | |
| Who hears serious cases | fitted | Conflict working group | 62 | 62 | |
| Who hears serious cases | fitted | Mediation | 11 | 11 | |
| Who hears serious cases | fitted | Code of conduct team | 0 | 0 | |
| Who hears serious cases | fitted | Hearing panel | 17 | 17 | |
| Who hears serious cases | fitted | Managerial decision | 2 | 2 | |
| Repair and removal | floor | Graduated responses (every Rule) | 92 | 92 | |
| Repair and removal | floor | Restorative practices | 0 | 0 | |
| Repair and removal | floor | Accountability process | 0 | 0 | |
| Outside help and legal duties | safety | Harassment and discrimination complaints | 55 | 55 | |
| Outside help and legal duties | safety | Outside help | 37 | 37 | |
| Outside help and legal duties | safety | Whistleblower protection | 0 | 0 | |
| Legal matters | floor | Legal action (every Rule) | 92 | 0 | |
| Legal matters | floor | Disputes with partners | 0 | 0 | |
| Legal matters | floor | Non-binding arbitration | 0 | 0 | |
| Legal matters | floor | Binding arbitration | 0 | 0 | |
| Police and arrests | safety | Police and immigration visits | 92 | 92 | |
| Police and arrests | safety | Legal support for members | 0 | 0 | |
| Prevention | fitted | Regular debriefs | 92 | 92 | |
| Prevention | fitted | Conflict workshops | 0 | 0 | |
| Prevention | fitted | Circle processes | 0 | 0 | |
92 of 92 groups have more than seven recommended in this deck. Recommended cards per group run from 8 to 8.
## Communication
| Job | Tier | Card | Recommended | Shown | With fixes |
| --- | --- | --- | --- | --- | --- |
| Day-to-day chat | frame | Signal | 85 | 85 | |
| Day-to-day chat | frame | Slack | 7 | 7 | |
| Day-to-day chat | frame | Matrix / Element | 0 | 0 | |
| Day-to-day chat | frame | Discord | 0 | 0 | |
| Day-to-day chat | frame | WhatsApp | 0 | 0 | |
| Day-to-day chat | frame | Chat channels | 0 | 0 | |
| Day-to-day chat | frame | Field communication | 0 | 0 | |
| Day-to-day chat | frame | Telegram | 0 | 0 | |
| Day-to-day chat | frame | Zulip | 0 | 0 | |
| Our records | floor | Shared records (every Rule) | 92 | 92 | |
| Our records | floor | GitHub / GitLab | 0 | 0 | |
| Records about people | safety | Records about people | 90 | 90 | |
| Records about people | safety | No card | 2 | | |
| Reaching every member | floor | Announcements channel (every Rule) | 92 | 92 | |
| Reaching every member | floor | Email distribution list | 0 | 0 | |
| Reaching every member | floor | Phone and text tree | 0 | 0 | |
| How we treat each other | safety | Code of conduct (every Rule) | 92 | 92 | |
| Accounts, keys and security | safety | Accounts and keys (every Rule) | 92 | 92 | |
| Accounts, keys and security | safety | Password manager | 0 | 0 | |
| Accounts, keys and security | safety | Security plan | 0 | 0 | |
| Meetings and discussion | fitted | How we run meetings | 92 | 92 | |
| Meetings and discussion | fitted | Discourse (forum) | 0 | 0 | |
| Meetings and discussion | fitted | In-person meetings | 0 | 0 | |
| Meetings and discussion | fitted | Video meetings | 0 | 0 | |
| Meetings and discussion | fitted | Deciding in threads | 0 | 0 | |
| Meetings and discussion | fitted | Loomio | 0 | 0 | |
0 of 92 groups have more than seven recommended in this deck. Recommended cards per group run from 6 to 7.
## What decided each pick
The 27 Oct ruling on call 1 asks this run to report, job by job, how many picks row order settled. Only means the job had one eligible card. Score means scale, stage or the answer point. Other kinds means the group's other kinds broke a tie. Row order means the winner tied on all of those and came first in the job.
| Deck | Job | Only | Score | Other kinds | Row order |
| --- | --- | --- | --- | --- | --- |
| Decisions | Everyday decisions | 0 | 0 | 15 | 77 |
| Decisions | Big decisions | 0 | 21 | 33 | 38 |
| Decisions | Changing this Rule | 2 | 0 | 34 | 56 |
| Decisions | Emergencies | 92 | 0 | 0 | 0 |
| Decisions | Money | 1 | 1 | 18 | 72 |
| Decisions | Spending | 75 | 0 | 0 | 0 |
| Decisions | Roles and delegation | 0 | 0 | 29 | 63 |
| Membership | Joining | 2 | 0 | 0 | 90 |
| Membership | Levels of membership | 18 | 5 | 3 | 66 |
| Membership | Learning how we work | 80 | 0 | 0 | 12 |
| Membership | Staying and leaving | 48 | 0 | 5 | 39 |
| Membership | Removal | 92 | 0 | 0 | 0 |
| Conflict | First steps | 92 | 0 | 0 | 0 |
| Conflict | Safety first | 49 | 0 | 0 | 43 |
| Conflict | Who hears serious cases | 80 | 0 | 0 | 12 |
| Conflict | Repair and removal | 2 | 0 | 34 | 56 |
| Conflict | Outside help and legal duties | 39 | 34 | 10 | 9 |
| Conflict | Legal matters | 37 | 0 | 33 | 22 |
| Conflict | Police and arrests | 49 | 0 | 0 | 43 |
| Conflict | Prevention | 2 | 0 | 34 | 56 |
| Communication | Day-to-day chat | 7 | 0 | 0 | 85 |
| Communication | Our records | 91 | 0 | 0 | 1 |
| Communication | Records about people | 90 | 0 | 0 | 0 |
| Communication | Reaching every member | 2 | 0 | 34 | 56 |
| Communication | How we treat each other | 92 | 0 | 0 | 0 |
| Communication | Accounts, keys and security | 0 | 0 | 0 | 92 |
| Communication | Meetings and discussion | 2 | 0 | 34 | 56 |
| All | All jobs | 1044 | 61 | 316 | 1044 |
Of 2465 picks for the 92 groups, row order settled 1044, other kinds 316 and fit 61, and 1044 had one eligible card.
## A whole Rule
A recommended Rule runs 26 to 31 cards, 28.4 on average, and the four sevens show 25 to 28, 26.5 on average.
The 6 one-member founders get 16 to 22 cards. Their picks are at the end of profiles-picks.md.
+17 -17
View File
@@ -30,14 +30,14 @@ describe("Create flow communication-methods page", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
const signalCards = screen.getAllByRole("button", { const signalCards = screen.getAllByRole("button", {
name: /Signal: Encrypted messaging/, name: /Signal: We coordinate in encrypted Signal groups/,
}); });
await user.click(signalCards[0]); await user.click(signalCards[0]);
const dialog = screen.getByRole("dialog"); const dialog = screen.getByRole("dialog");
expect(dialog).toBeInTheDocument(); expect(dialog).toBeInTheDocument();
expect(within(dialog).getByText("Signal")).toBeInTheDocument(); expect(within(dialog).getByText("Signal")).toBeInTheDocument();
expect(within(dialog).getByText("Add Platform")).toBeInTheDocument(); expect(within(dialog).getByText("Add card")).toBeInTheDocument();
}); });
test("re-opening a selected method shows Save and footer Remove; Remove stays in the kebab", async () => { test("re-opening a selected method shows Save and footer Remove; Remove stays in the kebab", async () => {
@@ -45,11 +45,11 @@ describe("Create flow communication-methods page", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
const signalCards = screen.getAllByRole("button", { const signalCards = screen.getAllByRole("button", {
name: /Signal: Encrypted messaging/, name: /Signal: We coordinate in encrypted Signal groups/,
}); });
await user.click(signalCards[0]); await user.click(signalCards[0]);
const dialog = screen.getByRole("dialog"); const dialog = screen.getByRole("dialog");
await user.click(within(dialog).getByRole("button", { name: "Add Platform" })); await user.click(within(dialog).getByRole("button", { name: "Add card" }));
await user.click(signalCards[0]); await user.click(signalCards[0]);
const dialogAgain = screen.getByRole("dialog"); const dialogAgain = screen.getByRole("dialog");
@@ -57,7 +57,7 @@ describe("Create flow communication-methods page", () => {
within(dialogAgain).getByRole("button", { name: "Remove" }), within(dialogAgain).getByRole("button", { name: "Remove" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(dialogAgain).queryByRole("button", { name: "Add Platform" }), within(dialogAgain).queryByRole("button", { name: "Add card" }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect( expect(
within(dialogAgain).getByRole("button", { name: "Save" }), within(dialogAgain).getByRole("button", { name: "Save" }),
@@ -72,12 +72,12 @@ describe("Create flow communication-methods page", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
const signalCards = screen.getAllByRole("button", { const signalCards = screen.getAllByRole("button", {
name: /Signal: Encrypted messaging/, name: /Signal: We coordinate in encrypted Signal groups/,
}); });
await user.click(signalCards[0]); await user.click(signalCards[0]);
await user.click( await user.click(
within(screen.getByRole("dialog")).getByRole("button", { within(screen.getByRole("dialog")).getByRole("button", {
name: "Add Platform", name: "Add card",
}), }),
); );
@@ -96,12 +96,12 @@ describe("Create flow communication-methods page", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
const signalCards = screen.getAllByRole("button", { const signalCards = screen.getAllByRole("button", {
name: /Signal: Encrypted messaging/, name: /Signal: We coordinate in encrypted Signal groups/,
}); });
await user.click(signalCards[0]); await user.click(signalCards[0]);
await user.click( await user.click(
within(screen.getByRole("dialog")).getByRole("button", { within(screen.getByRole("dialog")).getByRole("button", {
name: "Add Platform", name: "Add card",
}), }),
); );
@@ -123,7 +123,7 @@ describe("Create flow communication-methods page", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
const signalCards = screen.getAllByRole("button", { const signalCards = screen.getAllByRole("button", {
name: /Signal: Encrypted messaging/, name: /Signal: We coordinate in encrypted Signal groups/,
}); });
await user.click(signalCards[0]); await user.click(signalCards[0]);
const dialog = screen.getByRole("dialog"); const dialog = screen.getByRole("dialog");
@@ -138,7 +138,7 @@ describe("Create flow communication-methods page", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
const signalCards = screen.getAllByRole("button", { const signalCards = screen.getAllByRole("button", {
name: /Signal: Encrypted messaging/, name: /Signal: We coordinate in encrypted Signal groups/,
}); });
await user.click(signalCards[0]); await user.click(signalCards[0]);
@@ -182,7 +182,7 @@ describe("Create flow communication-methods page", () => {
expect( expect(
screen.getByText( screen.getByText(
"How should this community communicate with each-other?", "How does your group talk, meet and keep its records?",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -191,11 +191,11 @@ describe("Create flow communication-methods page", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
expect( expect(
screen.getByText(/You can select multiple methods for different needs or/), screen.getByText(/Pick a card for each part of how your group communicates/),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("button", { name: /^add$/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /^add$/i })).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "See all communication approaches" }), screen.getByRole("button", { name: "See all communication cards" }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -230,7 +230,7 @@ describe("Create flow communication-methods page", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
const toggle = screen.getByRole("button", { const toggle = screen.getByRole("button", {
name: "See all communication approaches", name: "See all communication cards",
}); });
await user.click(toggle); await user.click(toggle);
@@ -239,7 +239,7 @@ describe("Create flow communication-methods page", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
"How should this community communicate with each-other?", "How does your group talk, meet and keep its records?",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
@@ -419,7 +419,7 @@ describe("Create flow communication-methods page", () => {
const dupId = dupIds[0]; const dupId = dupIds[0];
expect(latest.selectedCommunicationMethodIds).toEqual([CUSTOM_POLICY_ID]); expect(latest.selectedCommunicationMethodIds).toEqual([CUSTOM_POLICY_ID]);
expect( expect(
within(dialog).getByRole("button", { name: "Add Platform" }), within(dialog).getByRole("button", { name: "Add card" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(metaAfterDup[dupId].label.endsWith(" (copy)")).toBe(true); expect(metaAfterDup[dupId].label.endsWith(" (copy)")).toBe(true);
expect(within(dialog).getByRole("heading", { level: 1 })).toHaveTextContent( expect(within(dialog).getByRole("heading", { level: 1 })).toHaveTextContent(
+84 -84
View File
@@ -31,7 +31,7 @@ describe("Create flow decision-approaches page", () => {
expect( expect(
screen.getByRole("heading", { screen.getByRole("heading", {
name: "How should this community make difficult decisions?", name: "How does your group decide, for small things and for big ones?",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -46,8 +46,8 @@ describe("Create flow decision-approaches page", () => {
const description = addControl.parentElement; const description = addControl.parentElement;
expect(description).not.toBeNull(); expect(description).not.toBeNull();
expect(description?.textContent).toMatch(/Select as many as you need/); expect(description?.textContent).toMatch(/Pick a card for each kind of decision/);
expect(description?.textContent).toMatch(/new decision making approaches/); expect(description?.textContent).toMatch(/your own/);
}); });
test("with a finalized custom approach, sidebar still shows add (not Remove policy)", () => { test("with a finalized custom approach, sidebar still shows add (not Remove policy)", () => {
@@ -71,41 +71,41 @@ describe("Create flow decision-approaches page", () => {
const addControl = screen.getByRole("button", { name: /^Add$/i }); const addControl = screen.getByRole("button", { name: /^Add$/i });
expect(addControl).toBeInTheDocument(); expect(addControl).toBeInTheDocument();
const description = addControl.parentElement; const description = addControl.parentElement;
expect(description?.textContent).toMatch(/Select as many as you need/); expect(description?.textContent).toMatch(/Pick a card for each kind of decision/);
expect(description?.textContent).toMatch(/new decision making approaches/); expect(description?.textContent).toMatch(/your own/);
}); });
test("renders message box with title and checkboxes", () => { test("renders message box with title and checkboxes", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
const region = screen.getByRole("region", { const region = screen.getByRole("region", {
name: "Consider defining approaches to steward key resources:", name: "Make sure your Rule covers each of these",
}); });
expect(region).toBeInTheDocument(); expect(region).toBeInTheDocument();
expect( expect(
within(region).getByRole("checkbox", { within(region).getByRole("checkbox", {
name: "Amend your CommunityRule", name: "Everyday decisions",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(region).getByRole("checkbox", { name: "Steward finances" }), within(region).getByRole("checkbox", { name: "Shared money" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(region).getByRole("checkbox", { name: "Project level decisions" }), within(region).getByRole("checkbox", { name: "Spending" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(region).getByRole("checkbox", { within(region).getByRole("checkbox", {
name: "Discipline and member termination", name: "Roles and working groups",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
test("renders card stack with See all decision approaches toggle", () => { test("renders card stack with See all decision cards toggle", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
expect( expect(
screen.getByRole("button", { name: "See all decision approaches" }), screen.getByRole("button", { name: "See all decision cards" }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -114,17 +114,17 @@ describe("Create flow decision-approaches page", () => {
expect( expect(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { screen.getByRole("button", {
name: /Do-ocracy: Decisions are made by those who take initiative/, name: /Do-ocracy: Whoever does the work/,
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { screen.getByRole("button", {
name: /Consensus Decision-Making: All members must agree/, name: /Decision speeds: We sort each decision/,
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -134,7 +134,7 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
const toggle = screen.getByRole("button", { const toggle = screen.getByRole("button", {
name: "See all decision approaches", name: "See all decision cards",
}); });
await user.click(toggle); await user.click(toggle);
@@ -149,16 +149,16 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
expect( expect(
screen.queryByRole("button", { name: /^Sociocracy:/ }), screen.queryByRole("button", { name: /^Consent \(sociocracy\):/ }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
const toggle = screen.getByRole("button", { const toggle = screen.getByRole("button", {
name: "See all decision approaches", name: "See all decision cards",
}); });
await user.click(toggle); await user.click(toggle);
expect( expect(
screen.getByRole("button", { name: /^Sociocracy:/ }), screen.getByRole("button", { name: /^Consent \(sociocracy\):/ }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -167,7 +167,7 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
await user.click( await user.click(
screen.getByRole("button", { name: "See all decision approaches" }), screen.getByRole("button", { name: "See all decision cards" }),
); );
const cardLabels = () => const cardLabels = () =>
@@ -177,15 +177,15 @@ describe("Create flow decision-approaches page", () => {
.filter((label) => label.includes(": ")); .filter((label) => label.includes(": "));
const labelsBefore = cardLabels(); const labelsBefore = cardLabels();
expect(labelsBefore[0]).toMatch(/^Lazy Consensus:/); expect(labelsBefore[0]).toMatch(/^Decision speeds:/);
expect(labelsBefore.some((label) => label.startsWith("Sociocracy:"))).toBe( expect(labelsBefore.some((label) => label.startsWith("Consent (sociocracy):"))).toBe(
true, true,
); );
await user.click(screen.getByRole("button", { name: /^Sociocracy:/ })); await user.click(screen.getByRole("button", { name: /^Consent \(sociocracy\):/ }));
await user.click( await user.click(
within(await screen.findByRole("dialog")).getByRole("button", { within(await screen.findByRole("dialog")).getByRole("button", {
name: "Add Approach", name: "Add card",
}), }),
); );
await waitFor(() => { await waitFor(() => {
@@ -197,7 +197,7 @@ describe("Create flow decision-approaches page", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
expect(cardLabels()).toEqual(labelsBefore); expect(cardLabels()).toEqual(labelsBefore);
expect( expect(
screen.getByRole("button", { name: /^Sociocracy:/ }), screen.getByRole("button", { name: /^Consent \(sociocracy\):/ }),
).toHaveTextContent("SELECTED"); ).toHaveTextContent("SELECTED");
}); });
@@ -207,7 +207,7 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
@@ -229,7 +229,7 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
const card = screen.getByRole("button", { const card = screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}); });
await user.click(card); await user.click(card);
@@ -237,7 +237,7 @@ describe("Create flow decision-approaches page", () => {
expect(dialog).toBeInTheDocument(); expect(dialog).toBeInTheDocument();
const confirmButton = within(dialog).getByRole("button", { const confirmButton = within(dialog).getByRole("button", {
name: "Add Approach", name: "Add card",
}); });
await user.click(confirmButton); await user.click(confirmButton);
@@ -249,13 +249,13 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
const card = screen.getByRole("button", { const card = screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}); });
await user.click(card); await user.click(card);
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
await user.click( await user.click(
within(dialog).getByRole("button", { within(dialog).getByRole("button", {
name: "Add Approach", name: "Add card",
}), }),
); );
@@ -265,7 +265,7 @@ describe("Create flow decision-approaches page", () => {
within(dialogAgain).getByRole("button", { name: "Remove" }), within(dialogAgain).getByRole("button", { name: "Remove" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(dialogAgain).queryByRole("button", { name: "Add Approach" }), within(dialogAgain).queryByRole("button", { name: "Add card" }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect( expect(
within(dialogAgain).getByRole("button", { name: "Save" }), within(dialogAgain).getByRole("button", { name: "Save" }),
@@ -280,12 +280,12 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
const card = screen.getByRole("button", { const card = screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}); });
await user.click(card); await user.click(card);
await user.click( await user.click(
within(await screen.findByRole("dialog")).getByRole("button", { within(await screen.findByRole("dialog")).getByRole("button", {
name: "Add Approach", name: "Add card",
}), }),
); );
@@ -304,12 +304,12 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
const card = screen.getByRole("button", { const card = screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}); });
await user.click(card); await user.click(card);
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
await user.click( await user.click(
within(dialog).getByRole("button", { name: "Add Approach" }), within(dialog).getByRole("button", { name: "Add card" }),
); );
await user.click(card); await user.click(card);
@@ -331,12 +331,12 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
const card = screen.getByRole("button", { const card = screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}); });
await user.click(card); await user.click(card);
await user.click( await user.click(
within(await screen.findByRole("dialog")).getByRole("button", { within(await screen.findByRole("dialog")).getByRole("button", {
name: "Add Approach", name: "Add card",
}), }),
); );
@@ -363,7 +363,7 @@ describe("Create flow decision-approaches page", () => {
); );
const card = screen.getByRole("button", { const card = screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}); });
await user.click(card); await user.click(card);
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
@@ -380,7 +380,7 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
const amendCheckbox = screen.getByRole("checkbox", { const amendCheckbox = screen.getByRole("checkbox", {
name: "Amend your CommunityRule", name: "Changing this Rule",
}); });
expect(amendCheckbox).not.toBeChecked(); expect(amendCheckbox).not.toBeChecked();
await user.click(amendCheckbox); await user.click(amendCheckbox);
@@ -393,26 +393,26 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect( expect(
within(dialog).getByRole("button", { within(dialog).getByRole("button", {
name: "Select Amend your CommunityRule", name: "Select Changing this Rule",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(dialog).getByRole("button", { name: "Select Steward finances" }), within(dialog).getByRole("button", { name: "Select Shared money" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(dialog).getByRole("button", { within(dialog).getByRole("button", {
name: "Select Project level decisions", name: "Select Big decisions",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(dialog).getByRole("button", { within(dialog).getByRole("button", {
name: "Select Discipline and member termination", name: "Select Spending",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -422,18 +422,18 @@ describe("Create flow decision-approaches page", () => {
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);
await user.click( await user.click(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
); );
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const lazyDialog = await screen.findByRole("dialog"); const lazyDialog = await screen.findByRole("dialog");
expect( expect(
within(lazyDialog).getByRole("button", { within(lazyDialog).getByRole("button", {
name: "Select Steward finances", name: "Select Shared money",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -444,20 +444,20 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const lazyDialog = await screen.findByRole("dialog"); const lazyDialog = await screen.findByRole("dialog");
await user.click( await user.click(
within(lazyDialog).getByRole("button", { within(lazyDialog).getByRole("button", {
name: "Select Steward finances", name: "Select Shared money",
}), }),
); );
expect( expect(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
).toBeChecked(); ).toBeChecked();
await user.click( await user.click(
within(lazyDialog).getByRole("button", { name: "Add Approach" }), within(lazyDialog).getByRole("button", { name: "Add card" }),
); );
await waitFor(() => { await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
@@ -465,22 +465,22 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Do-ocracy: Decisions are made by those who take initiative/, name: /Do-ocracy: Whoever does the work/,
}), }),
); );
const doocracyDialog = await screen.findByRole("dialog"); const doocracyDialog = await screen.findByRole("dialog");
expect( expect(
within(doocracyDialog).getByRole("button", { within(doocracyDialog).getByRole("button", {
name: "Select Steward finances", name: "Select Shared money",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(doocracyDialog).queryByRole("button", { within(doocracyDialog).queryByRole("button", {
name: "Deselect Steward finances", name: "Deselect Shared money",
}), }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect( expect(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
).not.toBeChecked(); ).not.toBeChecked();
await user.click( await user.click(
within(doocracyDialog).getByRole("button", { name: "Close" }), within(doocracyDialog).getByRole("button", { name: "Close" }),
@@ -490,22 +490,22 @@ describe("Create flow decision-approaches page", () => {
}); });
expect( expect(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
).toBeChecked(); ).toBeChecked();
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const lazyAgain = await screen.findByRole("dialog"); const lazyAgain = await screen.findByRole("dialog");
expect( expect(
within(lazyAgain).getByRole("button", { within(lazyAgain).getByRole("button", {
name: "Deselect Steward finances", name: "Deselect Shared money",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
).toBeChecked(); ).toBeChecked();
}); });
@@ -515,12 +515,12 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const lazyDialog = await screen.findByRole("dialog"); const lazyDialog = await screen.findByRole("dialog");
await user.click( await user.click(
within(lazyDialog).getByRole("button", { name: "Add Approach" }), within(lazyDialog).getByRole("button", { name: "Add card" }),
); );
await waitFor(() => { await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
@@ -528,16 +528,16 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Do-ocracy: Decisions are made by those who take initiative/, name: /Do-ocracy: Whoever does the work/,
}), }),
); );
const doocracyDialog = await screen.findByRole("dialog"); const doocracyDialog = await screen.findByRole("dialog");
await user.click( await user.click(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
); );
expect( expect(
within(doocracyDialog).getByRole("button", { within(doocracyDialog).getByRole("button", {
name: "Deselect Steward finances", name: "Deselect Shared money",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
await user.click( await user.click(
@@ -550,13 +550,13 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const lazyAgain = await screen.findByRole("dialog"); const lazyAgain = await screen.findByRole("dialog");
expect( expect(
within(lazyAgain).getByRole("button", { within(lazyAgain).getByRole("button", {
name: "Select Steward finances", name: "Select Shared money",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -567,20 +567,20 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const lazyDialog = await screen.findByRole("dialog"); const lazyDialog = await screen.findByRole("dialog");
await user.click( await user.click(
within(lazyDialog).getByRole("button", { within(lazyDialog).getByRole("button", {
name: "Select Steward finances", name: "Select Shared money",
}), }),
); );
expect( expect(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
).toBeChecked(); ).toBeChecked();
await user.click( await user.click(
within(lazyDialog).getByRole("button", { name: "Add Approach" }), within(lazyDialog).getByRole("button", { name: "Add card" }),
); );
await waitFor(() => { await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
@@ -588,54 +588,54 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Do-ocracy: Decisions are made by those who take initiative/, name: /Do-ocracy: Whoever does the work/,
}), }),
); );
const doocracyDialog = await screen.findByRole("dialog"); const doocracyDialog = await screen.findByRole("dialog");
await user.click( await user.click(
within(doocracyDialog).getByRole("button", { within(doocracyDialog).getByRole("button", {
name: "Select Project level decisions", name: "Select Big decisions",
}), }),
); );
expect( expect(
screen.getByRole("checkbox", { name: "Project level decisions" }), screen.getByRole("checkbox", { name: "Big decisions" }),
).toBeChecked(); ).toBeChecked();
expect( expect(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
).not.toBeChecked(); ).not.toBeChecked();
expect( expect(
within(doocracyDialog).getByRole("button", { within(doocracyDialog).getByRole("button", {
name: "Select Steward finances", name: "Select Shared money",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
await user.click( await user.click(
within(doocracyDialog).getByRole("button", { name: "Add Approach" }), within(doocracyDialog).getByRole("button", { name: "Add card" }),
); );
await waitFor(() => { await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
}); });
expect( expect(
screen.getByRole("checkbox", { name: "Steward finances" }), screen.getByRole("checkbox", { name: "Shared money" }),
).toBeChecked(); ).toBeChecked();
expect( expect(
screen.getByRole("checkbox", { name: "Project level decisions" }), screen.getByRole("checkbox", { name: "Big decisions" }),
).toBeChecked(); ).toBeChecked();
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/, name: /Lazy consensus: A proposal passes/,
}), }),
); );
const lazyAgain = await screen.findByRole("dialog"); const lazyAgain = await screen.findByRole("dialog");
expect( expect(
within(lazyAgain).getByRole("button", { within(lazyAgain).getByRole("button", {
name: "Deselect Steward finances", name: "Deselect Shared money",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(lazyAgain).getByRole("button", { within(lazyAgain).getByRole("button", {
name: "Select Project level decisions", name: "Select Big decisions",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
await user.click( await user.click(
@@ -647,18 +647,18 @@ describe("Create flow decision-approaches page", () => {
await user.click( await user.click(
screen.getByRole("button", { screen.getByRole("button", {
name: /Do-ocracy: Decisions are made by those who take initiative/, name: /Do-ocracy: Whoever does the work/,
}), }),
); );
const doocracyAgain = await screen.findByRole("dialog"); const doocracyAgain = await screen.findByRole("dialog");
expect( expect(
within(doocracyAgain).getByRole("button", { within(doocracyAgain).getByRole("button", {
name: "Deselect Project level decisions", name: "Deselect Big decisions",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
within(doocracyAgain).getByRole("button", { within(doocracyAgain).getByRole("button", {
name: "Select Steward finances", name: "Select Shared money",
}), }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
+3 -3
View File
@@ -84,7 +84,7 @@ describe("Page Flow Integration", () => {
screen.getByRole("heading", { name: "Do-ocracy" }), screen.getByRole("heading", { name: "Do-ocracy" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("heading", { name: "Quadratic Governance" }), screen.getByRole("heading", { name: "Solidarity Network" }),
).toBeInTheDocument(); ).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
@@ -155,10 +155,10 @@ describe("Page Flow Integration", () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Devolution")).toBeInTheDocument(); expect(screen.getByText("Devolution")).toBeInTheDocument();
}); });
expect(screen.queryByText("Solidarity Network")).not.toBeInTheDocument(); expect(screen.queryByText("Quadratic Governance")).not.toBeInTheDocument();
expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0); expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0);
expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0); expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0);
expect(screen.getByText("Quadratic Governance")).toBeInTheDocument(); expect(screen.getByText("Solidarity Network")).toBeInTheDocument();
const seeAll = screen.getByRole("link", { name: "See all templates" }); const seeAll = screen.getByRole("link", { name: "See all templates" });
expect(seeAll).toHaveAttribute("href", "/templates"); expect(seeAll).toHaveAttribute("href", "/templates");
+1 -1
View File
@@ -60,7 +60,7 @@ describe("User Journey Integration", () => {
}); });
expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0); expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0);
expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0); expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0);
expect(screen.getByText("Quadratic Governance")).toBeInTheDocument(); expect(screen.getByText("Solidarity Network")).toBeInTheDocument();
const seeHowLinks = screen.getAllByRole("link", { const seeHowLinks = screen.getAllByRole("link", {
name: "See how it works", name: "See how it works",
+2 -4
View File
@@ -105,9 +105,7 @@ describe("RuleStack Component", () => {
expect(GOVERNANCE_TEMPLATE_CATALOG.length).toBeGreaterThan( expect(GOVERNANCE_TEMPLATE_CATALOG.length).toBeGreaterThan(
homeFeatured.length, homeFeatured.length,
); );
expect( expect(screen.queryByText("Quadratic Governance")).not.toBeInTheDocument();
screen.queryByText("Solidarity Network"),
).not.toBeInTheDocument();
}); });
test("renders with custom className", async () => { test("renders with custom className", async () => {
@@ -139,7 +137,7 @@ describe("RuleStack Component", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
/Voting cost is squared \(V²\), preventing a majority from steamrolling a passionate minority\./, /Power is held by autonomous "cells\." A central hub acts as a switchboard for resources but cannot dictate cell activities\./,
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
+7 -9
View File
@@ -7,14 +7,12 @@ import { methodSectionsPinsForHydratedSelections } from "../../lib/create/publis
import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json"; import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json";
function coreValuePresetId(label: string): string { function coreValuePresetId(label: string): string {
const values = coreValuesMessages.values as Array< const values = coreValuesMessages.values as Array<{ id: string; label: string }>;
string | { label: string } const row = values.find(
>; (value) => value.label.toLowerCase() === label.toLowerCase(),
const idx = values.findIndex((v) => { );
const l = typeof v === "string" ? v : v.label; if (!row) throw new Error(`No core value named ${label}`);
return l.toLowerCase() === label.toLowerCase(); return row.id;
});
return String(idx + 1);
} }
describe("buildTemplateCustomizePrefill", () => { describe("buildTemplateCustomizePrefill", () => {
@@ -76,7 +74,7 @@ describe("buildTemplateCustomizePrefill", () => {
const snapshot = prefill.coreValuesChipsSnapshot ?? []; const snapshot = prefill.coreValuesChipsSnapshot ?? [];
const selectedRows = snapshot.filter((r) => r.state === "selected"); const selectedRows = snapshot.filter((r) => r.state === "selected");
expect(selectedRows.map((r) => r.label).sort()).toEqual([ expect(selectedRows.map((r) => r.label).sort()).toEqual([
"Community Care", "Community care",
"Consensus", "Consensus",
]); ]);
// Unmatched presets should still appear, as unselected, so the screen // Unmatched presets should still appear, as unselected, so the screen
@@ -20,19 +20,19 @@ describe("buildFinalReviewCategoriesFromState", () => {
// Same shape `buildTemplateCustomizePrefill` emits via methodSlugFromTitle. // Same shape `buildTemplateCustomizePrefill` emits via methodSlugFromTitle.
const state: CreateFlowState = { const state: CreateFlowState = {
selectedCommunicationMethodIds: ["signal", "in-person-meetings"], selectedCommunicationMethodIds: ["signal", "in-person-meetings"],
selectedMembershipMethodIds: ["open-access"], selectedMembershipMethodIds: ["open-admission"],
selectedDecisionApproachIds: ["lazy-consensus"], selectedDecisionApproachIds: ["lazy-consensus"],
selectedConflictManagementIds: ["peer-mediation"], selectedConflictManagementIds: ["conflict-ladder"],
}; };
const rows = buildFinalReviewCategoriesFromState(state, NAMES); const rows = buildFinalReviewCategoriesFromState(state, NAMES);
const byName = new Map(rows.map((r) => [r.name, r.chips])); const byName = new Map(rows.map((r) => [r.name, r.chips]));
expect(byName.get("Communication")).toEqual([ expect(byName.get("Communication")).toEqual([
"Signal", "Signal",
"In-Person Meetings", "In-person meetings",
]); ]);
expect(byName.get("Membership")).toEqual(["Open Access"]); expect(byName.get("Membership")).toEqual(["Open admission"]);
expect(byName.get("Decision-making")).toEqual(["Lazy Consensus"]); expect(byName.get("Decision-making")).toEqual(["Lazy consensus"]);
expect(byName.get("Conflict management")).toEqual(["Peer Mediation"]); expect(byName.get("Conflict management")).toEqual(["Conflict ladder"]);
expect(byName.has("Values")).toBe(false); expect(byName.has("Values")).toBe(false);
}); });
@@ -116,7 +116,7 @@ describe("buildFinalReviewCategoriesFromState", () => {
}; };
const rows = buildFinalReviewCategoriesFromState(state, NAMES); const rows = buildFinalReviewCategoriesFromState(state, NAMES);
expect(rows).toEqual([ expect(rows).toEqual([
{ name: "Communication", chips: ["In-Person Meetings"] }, { name: "Communication", chips: ["In-person meetings"] },
]); ]);
}); });
+6 -3
View File
@@ -41,6 +41,7 @@ describe("buildPublishPayload", () => {
}, },
], ],
coreValues: [], coreValues: [],
supermajorityShare: 75,
}); });
}); });
@@ -61,6 +62,7 @@ describe("buildPublishPayload", () => {
}, },
], ],
coreValues: [], coreValues: [],
supermajorityShare: 75,
}); });
}); });
@@ -85,7 +87,7 @@ describe("buildPublishPayload", () => {
const r = buildPublishPayload({ title: "T", sections }); const r = buildPublishPayload({ title: "T", sections });
expect(r.ok).toBe(true); expect(r.ok).toBe(true);
if (!r.ok) return; if (!r.ok) return;
expect(r.document).toEqual({ sections, coreValues: [] }); expect(r.document).toEqual({ sections, coreValues: [], supermajorityShare: 75 });
}); });
it("filters invalid section entries from state.sections", () => { it("filters invalid section entries from state.sections", () => {
@@ -101,6 +103,7 @@ describe("buildPublishPayload", () => {
expect(r.document).toEqual({ expect(r.document).toEqual({
sections: [{ categoryName: "Values", entries: [{ title: "A", body: "B" }] }], sections: [{ categoryName: "Values", entries: [{ title: "A", body: "B" }] }],
coreValues: [], coreValues: [],
supermajorityShare: 75,
}); });
}); });
@@ -331,9 +334,9 @@ describe("buildPublishPayload — methodSelections", () => {
const r = buildPublishPayload({ const r = buildPublishPayload({
title: "T", title: "T",
selectedCommunicationMethodIds: ["signal"], selectedCommunicationMethodIds: ["signal"],
selectedMembershipMethodIds: ["open-access"], selectedMembershipMethodIds: ["open-admission"],
selectedDecisionApproachIds: ["lazy-consensus"], selectedDecisionApproachIds: ["lazy-consensus"],
selectedConflictManagementIds: ["peer-mediation"], selectedConflictManagementIds: ["conflict-ladder"],
}); });
expect(r.ok).toBe(true); expect(r.ok).toBe(true);
if (!r.ok) return; if (!r.ok) return;
@@ -4,9 +4,9 @@ import type { CommunityStructureChipSnapshotRow } from "../../app/(app)/create/t
import { buildCoreValueChipOptionsFromDraft } from "../../lib/create/coreValueChipOptionsFromDraft"; import { buildCoreValueChipOptionsFromDraft } from "../../lib/create/coreValueChipOptionsFromDraft";
const PRESETS = [ const PRESETS = [
{ label: "A", meaning: "am", signals: "as" }, { id: "1", label: "A", meaning: "am", signals: "as" },
{ label: "B", meaning: "bm", signals: "bs" }, { id: "2", label: "B", meaning: "bm", signals: "bs" },
{ label: "C", meaning: "cm", signals: "cs" }, { id: "3", label: "C", meaning: "cm", signals: "cs" },
] as const; ] as const;
describe("buildCoreValueChipOptionsFromDraft", () => { describe("buildCoreValueChipOptionsFromDraft", () => {
+8 -9
View File
@@ -100,27 +100,26 @@ describe("GET /api/create-flow/methods", () => {
}; };
expect(json.section).toBe("communication"); expect(json.section).toBe("communication");
expect(json.methods.length).toBe(communicationCatalog.length); expect(json.methods.length).toBe(communicationCatalog.length);
expect(json.methods[0].slug).toBe("loomio"); expect(json.methods[0].matches.score).toBeGreaterThan(json.methods[1].matches.score);
expect(json.methods[0].matches.score).toBe(2); expect(findManyMock).not.toHaveBeenCalled();
expect(json.methods[1].slug).toBe("in-person-meetings");
expect(json.methods[1].matches.score).toBe(1);
expect(json.methods[0].label).toBeTruthy(); expect(json.methods[0].label).toBeTruthy();
}); });
it("returns full catalog without matches when the DB query throws", async () => { it("ranks from the deck files when the database is down", async () => {
findManyMock.mockRejectedValueOnce(new Error("db down")); findManyMock.mockRejectedValueOnce(new Error("db down"));
const res = await GET( const res = await GET(
makeReq( makeReq(
"https://x.test/api/create-flow/methods?section=communication&facet.size=oneMember", "https://x.test/api/create-flow/methods?section=communication&facet.size=oneMember&facet.orgType=workersCoop",
), ),
undefined, undefined,
); );
expect(res.status).toBe(200); expect(res.status).toBe(200);
const json = (await res.json()) as { const json = (await res.json()) as {
methods: Array<{ slug: string; matches?: unknown }>; methods: Array<{ slug: string; matches?: { score: number } }>;
}; };
expect(json.methods.length).toBe(communicationCatalog.length); expect(json.methods.length).toBe(communicationCatalog.length);
expect(json.methods[0].matches).toBeUndefined(); expect(json.methods[0].matches?.score).toBeGreaterThan(0);
expect(findManyMock).not.toHaveBeenCalled();
}); });
it("returns all core values for section=coreValues", async () => { it("returns all core values for section=coreValues", async () => {
@@ -137,7 +136,7 @@ describe("GET /api/create-flow/methods", () => {
}; };
expect(json.section).toBe("coreValues"); expect(json.section).toBe("coreValues");
expect(json.methods.length).toBeGreaterThan(50); expect(json.methods.length).toBeGreaterThan(50);
expect(json.methods[0].id).toBe("1"); expect(json.methods[0].id).toBe("accessibility");
expect(json.methods[0].label).toBeTruthy(); expect(json.methods[0].label).toBeTruthy();
expect(findManyMock).not.toHaveBeenCalled(); expect(findManyMock).not.toHaveBeenCalled();
}); });
+40 -34
View File
@@ -23,39 +23,42 @@ const emptyEntry = (): DecisionApproachDetailEntry => ({
}); });
describe("decisionApproachKeyResources", () => { describe("decisionApproachKeyResources", () => {
it("reads the four key-resource items from messages", () => { it("reads the decision checklist from messages", () => {
expect(decisionApproachKeyResourceItems()).toEqual([ expect(decisionApproachKeyResourceItems()).toEqual([
{ id: "amend", label: "Amend your CommunityRule" }, { id: "everyday", label: "Everyday decisions" },
{ id: "finances", label: "Steward finances" }, { id: "big", label: "Big decisions" },
{ id: "project", label: "Project level decisions" }, { id: "amend", label: "Changing this Rule" },
{ id: "discipline", label: "Discipline and member termination" }, { id: "emergency", label: "Emergencies" },
{ id: "finances", label: "Shared money" },
{ id: "spending", label: "Spending" },
{ id: "roles", label: "Roles and working groups" },
]); ]);
}); });
it("selects checked key-resource labels without rewriting applicableScope", () => { it("selects checked key-resource labels without rewriting applicableScope", () => {
const next = applyDecisionApproachKeyResources(emptyEntry(), [ const next = applyDecisionApproachKeyResources(emptyEntry(), [
"Steward finances", "Shared money",
]); ]);
expect(next.applicableScope).toEqual(["Daily Operations"]); expect(next.applicableScope).toEqual(["Daily Operations"]);
expect(next.selectedApplicableScope).toEqual(["Steward finances"]); expect(next.selectedApplicableScope).toEqual(["Shared money"]);
}); });
it("keeps non-key chip selections when syncing sidebar checks", () => { it("keeps non-key chip selections when syncing sidebar checks", () => {
const next = applyDecisionApproachKeyResources( const next = applyDecisionApproachKeyResources(
{ {
...emptyEntry(), ...emptyEntry(),
applicableScope: ["Steward finances", "Daily Operations"], applicableScope: ["Shared money", "Daily Operations"],
selectedApplicableScope: ["Daily Operations"], selectedApplicableScope: ["Daily Operations"],
}, },
["Steward finances"], ["Shared money"],
); );
expect(next.applicableScope).toEqual([ expect(next.applicableScope).toEqual([
"Steward finances", "Shared money",
"Daily Operations", "Daily Operations",
]); ]);
expect(next.selectedApplicableScope).toEqual([ expect(next.selectedApplicableScope).toEqual([
"Daily Operations", "Daily Operations",
"Steward finances", "Shared money",
]); ]);
}); });
@@ -63,19 +66,19 @@ describe("decisionApproachKeyResources", () => {
expect( expect(
decisionApproachScopeForPublish( decisionApproachScopeForPublish(
["Daily Operations", "Minor Expenditures"], ["Daily Operations", "Minor Expenditures"],
["Steward finances"], ["Shared money"],
), ),
).toEqual([ ).toEqual([
"Daily Operations", "Daily Operations",
"Minor Expenditures", "Minor Expenditures",
"Steward finances", "Shared money",
]); ]);
expect( expect(
decisionApproachScopeForPublish( decisionApproachScopeForPublish(
["Daily Operations", "Minor Expenditures"], ["Daily Operations", "Minor Expenditures"],
["Daily Operations", "Steward finances"], ["Daily Operations", "Shared money"],
), ),
).toEqual(["Daily Operations", "Steward finances"]); ).toEqual(["Daily Operations", "Shared money"]);
expect( expect(
decisionApproachScopeForPublish( decisionApproachScopeForPublish(
["Daily Operations", "Minor Expenditures"], ["Daily Operations", "Minor Expenditures"],
@@ -85,44 +88,44 @@ describe("decisionApproachKeyResources", () => {
}); });
it("maps checkbox ids to labels and back", () => { it("maps checkbox ids to labels and back", () => {
expect(selectedKeyResourceLabelsFromCheckedIds(["amend", "project"])).toEqual( expect(selectedKeyResourceLabelsFromCheckedIds(["amend", "big"])).toEqual(
["Amend your CommunityRule", "Project level decisions"], ["Big decisions", "Changing this Rule"],
); );
expect( expect(
decisionApproachKeyResourceIdsFromLabels([ decisionApproachKeyResourceIdsFromLabels([
"Project level decisions", "Big decisions",
"Unknown", "Unknown",
"Amend your CommunityRule", "Changing this Rule",
]), ]),
).toEqual(["amend", "project"]); ).toEqual(["big", "amend"]);
}); });
it("derives sidebar checks from the open draft or selected approaches", () => { it("derives sidebar checks from the open draft or selected approaches", () => {
const detailsById = { const detailsById = {
"lazy-consensus": { "lazy-consensus": {
...emptyEntry(), ...emptyEntry(),
selectedApplicableScope: ["Steward finances"], selectedApplicableScope: ["Shared money"],
}, },
"do-ocracy": { "do-ocracy": {
...emptyEntry(), ...emptyEntry(),
selectedApplicableScope: ["Project level decisions"], selectedApplicableScope: ["Big decisions"],
}, },
}; };
expect( expect(
decisionApproachKeyResourceCheckboxIds({ decisionApproachKeyResourceCheckboxIds({
detailsById, detailsById,
selectedApproachIds: ["lazy-consensus", "do-ocracy"], selectedApproachIds: ["lazy-consensus", "do-ocracy"],
reminderIds: ["discipline"], reminderIds: ["spending"],
}), }),
).toEqual(["finances", "project", "discipline"]); ).toEqual(["big", "finances", "spending"]);
expect( expect(
decisionApproachKeyResourceCheckboxIds({ decisionApproachKeyResourceCheckboxIds({
detailsById, detailsById,
selectedApproachIds: ["lazy-consensus", "do-ocracy"], selectedApproachIds: ["lazy-consensus", "do-ocracy"],
reminderIds: ["discipline"], reminderIds: ["spending"],
openDraft: { openDraft: {
...emptyEntry(), ...emptyEntry(),
selectedApplicableScope: ["Amend your CommunityRule"], selectedApplicableScope: ["Changing this Rule"],
}, },
}), }),
).toEqual(["amend"]); ).toEqual(["amend"]);
@@ -132,7 +135,7 @@ describe("decisionApproachKeyResources", () => {
const next = syncDecisionApproachKeyResourceDetails( const next = syncDecisionApproachKeyResourceDetails(
{ "lazy-consensus": emptyEntry() }, { "lazy-consensus": emptyEntry() },
["do-ocracy"], ["do-ocracy"],
["Amend your CommunityRule"], ["Changing this Rule"],
(id) => ({ (id) => ({
...emptyEntry(), ...emptyEntry(),
corePrinciple: id, corePrinciple: id,
@@ -140,19 +143,19 @@ describe("decisionApproachKeyResources", () => {
}), }),
); );
expect(next["lazy-consensus"]?.selectedApplicableScope).toEqual([ expect(next["lazy-consensus"]?.selectedApplicableScope).toEqual([
"Amend your CommunityRule", "Changing this Rule",
]); ]);
expect(next["do-ocracy"]?.corePrinciple).toBe("do-ocracy"); expect(next["do-ocracy"]?.corePrinciple).toBe("do-ocracy");
expect(next["do-ocracy"]?.applicableScope).toEqual(["Volunteer Tasks"]); expect(next["do-ocracy"]?.applicableScope).toEqual(["Volunteer Tasks"]);
expect(next["do-ocracy"]?.selectedApplicableScope).toEqual([ expect(next["do-ocracy"]?.selectedApplicableScope).toEqual([
"Amend your CommunityRule", "Changing this Rule",
]); ]);
}); });
it("compares string arrays by order", () => { it("compares string arrays by order", () => {
expect(stringArraysEqual(["a", "b"], ["a", "b"])).toBe(true); expect(stringArraysEqual(["a", "b"], ["a", "b"])).toBe(true);
expect(stringArraysEqual(["a", "b"], ["b", "a"])).toBe(false); expect(stringArraysEqual(["a", "b"], ["b", "a"])).toBe(false);
expect(decisionApproachKeyResourceLabels()).toHaveLength(4); expect(decisionApproachKeyResourceLabels()).toHaveLength(7);
}); });
it("unions key-resource labels onto an existing scope list", () => { it("unions key-resource labels onto an existing scope list", () => {
@@ -160,10 +163,13 @@ describe("decisionApproachKeyResources", () => {
withDecisionApproachKeyResourceScopes(["Daily Operations"]), withDecisionApproachKeyResourceScopes(["Daily Operations"]),
).toEqual([ ).toEqual([
"Daily Operations", "Daily Operations",
"Amend your CommunityRule", "Everyday decisions",
"Steward finances", "Big decisions",
"Project level decisions", "Changing this Rule",
"Discipline and member termination", "Emergencies",
"Shared money",
"Spending",
"Roles and working groups",
]); ]);
}); });
}); });
+46 -77
View File
@@ -1,93 +1,62 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { deriveCompactCards } from "../../app/(app)/create/hooks/useFacetRecommendations"; import { deriveCompactCards } from "../../app/(app)/create/hooks/useFacetRecommendations";
const methods = [
{ id: "alpha" },
{ id: "bravo" },
{ id: "charlie" },
{ id: "delta" },
{ id: "echo" },
{ id: "foxtrot" },
{ id: "golf" },
] as const;
describe("deriveCompactCards", () => { describe("deriveCompactCards", () => {
it("falls back to authoring order with no badges when no facets selected", () => { it("falls back to deck order with no tags when no facets are chosen", () => {
const result = deriveCompactCards(methods, {}, false, 5); const result = deriveCompactCards("decisionApproaches", {}, 5);
expect(result.compactCardIds).toEqual([ expect(result.compactCardIds).toHaveLength(5);
"alpha",
"bravo",
"charlie",
"delta",
"echo",
]);
expect(result.recommendedIds.size).toBe(0); expect(result.recommendedIds.size).toBe(0);
expect(result.orderedIds[0]).toBe(result.compactCardIds[0]);
}); });
it("falls back to authoring order with no badges when facets selected but every score is zero", () => { it("shows the top five in deck order and leaves every-rule cards untagged", () => {
const result = deriveCompactCards(methods, {}, true, 5);
expect(result.compactCardIds).toEqual([
"alpha",
"bravo",
"charlie",
"delta",
"echo",
]);
expect(result.recommendedIds.size).toBe(0);
});
it("shows only recommended (matched) cards when fewer than the limit match", () => {
const result = deriveCompactCards( const result = deriveCompactCards(
methods, "decisionApproaches",
{ bravo: 2, delta: 1 }, {
true, size: "thirteenToOneHundred",
mainKind: "workersCoop",
scale: ["local"],
stage: "established",
traits: { risky: true, sponsor: true, employer: true },
},
5, 5,
); );
// Caller is responsible for pre-ranking by score (rankMethodsByScore).
// This test passes already-ranked input; the hook just respects ordering
// and tags only the matched subset — no padding with unrecommended cards.
expect(result.compactCardIds).toEqual(["bravo", "delta"]);
expect([...result.recommendedIds].sort()).toEqual(["bravo", "delta"]);
});
it("caps recommended cards at the limit when more than `limit` match", () => {
const scores = {
alpha: 4,
bravo: 3,
charlie: 3,
delta: 2,
echo: 1,
foxtrot: 1,
golf: 1,
};
const result = deriveCompactCards(methods, scores, true, 5);
expect(result.compactCardIds).toEqual([ expect(result.compactCardIds).toEqual([
"alpha", "decision-speeds",
"bravo", "modified-consensus",
"charlie", "emergency-decisions",
"delta", "on-site-lead",
"echo", "how-we-are-covered",
]);
expect(result.recommendedIds.size).toBe(5);
expect([...result.recommendedIds].sort()).toEqual([
"alpha",
"bravo",
"charlie",
"delta",
"echo",
]); ]);
expect(result.recommendedIds.has("decision-speeds")).toBe(false);
expect(result.recommendedIds.has("on-site-lead")).toBe(true);
}); });
it("returns a single card when only one method matches", () => { it("shows seven when the screen asks for seven, with spending behind see all", () => {
const result = deriveCompactCards(methods, { charlie: 4 }, true, 5); const result = deriveCompactCards(
expect(result.compactCardIds).toEqual(["charlie"]); "decisionApproaches",
expect([...result.recommendedIds]).toEqual(["charlie"]); {
}); size: "thirteenToOneHundred",
mainKind: "workersCoop",
it("respects a smaller `limit` even when many methods match", () => { scale: ["local"],
const scores = { alpha: 4, bravo: 3, charlie: 3, delta: 2 }; stage: "established",
const result = deriveCompactCards(methods, scores, true, 3); traits: { risky: true, sponsor: true, employer: true },
expect(result.compactCardIds).toEqual(["alpha", "bravo", "charlie"]); },
expect(result.recommendedIds.size).toBe(3); 7,
);
expect(result.compactCardIds).toEqual([
"decision-speeds",
"modified-consensus",
"changing-this-rule",
"emergency-decisions",
"on-site-lead",
"how-we-are-covered",
"elected-board-of-directors",
]);
expect(result.orderedIds.slice(7, 10)).toEqual([
"shared-money",
"spending-limits",
"pay-and-hiring",
]);
}); });
}); });
+5 -5
View File
@@ -46,16 +46,16 @@ describe("governanceCatalog (CR-115)", () => {
}); });
} }
it("core values: ids are 1-based positions with copy fields", () => { it("core values: ids come from the value id field", () => {
const values = listCatalogCoreValues(); const values = listCatalogCoreValues();
const messages = readJson<{ const messages = readJson<{
values: Array<string | { label: string }>; values: Array<{ id: string; label: string }>;
}>("messages/en/create/customRule/coreValues.json"); }>("messages/en/create/customRule/coreValues.json");
expect(values.length).toBe(messages.values.length); expect(values.length).toBe(messages.values.length);
expect(values[0].id).toBe("1"); expect(values[0].id).toBe(messages.values[0].id);
expect(values[0].label).toBeTruthy(); expect(values[0].label).toBe(messages.values[0].label);
expect(typeof values[0].meaning).toBe("string"); expect(typeof values[0].meaning).toBe("string");
expect(typeof values[0].signals).toBe("string"); expect(typeof values[0].signals).toBe("string");
expect(getCatalogCoreValue("1")).toEqual(values[0]); expect(getCatalogCoreValue(values[0].id)).toEqual(values[0]);
}); });
}); });
+1 -1
View File
@@ -45,7 +45,7 @@ describe("governanceTemplateCatalog (Figma 21764-16435)", () => {
"consensus", "consensus",
"do-ocracy", "do-ocracy",
"devolution", "devolution",
"quadratic-governance", "solidarity-network",
]); ]);
expect(getGovernanceTemplatesForHome()).toHaveLength(4); expect(getGovernanceTemplatesForHome()).toHaveLength(4);
}); });
+68
View File
@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState";
import { groupAnswersFromCreateFlowState } from "../../lib/create/groupAnswers";
describe("maturity and chip aliases", () => {
it("scores one maturity answer and keeps a draft that stored several", () => {
const draft = migrateLegacyCreateFlowState({
selectedMaturityIds: ["1", "2", "4"],
selectedOrganizationTypeIds: ["1"],
});
expect(draft.selectedMaturityIds).toEqual([
"earlyStage",
"growthStage",
"established",
]);
expect(draft.selectedOrganizationTypeIds).toEqual(["workersCoop"]);
const answers = groupAnswersFromCreateFlowState(draft);
expect(answers.stage).toBe("earlyStage");
expect(answers.mainKind).toBe("workersCoop");
});
it("loads Enterprise and Larger organization as Established", () => {
const fromId = migrateLegacyCreateFlowState({
selectedMaturityIds: ["4"],
communityStructureChipSnapshots: {
maturity: [
{ id: "1", label: "Early stage", state: "unselected" },
{ id: "2", label: "Growth stage", state: "unselected" },
{ id: "3", label: "Established", state: "unselected" },
{ id: "4", label: "Enterprise", state: "selected" },
],
},
});
expect(fromId.selectedMaturityIds).toEqual(["established"]);
expect(
fromId.communityStructureChipSnapshots?.maturity?.find(
(row) => row.id === "established",
)?.label,
).toBe("Established");
const fromLabel = migrateLegacyCreateFlowState({
selectedMaturityIds: ["larger-organization"],
communityStructureChipSnapshots: {
maturity: [
{ id: "larger-organization", label: "Larger organization", state: "selected" },
],
},
});
expect(fromLabel.selectedMaturityIds).toEqual(["established"]);
});
it("keeps the worker co-op pick when the label was Worker cooperative", () => {
const draft = migrateLegacyCreateFlowState({
selectedOrganizationTypeIds: ["1"],
communityStructureChipSnapshots: {
organizationTypes: [
{ id: "1", label: "Worker cooperative", state: "selected" },
],
},
});
expect(draft.selectedOrganizationTypeIds).toEqual(["workersCoop"]);
expect(
draft.communityStructureChipSnapshots?.organizationTypes?.find(
(row) => row.id === "workersCoop",
)?.label,
).toBe("Worker co-op");
});
});
+59
View File
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState";
import { resolveCoreValueId, resolveMethodId } from "../../lib/create/idAliases";
describe("deck id aliases", () => {
it("maps old card ids onto the remade decks", () => {
expect(resolveMethodId("membership", "open-access")).toEqual({
deck: "membership",
id: "open-admission",
});
expect(resolveMethodId("conflictManagement", "peer-mediation")).toEqual({
deck: "conflictManagement",
id: "conflict-ladder",
});
expect(resolveMethodId("decisionApproaches", "lazy-consensus")).toEqual({
deck: "decisionApproaches",
id: "lazy-consensus",
});
});
it("moves a cut card to the card that now holds its content", () => {
expect(resolveMethodId("decisionApproaches", "proof-of-work")).toEqual({
deck: "decisionApproaches",
id: "weighted-voting",
});
});
it("migrates a saved draft's cards and core values", () => {
const draft = migrateLegacyCreateFlowState({
selectedMembershipMethodIds: ["open-access"],
selectedConflictManagementIds: ["peer-mediation"],
selectedCoreValueIds: ["1", "5", "36"],
coreValuesChipsSnapshot: [
{ id: "1", label: "Accessibility", state: "selected" },
{ id: "5", label: "Altruism", state: "selected" },
{ id: "36", label: "Holocracy", state: "selected" },
],
});
expect(draft.selectedMembershipMethodIds).toEqual(["open-admission"]);
expect(draft.selectedConflictManagementIds).toEqual(["conflict-ladder"]);
expect(draft.selectedCoreValueIds).toEqual(["accessibility", "generosity"]);
expect(resolveCoreValueId("36")).toBeNull();
expect(draft.coreValuesChipsSnapshot?.map((row) => row.id)).toEqual([
"accessibility",
"generosity",
]);
expect(draft.coreValuesChipsSnapshot?.[1]?.label).toBe("Generosity");
});
it("is safe to run twice", () => {
const once = migrateLegacyCreateFlowState({
selectedMembershipMethodIds: ["open-access"],
selectedCoreValueIds: ["2"],
});
const twice = migrateLegacyCreateFlowState(once);
expect(twice.selectedMembershipMethodIds).toEqual(once.selectedMembershipMethodIds);
expect(twice.selectedCoreValueIds).toEqual(once.selectedCoreValueIds);
});
});
@@ -32,7 +32,7 @@ describe("methodCardFacetMatchesPresetForId", () => {
decisionApproachFacetMatchesPreset( decisionApproachFacetMatchesPreset(
{ {
...p, ...p,
selectedApplicableScope: ["Steward finances"], selectedApplicableScope: ["Shared money"],
}, },
uuid, uuid,
), ),
+11 -4
View File
@@ -66,13 +66,20 @@ describe("data/create/customRule/_facetGroups.json (CR-88)", () => {
const [filePath, jsonPointer] = block.source.split("#"); const [filePath, jsonPointer] = block.source.split("#");
const file = readJson<Record<string, { label: string }[]>>(filePath); const file = readJson<Record<string, { label: string }[]>>(filePath);
const arrayKey = jsonPointer.replace(/^\//, ""); const arrayKey = jsonPointer.replace(/^\//, "");
const arr = file[arrayKey]; const arr = file[arrayKey] as { id?: string }[];
expect(Array.isArray(arr), `${group}: ${block.source} → array`).toBe(true); expect(Array.isArray(arr), `${group}: ${block.source} → array`).toBe(true);
const positions = new Set(arr.map((_, i) => String(i + 1))); const chipIds = new Set(
arr.map((row, i) =>
row && typeof row === "object" && "id" in row && typeof row.id === "string"
? row.id
: String(i + 1),
),
);
for (const [valueId, { chipId }] of Object.entries(block.values)) { for (const [valueId, { chipId }] of Object.entries(block.values)) {
const retiredEnterprise = group === "maturity" && valueId === "enterprise";
expect( expect(
positions.has(chipId), chipIds.has(chipId) || retiredEnterprise,
`${group}.${valueId} chipId ${chipId} should be a position in ${block.source} (have ${[...positions].join(",")})`, `${group}.${valueId} chipId ${chipId} should be an option id in ${block.source}`,
).toBe(true); ).toBe(true);
} }
} }
+24 -39
View File
@@ -23,7 +23,7 @@ beforeEach(() => {
findManyMock.mockReset(); findManyMock.mockReset();
}); });
describe("listMethodRecommendations (CR-88 §9.2)", () => { describe("listMethodRecommendations", () => {
it("returns empty rankings when no facets are requested", async () => { it("returns empty rankings when no facets are requested", async () => {
const result = await listMethodRecommendations({ const result = await listMethodRecommendations({
section: "communication", section: "communication",
@@ -33,55 +33,40 @@ describe("listMethodRecommendations (CR-88 §9.2)", () => {
expect(findManyMock).not.toHaveBeenCalled(); expect(findManyMock).not.toHaveBeenCalled();
}); });
it("scores methods by counting matched (group, value) pairs", async () => { it("ranks from the job rule and scores maturity once", async () => {
findManyMock.mockResolvedValueOnce([ const once = await listMethodRecommendations({
{ slug: "loomio", group: "size", value: "thirteenToOneHundred" }, section: "decisionApproaches",
{ slug: "loomio", group: "orgType", value: "workersCoop" },
{ slug: "in-person", group: "size", value: "thirteenToOneHundred" },
]);
const result = await listMethodRecommendations({
section: "communication",
facets: { facets: {
size: ["thirteenToOneHundred"], size: ["thirteenToOneHundred"],
orgType: ["workersCoop"], orgType: ["workersCoop"],
maturity: ["established"],
}, },
}); });
const several = await listMethodRecommendations({
expect(result).toEqual({ section: "decisionApproaches",
rankedSlugs: ["loomio", "in-person"], facets: {
matchesBySlug: { size: ["thirteenToOneHundred"],
loomio: { orgType: ["workersCoop"],
score: 2, maturity: ["established", "earlyStage", "enterprise"],
matchedFacets: ["size:thirteenToOneHundred", "orgType:workersCoop"],
},
"in-person": {
score: 1,
matchedFacets: ["size:thirteenToOneHundred"],
},
}, },
}); });
expect(once?.rankedSlugs).toEqual(several?.rankedSlugs);
expect(once?.rankedSlugs[0]).toBeTruthy();
expect(findManyMock).not.toHaveBeenCalled();
}); });
it("returns null on query failure (caller falls back to authoring order)", async () => { it("uses only the main kind for eligibility", async () => {
findManyMock.mockRejectedValueOnce(new Error("db down"));
const result = await listMethodRecommendations({ const result = await listMethodRecommendations({
section: "communication", section: "decisionApproaches",
facets: { size: ["oneMember"] }, facets: {
size: ["thirteenToOneHundred"],
orgType: ["workersCoop", "forProfit"],
scale: ["local"],
maturity: ["established"],
},
}); });
expect(result).toBeNull(); expect(result?.rankedSlugs).toContain("consent-sociocracy");
}); expect(findManyMock).not.toHaveBeenCalled();
it("dedupes (group, value) so the same row never double-counts", async () => {
findManyMock.mockResolvedValueOnce([
{ slug: "loomio", group: "size", value: "twoToFive" },
{ slug: "loomio", group: "size", value: "twoToFive" },
]);
const result = await listMethodRecommendations({
section: "communication",
facets: { size: ["twoToFive"] },
});
expect(result?.matchesBySlug["loomio"]?.score).toBe(1);
}); });
}); });
@@ -276,7 +276,7 @@ describe("createFlowStateFromPublishedRule", () => {
expect(partial.editingPublishedRuleId).toBe("rule-1"); expect(partial.editingPublishedRuleId).toBe("rule-1");
expect(partial.title).toBe("Oak"); expect(partial.title).toBe("Oak");
expect(partial.communityContext).toBe("River cleanup"); expect(partial.communityContext).toBe("River cleanup");
expect(partial.selectedCoreValueIds).toEqual(["1"]); expect(partial.selectedCoreValueIds).toEqual(["accessibility"]);
expect(partial.coreValuesChipsSnapshot?.[0]?.label).toBe("Ecology"); expect(partial.coreValuesChipsSnapshot?.[0]?.label).toBe("Ecology");
expect(partial.selectedCommunicationMethodIds).toEqual(["slack"]); expect(partial.selectedCommunicationMethodIds).toEqual(["slack"]);
expect(partial.communicationMethodDetailsById?.slack?.corePrinciple).toBe( expect(partial.communicationMethodDetailsById?.slack?.corePrinciple).toBe(
@@ -32,7 +32,11 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
}, },
}; };
const out = parsePublishedDocumentForCommunityRuleDisplay(doc); const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
expect(out.map((s) => s.categoryName)).toEqual(["Values", "Communication"]); expect(out.map((s) => s.categoryName)).toEqual([
"Values",
"Communication",
"Shared terms",
]);
expect(out[0].entries[0].title).toBe("Ecology"); expect(out[0].entries[0].title).toBe("Ecology");
expect(out[0].entries[0].body).toBe( expect(out[0].entries[0].body).toBe(
"We protect water.\n\nLitter = violation.", "We protect water.\n\nLitter = violation.",
@@ -50,7 +54,10 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
], ],
}; };
const out = parsePublishedDocumentForCommunityRuleDisplay(doc); const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
expect(out.map((s) => s.categoryName)).toEqual(["Membership"]); expect(out.map((s) => s.categoryName)).toEqual([
"Membership",
"Shared terms",
]);
}); });
it("prefers document.coreValues over a parallel Values section in document.sections", () => { it("prefers document.coreValues over a parallel Values section in document.sections", () => {
@@ -66,8 +73,10 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
], ],
}; };
const out = parsePublishedDocumentForCommunityRuleDisplay(doc); const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
expect(out.length).toBe(1); expect(out.map((section) => section.categoryName)).toEqual([
expect(out[0].categoryName).toBe("Values"); "Values",
"Shared terms",
]);
expect(out[0].entries[0].title).toBe("Should not duplicate"); expect(out[0].entries[0].title).toBe("Should not duplicate");
expect(out[0].entries[0].body).toBe("x\n\ny"); expect(out[0].entries[0].body).toBe("x\n\ny");
}); });
@@ -80,7 +89,7 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
], ],
}; };
const out = parsePublishedDocumentForCommunityRuleDisplay(doc); const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
expect(out[0].entries[0].body).toMatch(/survival and success/); expect(out[0].entries[0].body).toMatch(/well-being is bound up/);
}); });
it("replaces template placeholder bodies with preset copy, Values first", () => { it("replaces template placeholder bodies with preset copy, Values first", () => {
@@ -99,7 +108,11 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
], ],
}; };
const out = parsePublishedDocumentForCommunityRuleDisplay(doc); const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
expect(out.map((s) => s.categoryName)).toEqual(["Values", "Communication"]); expect(out.map((s) => s.categoryName)).toEqual([
"Values",
"Communication",
"Shared terms",
]);
expect(out[0].entries[0].body).not.toContain("Suggested focus"); expect(out[0].entries[0].body).not.toContain("Suggested focus");
expect(out[0].entries[0].body.length).toBeGreaterThan(20); expect(out[0].entries[0].body.length).toBeGreaterThan(20);
expect(out[1].entries[0].blocks?.length).toBeGreaterThanOrEqual(1); expect(out[1].entries[0].blocks?.length).toBeGreaterThanOrEqual(1);
@@ -112,9 +125,12 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
{ categoryName: "X", entries: [{ title: "t", body: "b" }] }, { categoryName: "X", entries: [{ title: "t", body: "b" }] },
], ],
}; };
expect(parsePublishedDocumentForCommunityRuleDisplay(doc)).toEqual( const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
doc.sections, expect(out[0]).toEqual(doc.sections[0]);
); expect(out.map((section) => section.categoryName)).toEqual([
"X",
"Shared terms",
]);
}); });
it("replaces stale document.sections method category with full methodSelections (custom rules)", () => { it("replaces stale document.sections method category with full methodSelections (custom rules)", () => {
+160
View File
@@ -0,0 +1,160 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { DECK_IDS } from "../../lib/create/deckTypes";
import type { DeckId, RecommendedCard } from "../../lib/create/deckTypes";
import { deckData, recommendDeck } from "../../lib/create/deckRecommend";
import { variantsOf } from "../../lib/create/reviewProfiles";
import type { ReviewProfile } from "../../lib/create/reviewProfiles";
import { methodSlugFromTitle } from "../../lib/create/methodSlugFromTitle";
import { resolveMethodId } from "../../lib/create/idAliases";
import useCasesCompletedRules from "../../messages/en/pages/useCasesCompletedRules.json";
const ROOT = path.resolve(__dirname, "..", "..");
function readJson<T>(rel: string): T {
return JSON.parse(readFileSync(path.join(ROOT, rel), "utf8")) as T;
}
const profiles = readJson<ReviewProfile[]>("data/create/profiles/real-group-profiles.json");
const founders = readJson<ReviewProfile[]>("data/create/profiles/one-member-founders.json");
function compact(card: RecommendedCard) {
const reason =
card.reason.kind === "added"
? `added:${card.reason.answer}`
: card.reason.kind === "opened"
? `opened:${card.reason.answer}`
: card.reason.answer
? `fit+${card.reason.answer}`
: "fit";
return {
id: card.id,
tag: card.tag,
tier: card.tier,
reason,
...(card.decidedBy ? { decidedBy: card.decidedBy } : {}),
...(card.alternative ? { alternative: card.alternative } : {}),
};
}
function picksFor(set: ReviewProfile[]) {
return set.flatMap(variantsOf).map((variant) => ({
profile: variant.profile,
variant: variant.variant,
decks: Object.fromEntries(
DECK_IDS.map((deck) => {
const result = recommendDeck(deckData(deck), variant.answers);
return [
deck,
{
shown: result.shown.map(compact),
waiting: result.waiting.map((card) => card.id),
},
];
}),
),
}));
}
function rowOrderSentence(set: ReviewProfile[]): string {
const base = set.flatMap(variantsOf).filter((variant) => variant.variant === "base");
const totals = { only: 0, score: 0, otherKinds: 0, rowOrder: 0 };
for (const deck of DECK_IDS) {
const jobs = deckData(deck).engine.jobs.filter((job) => job.displayTier !== "none");
for (const job of jobs) {
for (const variant of base) {
const winner = recommendDeck(deckData(deck), variant.answers).recommended.find(
(card) => card.job === job.id && card.tier !== "answers",
);
if (winner?.decidedBy) totals[winner.decidedBy] += 1;
}
}
}
const picks = totals.only + totals.score + totals.otherKinds + totals.rowOrder;
return `Of ${picks} picks for the 92 groups, row order settled ${totals.rowOrder}, other kinds ${totals.otherKinds} and fit ${totals.score}, and ${totals.only} had one eligible card.`;
}
const EXAMPLE_DECK_BY_CATEGORY: Record<string, DeckId> = {
Membership: "membership",
"Decision-Making": "decisionApproaches",
"Decision-making": "decisionApproaches",
"Conflict Management": "conflictManagement",
Communication: "communication",
};
function chosenCards(documentKey: string): { total: number; ids: string[] } {
const doc = (
useCasesCompletedRules as Record<
string,
{ document: { sections: { categoryName: string; entries: { title: string }[] }[] } }
>
)[documentKey];
const live = new Map(
DECK_IDS.map((deck) => [deck, new Set(Object.keys(deckData(deck).engine.cards))]),
);
let total = 0;
const ids: string[] = [];
for (const section of doc.document.sections) {
const deck = EXAMPLE_DECK_BY_CATEGORY[section.categoryName];
if (!deck) continue;
for (const entry of section.entries) {
total += 1;
const slug = methodSlugFromTitle(entry.title);
const resolved = resolveMethodId(deck, slug);
if (resolved && live.get(resolved.deck)?.has(resolved.id)) ids.push(resolved.id);
}
}
return { total, ids };
}
describe("real-group profiles", () => {
it("matches the reviewed 2 Nov package run", () => {
const expected = readJson<{ picks: unknown[] }>("tests/fixtures/profiles-picks.json");
expect(picksFor([...profiles, ...founders])).toEqual(expected.picks);
});
it("reports how often row order settled a pick", () => {
const summary = readFileSync(
path.join(ROOT, "tests/fixtures/profiles-summary.md"),
"utf8",
);
expect(summary).toContain(rowOrderSentence(profiles));
expect(profiles).toHaveLength(92);
expect(founders.length).toBeGreaterThan(0);
expect(JSON.stringify(profiles)).not.toMatch(/[A-Z][a-z]+ [A-Z][a-z]+/);
expect(JSON.stringify(founders)).not.toMatch(/[A-Z][a-z]+ [A-Z][a-z]+/);
});
it("counts how many example-rule cards the engine recommends", () => {
const base = profiles.flatMap(variantsOf).filter((variant) => variant.variant === "base");
const report: Record<string, { chosen: number; resolved: number; recommendedOnMost: number }> = {};
for (const key of ["mutualAidColorado", "foodNotBombs", "boulderCountyStreetMedics"] as const) {
const chosen = chosenCards(key);
let best = 0;
for (const variant of base) {
const recommended = new Set<string>();
for (const deck of DECK_IDS) {
for (const card of recommendDeck(deckData(deck), variant.answers).recommended) {
recommended.add(card.id);
}
}
let hit = 0;
for (const id of chosen.ids) if (recommended.has(id)) hit += 1;
if (hit > best) best = hit;
}
report[key] = {
chosen: chosen.total,
resolved: chosen.ids.length,
recommendedOnMost: best,
};
}
// The team's own Rule is not in the repo, so this locks the three example
// Rules. A change that moves a recommendation changes recommendedOnMost.
expect(report).toEqual({
mutualAidColorado: { chosen: 7, resolved: 6, recommendedOnMost: 4 },
foodNotBombs: { chosen: 8, resolved: 8, recommendedOnMost: 5 },
boulderCountyStreetMedics: { chosen: 9, resolved: 8, recommendedOnMost: 5 },
});
});
});
@@ -120,7 +120,7 @@ describe("sectionFromDecision", () => {
]); ]);
const blocks = section?.entries[0]?.blocks ?? []; const blocks = section?.entries[0]?.blocks ?? [];
expect(blocks).toContainEqual({ expect(blocks).toContainEqual({
label: "Consensus Level", label: "Consensus level",
body: "100%", body: "100%",
}); });
}); });