v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/

- crates/ld-engine : formule inertielle (parité avec la maquette, garde C),
  coquille / changement de fond, assistant d'édition à règles, composition
  d'un document sous vote permanent (adoptions, aperçus, rivales).
- crates/trustin-access : qui voit, qui ouvre, qui gère les accès.
- crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de
  connect.monnaie-libre.fr), invitations, entrée en vigueur automatique,
  signalements de contradiction, traductions éditoriales, publication git,
  taille de la toile lue sur la chaîne.
- seeds/acte-engagement-certification.json : 33 articles, fr + en + es ;
  en vigueur : préambule, E1, E2 (seed a minima).
- maquette/ : frontend, backend et docker v2 déplacés tels quels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-21 02:04:42 +02:00
co-authored by Claude Opus 5.5
parent 4b1732916d
commit 67ddee28b2
275 changed files with 8979 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
// ─────────────────────────────────────────────────────────────
// libreDecision v2 — local-first persistence (IndexedDB via idb-keyval).
// One key per collective: `ld2:<collectiveId>` holds a Bundle-shaped state.
// Seeds and user imports go through the SAME importBundle() path.
// Sync-ready invariants live in types/domain.ts — this layer stays dumb.
// ─────────────────────────────────────────────────────────────
import { get, set, del, keys } from 'idb-keyval'
import type { Bundle, Collective, Id, ISODate } from '~/types/domain'
const KEY_PREFIX = 'ld2:'
const ACTIVE_KEY = 'ld2-active'
/** In-memory state of one collective — exactly a Bundle minus export metadata. */
export type CollectiveState = Omit<Bundle, 'schemaVersion' | 'exportedAt'>
export function emptyState(collective: Collective): CollectiveState {
return {
collective,
people: [],
circles: [],
decisions: [],
concerns: [],
objections: [],
advices: [],
assents: [],
mandates: [],
docs: [],
clauses: [],
versions: [],
protocols: [],
sessions: [],
votes: [],
}
}
// ── Load / save ──────────────────────────────────────────────
export async function listCollectiveIds(): Promise<Id[]> {
const all = await keys()
return all
.filter((k): k is string => typeof k === 'string' && k.startsWith(KEY_PREFIX))
.map(k => k.slice(KEY_PREFIX.length))
}
export async function loadState(collectiveId: Id): Promise<CollectiveState | undefined> {
return await get<CollectiveState>(KEY_PREFIX + collectiveId)
}
const pendingSaves = new Map<Id, ReturnType<typeof setTimeout>>()
/** Debounced write (500 ms) — every mutation calls this; last write wins. */
export function saveStateDebounced(state: CollectiveState): void {
const id = state.collective.id
const existing = pendingSaves.get(id)
if (existing) clearTimeout(existing)
pendingSaves.set(
id,
setTimeout(() => {
pendingSaves.delete(id)
void set(KEY_PREFIX + id, toRaw(state))
}, 500),
)
}
export async function saveStateNow(state: CollectiveState): Promise<void> {
const id = state.collective.id
const existing = pendingSaves.get(id)
if (existing) {
clearTimeout(existing)
pendingSaves.delete(id)
}
await set(KEY_PREFIX + id, toRaw(state))
}
export async function deleteCollective(collectiveId: Id): Promise<void> {
await del(KEY_PREFIX + collectiveId)
const active = await getActiveCollectiveId()
if (active === collectiveId) await set(ACTIVE_KEY, null)
}
export async function getActiveCollectiveId(): Promise<Id | null> {
return (await get<Id | null>(ACTIVE_KEY)) ?? null
}
export async function setActiveCollectiveId(id: Id | null): Promise<void> {
await set(ACTIVE_KEY, id)
}
// ── Export / import — the single bundle path ─────────────────
export function toBundle(state: CollectiveState, exportedAt: ISODate): Bundle {
return { schemaVersion: 2, exportedAt, ...toRaw(state) }
}
export async function sha256Hex(text: string): Promise<string> {
const data = new TextEncoder().encode(text)
const digest = await crypto.subtle.digest('SHA-256', data)
return Array.from(new Uint8Array(digest))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
export interface ImportIssue {
level: 'error' | 'warning'
message: string
}
/** Structural validation — never throws, returns readable French issues. */
export function validateBundle(raw: unknown): { bundle?: Bundle; issues: ImportIssue[] } {
const issues: ImportIssue[] = []
if (typeof raw !== 'object' || raw === null) {
return { issues: [{ level: 'error', message: 'Ce fichier ne contient pas un collectif lisible.' }] }
}
const b = raw as Partial<Bundle>
if (b.schemaVersion !== 2)
issues.push({ level: 'error', message: 'Version de fichier inconnue (schemaVersion ≠ 2).' })
if (!b.collective?.id || !b.collective?.slug || !b.collective?.name)
issues.push({ level: 'error', message: 'Le collectif du fichier est incomplet (id, slug ou nom manquant).' })
for (const key of [
'people', 'circles', 'decisions', 'concerns', 'objections', 'advices', 'assents',
'mandates', 'docs', 'clauses', 'versions', 'protocols', 'sessions', 'votes',
] as const) {
if (!Array.isArray(b[key]))
issues.push({ level: 'error', message: `Collection manquante ou invalide : ${key}.` })
}
// Invariant: every collective carries a Consent protocol (import validates it).
if (Array.isArray(b.protocols) && !b.protocols.some(p => p.method === 'consent'))
issues.push({
level: 'warning',
message: 'Aucun protocole de consentement — les chemins collectifs retomberont sur « décider sur avis » jusqu\'à sa création.',
})
if (issues.some(i => i.level === 'error')) return { issues }
return { bundle: b as Bundle, issues }
}
export interface ImportResult {
state?: CollectiveState
issues: ImportIssue[]
collided: boolean
}
/**
* Import a bundle as a new local collective.
* Foreign bundles (exportedAt present, not one of ours) get a lineage stamp — « essaimé de … ».
*/
export async function importBundle(json: string, opts?: { asSeed?: boolean }): Promise<ImportResult> {
let raw: unknown
try {
raw = JSON.parse(json)
} catch {
return { issues: [{ level: 'error', message: 'Fichier illisible : ce n\'est pas du JSON valide.' }], collided: false }
}
const { bundle, issues } = validateBundle(raw)
if (!bundle) return { issues, collided: false }
const existingIds = await listCollectiveIds()
const collided = existingIds.includes(bundle.collective.id)
const { schemaVersion: _v, exportedAt, ...stateRest } = bundle
const state: CollectiveState = stateRest
if (!opts?.asSeed) {
state.collective = {
...state.collective,
lineage: {
sourceSlug: bundle.collective.slug,
exportedAt,
sha256: await sha256Hex(json),
},
}
}
return { state, issues, collided }
}
/** Deep-clone to plain JSON — strips Pinia/Vue reactivity proxies before IndexedDB. */
function toRaw<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+577
View File
@@ -0,0 +1,577 @@
/**
* The seven collective templates (BLUEPRINT-V2.md « Seeds » GABARITS + Δ10/Δ11/Δ20).
*
* buildTemplateBundle() produces a COMPLETE schemaVersion-2 Bundle that goes
* through the SAME import path as a user bundle (importBundle, asSeed:true).
*
* Doctrinal invariants honored here:
* - every template seeds a Consent protocol (protocolByRange.consent — Δ10);
* - every ClauseVersion is born of a Decision: one FOUNDING decision
* (route 'record', adopted) carries every seeded clause version — a single
* shared founding act, exactly what a real constitutive gesture is;
* - blank stays a blank page: preamble + A1 + the « consignation » clause +
* requireEffects='none' + the Consent protocol ONLY — every other setting
* falls back to SETTINGS_DEFAULTS through resolveSettings;
* - the five standard templates (informal/association/cooperative/commune/
* free-currency) seed the 10 triage setting clauses + Consent, Nuancé,
* Réglage collectif, Élection — 'protocols.large' = the NUANCED protocol
* everywhere EXCEPT free-currency, where large = the inertial binary
* (D30M50B.1G.2 — the Ğ1 heritage put back in its place);
* - free-currency: the default resources unit is 'DU' — v2 keeps it as a
* DESCRIPTIVE default suggested by the tunnel UI (Resources.unit), never a
* conversion: DU and € are oil and water, never summed (huile/eau);
* - symmetric: full transparency (isTransparent), a « Déclaration de
* symétrie » reference TextDoc, and the draft decision « Déclarer notre
* symétrie ». Two MandateDrafts are SUGGESTED by the onboarding UI (not
* seeded — a mandate is born of a decision, never of a template):
* { title: 'Ambassadeur', domainCircleIds: [root], domainTags: ['liaison'],
* durationDays: 180, reportEveryDays: 30 }
* { title: 'Négociateur', domainCircleIds: [root], domainTags: ['symétrie'],
* durationDays: 180, reportEveryDays: 30 }
* - every template proposes the first decision « Adopter notre Pacte »
* (route 'collective', status 'draft', consent protocol).
*/
import type {
Bundle,
Circle,
Clause,
ClauseVersion,
Collective,
CollectiveTemplate,
Decision,
FormulaParams,
Id,
InertiaPreset,
ISODate,
Json,
Person,
Protocol,
TextDoc,
VoteMethod,
} from '../types/domain'
import { formatModeParams } from '../engine/modeParams'
export type TemplateId = CollectiveTemplate
// ─────────────────────────────────────────────────────────────
// The seven cards (/creer, écran 2) — blank FIRST
// ─────────────────────────────────────────────────────────────
export interface TemplateCard {
id: TemplateId
title: string
subtitle: string
description: string
}
const STANDARD_SUBTITLE = 'point de départ — tout est amendable par décision'
export const TEMPLATE_CARDS: TemplateCard[] = [
{
id: 'blank',
title: 'Page blanche',
subtitle: "observatoire d'abord",
description:
"Une clause : nous consignons nos décisions ; le reste s'écrira au fil de l'eau. "
+ 'Consentement seul installé.',
},
{
id: 'informal',
title: 'Informel',
subtitle: STANDARD_SUBTITLE,
description:
"Un groupe d'amis, un projet naissant : des chemins légers, aucune matière exigée.",
},
{
id: 'association',
title: 'Association',
subtitle: STANDARD_SUBTITLE,
description:
'Un Bureau, le consentement et le vote nuancé — la vie associative sans les camps.',
},
{
id: 'cooperative',
title: 'Coopérative',
subtitle: STANDARD_SUBTITLE,
description:
'Décider ensemble du travail et des ressources — matière exigée sur ce qui engage.',
},
{
id: 'commune',
title: 'Commune',
subtitle: STANDARD_SUBTITLE,
description:
'Des quartiers, des périmètres emboîtés — la subsidiarité en actes.',
},
{
id: 'free-currency',
title: 'Communauté monnaie libre',
subtitle: STANDARD_SUBTITLE,
description:
"L'héritage Toile de Confiance : le pour/contre inertiel, outil de dernier recours "
+ 'des très grands corps.',
},
{
id: 'symmetric',
title: 'Institution symétrique',
subtitle: 'le geste fondateur',
description:
'Transparence totale, Déclaration de symétrie, et la première décision : '
+ 'déclarer notre symétrie.',
},
]
// ─────────────────────────────────────────────────────────────
// buildTemplateBundle
// ─────────────────────────────────────────────────────────────
export interface TemplateBuildOptions {
name: string
slug: string
color: string
icon: string
meName: string
memberNames: string[]
now: ISODate
newId: () => Id
}
/** Which protocols a template installs. */
const STANDARD_TEMPLATES: TemplateId[] = [
'informal',
'association',
'cooperative',
'commune',
'free-currency',
'symmetric',
]
export function buildTemplateBundle(id: TemplateId, opts: TemplateBuildOptions): Bundle {
const { name, slug, color, icon, meName, memberNames, now, newId } = opts
const collectiveId = newId()
const entity = () => ({
id: newId(),
collectiveId,
createdAt: now,
updatedAt: now,
})
// ── People — me + the initial members ──────────────────────
const me: Person = { ...entity(), displayName: meName, isMe: true }
const members: Person[] = memberNames.map(displayName => ({
...entity(),
displayName,
isMe: false,
}))
const people = [me, ...members]
// ── Circles — root « Tous » + one sober typed circle per template ──
const rootCircle: Circle = {
...entity(),
name: 'Tous',
purpose: 'Toutes les personnes du collectif',
memberIds: people.map(p => p.id),
domains: [],
}
const circles: Circle[] = [rootCircle]
const typed: Partial<Record<TemplateId, { name: string; purpose: string; kind: Circle['kind'] }>> = {
association: { name: 'Bureau', purpose: "L'équipe qui fait tourner l'association", kind: 'team' },
cooperative: { name: 'Conseil', purpose: 'Le conseil de la coopérative', kind: 'team' },
commune: { name: 'Quartiers', purpose: 'Les lieux de la commune', kind: 'place' },
'free-currency': { name: 'Forgerons', purpose: 'Celles et ceux qui forgent les blocs', kind: 'team' },
}
const typedDef = typed[id]
if (typedDef) {
circles.push({
...entity(),
name: typedDef.name,
purpose: typedDef.purpose,
kind: typedDef.kind,
memberIds: [],
parentCircleId: rootCircle.id,
domains: [],
})
}
// ── Protocols ──────────────────────────────────────────────
const protocols: Protocol[] = []
const mkProtocol = (
pName: string,
method: VoteMethod,
description: string,
durationDays: number,
formula: FormulaParams,
): Protocol => {
const protocol: Protocol = {
...entity(),
name: pName,
method,
description,
durationDays,
ballot: 'open',
formula,
modeParams: formatModeParams({
duration_days: durationDays,
majority_pct: formula.majorityPct,
base_exponent: formula.baseExponent,
gradient_exponent: formula.gradientExponent,
constant_base: formula.constantBase,
}),
}
protocols.push(protocol)
return protocol
}
const baseFormula = { majorityPct: 50, baseExponent: 0.1, gradientExponent: 0.2, constantBase: 0 }
const consent = mkProtocol(
'Consentement',
'consent',
'Ça me va / J\'objecte — zéro objection maintenue vaut adoption.',
7,
{ ...baseFormula },
)
let nuanced: Protocol | undefined
let parametric: Protocol | undefined
let election: Protocol | undefined
let binary: Protocol | undefined
if (STANDARD_TEMPLATES.includes(id)) {
nuanced = mkProtocol(
'Vote nuancé',
'nuanced',
'Chacun·e se prononce en nuances, pas en camps.',
14,
{ ...baseFormula, nuancedThresholdPct: 60, nuancedMinParticipants: 3 },
)
parametric = mkProtocol(
'Réglage collectif',
'parametric',
'Décider au curseur — le collectif retient la médiane de chaque curseur.',
14,
{ ...baseFormula, parametricMinParticipants: 3 },
)
election = mkProtocol(
'Élection',
'election',
"Désigner une personne — blanc possible ; en cas d'égalité, vous départagez.",
14,
{ ...baseFormula, electionMinParticipants: 3, tieBreak: 'runoff' },
)
}
if (id === 'free-currency') {
// D30M50B.1G.2 — the exact inherited inertia of the Ğ1 web of trust.
binary = mkProtocol(
'Vote WoT binaire inertiel',
'binary',
"l'outil de dernier recours des très grands corps — héritage Toile de Confiance",
30,
{ ...baseFormula },
)
}
// ── The Pact + clauses, each founded by ONE adopted decision ──
const pactDoc: TextDoc = {
...entity(),
slug: 'pacte',
title: 'Notre Pacte',
role: 'pact',
description: 'Notre contrat social — sacralisé, jamais immuable',
}
const docs: TextDoc[] = [pactDoc]
const founding: Decision = {
...entity(),
authorId: me.id,
title: 'Fonder notre Pacte',
tags: ['pacte'],
reversibility: 'costly',
weight: 'structural',
urgent: false,
scope: { selfOnly: false, circleIds: [rootCircle.id], personIds: [] },
route: 'record',
triageRule: 'R0a',
routeOverridden: false,
decidedHow: 'posé à la création du collectif — chaque clause reste amendable',
status: 'adopted',
decidedAt: now,
stewardIds: [me.id],
measurerIds: [],
visibility: 'collective',
}
const clauses: Clause[] = []
const versions: ClauseVersion[] = []
let position = 0
const mkClause = (
section: string,
code: string,
title: string,
inertia: InertiaPreset,
content: string,
settingKey?: string,
settingValue?: Json,
): Clause => {
const clause: Clause = {
...entity(),
docId: pactDoc.id,
section,
position: position++,
code,
title,
inertia,
...(settingKey !== undefined ? { settingKey } : {}),
}
const version: ClauseVersion = {
...entity(),
clauseId: clause.id,
decisionId: founding.id,
versionLabel: 'v1',
content,
...(settingValue !== undefined ? { settingValue } : {}),
status: 'current',
adoptedAt: now,
}
clause.currentVersionId = version.id
clauses.push(clause)
versions.push(version)
return clause
}
// Preamble P0 — Autonomie–Équilibre–Liaison, proposed and MODIFIABLE.
mkClause(
'Préambule',
'P0',
'Préambule',
'standard',
'Autonomie — chacun·e décide de ce qui ne concerne que lui ou elle. '
+ 'Équilibre — ce qui engage le collectif se décide ensemble, à la juste échelle. '
+ 'Liaison — chaque décision garde sa trace et reste révisable. '
+ 'Ce préambule est proposé, jamais imposé : modifiable comme tout le reste.',
)
// A1 — Notre finalité (default compasses: vitalité / émancipation).
mkClause(
'Finalité',
'A1',
'Notre finalité',
'high',
"La vitalité du collectif et l'émancipation de chacun·e sont nos deux boussoles.",
)
if (id === 'blank') {
// The one practice clause of the blank page.
mkClause(
'Pratique',
'C1',
'Nous consignons nos décisions',
'standard',
"Nous consignons nos décisions ; le reste s'écrira au fil de l'eau.",
)
}
// ── Setting clauses ────────────────────────────────────────
const requireEffects = id === 'blank' || id === 'informal' ? 'none' : 'binding'
const requireEffectsContent =
requireEffects === 'none'
? "Aucune matière exigée — le collectif s'outillera quand la pratique le demandera."
: 'Matière exigée : décisions engageantes et structurantes — au moins un effet recherché avant tout vote collectif.'
if (id === 'blank') {
// Blank page: requireEffects='none' + the consent protocol, NOTHING else —
// every other setting falls back to SETTINGS_DEFAULTS via resolveSettings.
mkClause('Réglages', 'S1', 'Matière exigée', 'standard', requireEffectsContent, 'triage.requireEffects', 'none')
mkClause(
'Réglages',
'S2',
'Protocole de consentement',
'high',
'Notre protocole de consentement : Consentement (7 jours).',
'protocols.consent',
consent.id,
)
} else {
// The 10 triage keys of SETTINGS_DEFAULTS, in clear French.
mkClause('Réglages', 'S1', 'Petit groupe', 'standard', "Jusqu'à 5 personnes, un avis suffit avant de décider.", 'triage.smallGroupMax', 5)
mkClause('Réglages', 'S2', 'Grand collectif', 'standard', 'Au-delà de 50 personnes, la modalité des grands corps s\'applique.', 'triage.collectiveMin', 50)
mkClause('Réglages', 'S3', 'Consentement', 'standard', "Jusqu'à 7 personnes, un tour d'accord suffit : sans objection, c'est adopté.", 'triage.consentMax', 7)
mkClause('Réglages', 'S4', "Fenêtre d'objection", 'standard', "Quarante-huit heures pour dire « Ça me va » ou objecter.", 'triage.objectionWindowHours', 48)
mkClause('Réglages', 'S5', "Fenêtre d'avis", 'standard', 'Soixante-douze heures pour déposer un avis.', 'triage.adviceWindowHours', 72)
mkClause('Réglages', 'S6', 'Formulation', 'standard', "Quatorze jours pour s'instruire et formuler des contre-propositions.", 'triage.framingDays', 14)
mkClause('Réglages', 'S7', 'Débordement du périmètre', 'standard', 'À la moitié de déclarations spontanées, on élargit ou on motive publiquement.', 'triage.concernEscalateRatio', 0.5)
mkClause('Réglages', 'S8', 'Récurrence', 'standard', 'À la troisième décision semblable, réclamer un mandat ou créer une règle.', 'triage.recurrenceThreshold', 3)
mkClause('Réglages', 'S9', "Épreuve du réel", 'standard', "Quatre-vingt-dix jours après l'adoption, le réel a-t-il suivi ?", 'triage.reviewDelayDays', 90)
mkClause('Réglages', 'S10', 'Matière exigée', 'standard', requireEffectsContent, 'triage.requireEffects', requireEffects)
// Protocol clauses — the Pact references its protocols (pactClauseId).
const consentClause = mkClause(
'Réglages',
'S11',
'Protocole de consentement',
'high',
'Notre protocole de consentement : Consentement (7 jours).',
'protocols.consent',
consent.id,
)
consent.pactClauseId = consentClause.id
if (nuanced) {
const c = mkClause(
'Réglages',
'S12',
'Protocole nuancé',
'high',
'De 8 à 50 personnes : le vote nuancé, six nuances, jamais deux camps.',
'protocols.nuanced',
nuanced.id,
)
nuanced.pactClauseId = c.id
}
// 'protocols.large' — nuanced everywhere, inertial binary ONLY here.
const large = id === 'free-currency' ? binary : nuanced
if (large) {
const c = mkClause(
'Réglages',
'S13',
'Modalité des grands corps',
'max',
id === 'free-currency'
? 'Au-delà de 50 : le pour/contre inertiel hérité de la Toile de Confiance (D30M50B.1G.2).'
: 'Au-delà de 50 : le vote nuancé, la modalité que notre Pacte a choisie.',
'protocols.large',
large.id,
)
if (id === 'free-currency' && binary) binary.pactClauseId = c.id
}
if (parametric) {
const c = mkClause(
'Réglages',
'S14',
'Réglage collectif',
'high',
'Montant, taux, répartition : décider au curseur — le collectif retient la médiane.',
'protocols.parametric',
parametric.id,
)
parametric.pactClauseId = c.id
}
if (election) {
const c = mkClause(
'Réglages',
'S15',
"Protocole d'élection",
'high',
"Désigner une personne — blanc possible ; en cas d'égalité, vous départagez, jamais l'outil.",
'protocols.election',
election.id,
)
election.pactClauseId = c.id
}
}
if (id === 'symmetric') {
// Full transparency, declared in the Pact itself.
mkClause(
'Symétrie',
'T1',
'Transparence totale',
'max',
'Tout ce que nous décidons est public — la symétrie commence par la transparence.',
)
docs.push({
...entity(),
slug: 'declaration-de-symetrie',
title: 'Déclaration de symétrie',
role: 'reference',
description:
'Le geste fondateur : notre institution se déclare symétrique de celles qui nous gouvernent.',
})
}
// ── Decisions — founding (adopted) + first proposals (draft) ──
const decisions: Decision[] = [founding]
decisions.push({
...entity(),
authorId: me.id,
title: 'Adopter notre Pacte',
body: 'Relire le Pacte proposé par le gabarit, l\'amender si besoin, et l\'adopter ensemble.',
tags: ['pacte'],
reversibility: 'costly',
weight: 'structural',
urgent: false,
scope: { selfOnly: false, circleIds: [rootCircle.id], personIds: [] },
route: 'collective',
triageRule: 'R5',
routeOverridden: false,
protocolId: consent.id,
status: 'draft',
stewardIds: [me.id],
measurerIds: [],
visibility: 'collective',
})
if (id === 'symmetric') {
decisions.push({
...entity(),
authorId: me.id,
title: 'Déclarer notre symétrie',
body: 'Adopter la Déclaration de symétrie et la rendre publique.',
tags: ['symétrie'],
reversibility: 'costly',
weight: 'structural',
urgent: false,
scope: { selfOnly: false, circleIds: [rootCircle.id], personIds: [] },
route: 'collective',
triageRule: 'R5',
routeOverridden: false,
protocolId: consent.id,
status: 'draft',
stewardIds: [me.id],
measurerIds: [],
visibility: 'collective',
})
}
// ── The tenant ─────────────────────────────────────────────
const collective: Collective = {
id: collectiveId,
slug,
name,
color,
icon,
template: id,
isTransparent: id === 'symmetric' || id === 'free-currency',
pactDocId: pactDoc.id,
rootCircleId: rootCircle.id,
createdAt: now,
updatedAt: now,
}
return {
schemaVersion: 2,
exportedAt: now,
collective,
people,
circles,
decisions,
concerns: [],
objections: [],
advices: [],
assents: [],
mandates: [],
docs,
clauses,
versions,
protocols,
sessions: [],
votes: [],
}
}