v2 : conception ultracode + moteur de formules porté en TS
- Blueprint définitif (docs/dev/BLUEPRINT-V2.md + JSON) : panel 4 visions + 3 juges, corpus doctrinal (Mon nom est personne, Une économie du don, SejeteralO), 4 critiques d'intégration, 3 vérificateurs, réparations - Corpus d'intention + lectures brutes archivés - Moteur porté Python→TS pur : threshold (inertie WoT, Smith, ComTech), nuancé 6 niveaux, DSL modeParams — 45 tests vitest, parité croisée vérifiée (Forgeron W=7224 T=120 ⇒ 94) - Contrat de domaine v2 (types/domain.ts) + lexique UI (lexicon.ts) - vitest + idb-keyval en dépendances Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// libreDecision v2 — domain core (BLUEPRINT-V2.md, docs/dev/)
|
||||
// Local-first, sync-ready. English identifiers, French UI via app/lexicon.ts.
|
||||
// INVARIANT 1: THE DECISION IS THE PIVOT — mandates, clauses, revocations,
|
||||
// ratifications and dossier elements are chained specializations of it.
|
||||
// INVARIANT 2: that unification is an architectural fact, NEVER UI vocabulary.
|
||||
// INVARIANT 3: no aggregate/score on a person, anywhere — we evaluate things.
|
||||
// INVARIANT 4: no computation ever produces adopted content without a dated
|
||||
// human gesture (parametric crystallization, election tie-break, dossier close).
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
export type Id = string // UUIDv7 — time-sortable, sync-ready
|
||||
export type ISODate = string // ISO 8601 UTC
|
||||
export type Json = string | number | boolean | null | Json[] | { [k: string]: Json }
|
||||
|
||||
// Every entity: collective scoping + sync clock + soft delete.
|
||||
// Sync-ready rules: never a physical delete (archivedAt);
|
||||
// Vote/Advice/Objection/Assent append-only (trivial merge); the rest LWW by updatedAt.
|
||||
export interface Entity {
|
||||
id: Id
|
||||
collectiveId: Id
|
||||
createdAt: ISODate
|
||||
updatedAt: ISODate
|
||||
archivedAt?: ISODate
|
||||
}
|
||||
|
||||
// ── The tenant ───────────────────────────────────────────────
|
||||
export type CollectiveTemplate =
|
||||
| 'blank' // « Page blanche — observatoire d'abord » : one-clause Pact + A1, Consent protocol ONLY
|
||||
| 'informal'
|
||||
| 'association'
|
||||
| 'cooperative'
|
||||
| 'commune'
|
||||
| 'free-currency' // Ğ1 heritage — ONLY template where protocolByRange.large = inertial binary
|
||||
| 'symmetric' // « Institution symétrique » — the founding militant gesture (MNP)
|
||||
|
||||
export interface Collective {
|
||||
id: Id
|
||||
slug: string
|
||||
name: string
|
||||
color: string // hex accent
|
||||
icon: string // i-lucide-*
|
||||
template: CollectiveTemplate
|
||||
isTransparent: boolean
|
||||
pactDocId: Id // TextDoc role='pact' — settings AS a voted document
|
||||
rootCircleId: Id
|
||||
lineage?: {
|
||||
// seeding trail: set automatically when importing a bundle from another collective
|
||||
sourceSlug: string
|
||||
exportedAt: ISODate
|
||||
sha256: string
|
||||
} // NO parentCollectiveId — federation is never a table
|
||||
createdAt: ISODate
|
||||
updatedAt: ISODate
|
||||
}
|
||||
|
||||
// ── The people ───────────────────────────────────────────────
|
||||
export interface Person extends Entity {
|
||||
displayName: string
|
||||
isMe: boolean // local profile (v2 single-seat; « mode atelier » for in-person capture)
|
||||
attributes?: Record<string, number> // SELF-declared attributes (free keys: 'heures/mois'…),
|
||||
// edited in /donnees — feed ONLY the « Pour moi » card;
|
||||
// never shown to others, never aggregated (invariant 3)
|
||||
duniterAddress?: string // SS58 — reserved for Duniter auth (out of v2 scope)
|
||||
wotStatus?: 'member' | 'smith'
|
||||
}
|
||||
|
||||
export type CircleKind = 'place' | 'theme' | 'team'
|
||||
|
||||
export interface Circle extends Entity {
|
||||
name: string
|
||||
purpose: string
|
||||
kind?: CircleKind // lieu / thème / équipe — iconography, grouping, suggestion order
|
||||
// ONLY; never a right; non-blocking hint when a 'team' exceeds 12
|
||||
memberIds: Id[] // v2: nominative list = the ONLY membership rule (auditable)
|
||||
parentCircleId?: Id // nesting (couches d'oignon) — root = rootCircle
|
||||
domains: string[] // tags — SUGGESTION/discovery only, never a voting right
|
||||
}
|
||||
// Corpus of a decision = COMPUTABLE union: members of scoped circles
|
||||
// ∪ named persons ∪ holders of mandates whose domain intersects.
|
||||
// Every inclusion keeps its reason (Concern.reason) — auditable in one tap.
|
||||
|
||||
// ── THE DECISION (pivot) ─────────────────────────────────────
|
||||
export type Reversibility = 'easy' | 'costly' | 'irreversible'
|
||||
export type Weight = 'light' | 'binding' | 'structural'
|
||||
|
||||
export type DecisionRoute =
|
||||
| 'solo' // « Je décide » (optional trace outside mandate)
|
||||
| 'mandate' // « Je décide, sous mandat » (mandatory trace + objection window)
|
||||
| 'transmit' // « Je transmets » — someone else's mandate covers
|
||||
| 'advice' // « J'écoute, puis je décide »
|
||||
| 'collective' // « Nous décidons » (protocol modality)
|
||||
| 'record' // « Déjà tranché — je le consigne » : the observatory in one gesture, adopted immediately
|
||||
|
||||
export type DecisionStatus =
|
||||
| 'draft'
|
||||
| 'advice'
|
||||
| 'objection'
|
||||
| 'framing'
|
||||
| 'voting'
|
||||
| 'adopted'
|
||||
| 'rejected'
|
||||
| 'revoked'
|
||||
| 'transmitted'
|
||||
| 'closed'
|
||||
|
||||
export type TriageRule = 'R-U' | 'R0a' | 'R0b' | 'R0c' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6'
|
||||
|
||||
// The matter — « il n'est pas consulté, il est instruit »
|
||||
export interface Effect {
|
||||
label: string // sought effect, one line
|
||||
target?: string // optional measurable target (« ≤ 400 € », « +5 membres »)
|
||||
measured?: { note: string; at: ISODate; byId: Id } // observed — filled by measurerIds at review time
|
||||
}
|
||||
export interface Brief {
|
||||
context?: string
|
||||
sources?: { label: string; url: string }[]
|
||||
symptomsVsCauses?: string // doctrinal qualification of the subject
|
||||
effects: Effect[] // GUARD (state.ts, opening of a COLLECTIVE session only):
|
||||
// required when weight ∈ Pact 'triage.requireEffects' ('binding' covers binding+structural);
|
||||
// structural under guard ⇒ ≥1 effect with target. NEVER for weight 'light',
|
||||
// NEVER in the tunnel, NEVER for solo/record.
|
||||
}
|
||||
|
||||
// The linked resource decision — « chaque décision implique une allocation »
|
||||
export interface Resources {
|
||||
note: string // « Ce que ça engage » in one sentence
|
||||
amount?: number
|
||||
unit?: 'heures' | '€' | 'DU' | string // oil/water: units NEVER converted nor summed together
|
||||
}
|
||||
|
||||
// The collective tuning — « raisonner curseur / table de mixage »
|
||||
export type ParamKind = 'slider' | 'share' // 'curve' reserved for v3 (full SejeteralO)
|
||||
export interface ParamDef {
|
||||
key: string
|
||||
label: string // MANDATORY business label — never raw a, b, c
|
||||
kind: ParamKind
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
unit?: string
|
||||
baseline?: number // status quo — hollow marker always displayed
|
||||
derived?: boolean // resolved by the constraint, never voted — labelled « calculé »
|
||||
}
|
||||
export interface ParamSpec {
|
||||
params: ParamDef[] // ≤ 7 (SejeteralO lesson: small manipulable space)
|
||||
constraint: 'none' | 'sum100' // sum100: 'share' params total 100; EXACTLY ONE 'derived'
|
||||
// share required (validated at spec creation) — the absorption variable, resolved
|
||||
// linearly (100 − Σ others)
|
||||
impactAttrKey?: string // key of Person.attributes → « Pour moi » card ('linear-share')
|
||||
}
|
||||
|
||||
export type ChainKind = 'ratification' | 'revision' | 'revocation' | 'element'
|
||||
|
||||
export interface Decision extends Entity {
|
||||
authorId: Id
|
||||
title: string // THE capture sentence — only mandatory field
|
||||
body?: string // progressive disclosure
|
||||
brief?: Brief // matter — see opening guard above
|
||||
baselineNote?: string // « Aujourd'hui : … » — suggested on advice/collective routes, never solo
|
||||
resources?: Resources // SUGGESTED-unfolded in the tunnel (never blocking); REQUIRED by the
|
||||
// window/session opening guard (state.ts) when weight != 'light' outside solo
|
||||
tags: string[] // discovery + recurrence — NEVER a corpus
|
||||
|
||||
// — triage verdict (pre-filled by the engine, adjustable) —
|
||||
reversibility: Reversibility
|
||||
weight: Weight
|
||||
urgent: boolean
|
||||
scope: {
|
||||
selfOnly: boolean
|
||||
circleIds: Id[] // floor: widening free, NEVER narrowing below the computation
|
||||
personIds: Id[]
|
||||
}
|
||||
route: DecisionRoute
|
||||
triageRule: TriageRule // relegated to the « pourquoi ? » disclosure, PV, journal — never on the card
|
||||
routeOverridden: boolean
|
||||
overrideNote?: string // MANDATORY when lightening (derogation asymmetry)
|
||||
scopeKeptNote?: string // influx reached + perimeter kept ⇒ mandatory PUBLIC motivated note
|
||||
|
||||
// — chained specializations (the generative folding) —
|
||||
underMandateId?: Id // decided UNDER this mandate → the mandate's « feux de la rampe »
|
||||
createsMandate?: MandateDraft
|
||||
amendsClauseId?: Id
|
||||
protocolId?: Id // resolved by the Pact, adjustable by weighting up
|
||||
paramSpec?: ParamSpec // when parametric modality (« Réglage collectif »)
|
||||
parentDecisionId?: Id
|
||||
chainKind?: ChainKind
|
||||
// 'element': micro-decision of a split dossier (« décision incrémentielle »).
|
||||
// Parent of elements: framing→closed transition guarded by
|
||||
// « all element children terminal » — steward GESTURE (Fil card), never automatic;
|
||||
// the closing cartography becomes the dossier's PV.
|
||||
decidedHow?: string // route 'record': « comment ça s'est décidé », one sentence
|
||||
|
||||
// — time & windows —
|
||||
status: DecisionStatus
|
||||
windowEndsAt?: ISODate
|
||||
windowSuspendedAt?: ISODate // boundary objection → countdown SUSPENDED (visible)
|
||||
decidedAt?: ISODate
|
||||
review?: Review // mandatory when irreversible || structural
|
||||
sunsetAt?: ISODate
|
||||
stewardIds: Id[] // stewards — REALIZE (and crystallize parametric, close dossiers)
|
||||
measurerIds: Id[] // measurers — OBSERVE (fill Effect.measured): the doctrinal pair
|
||||
|
||||
// — proof & visibility —
|
||||
visibility: 'private' | 'scope' | 'collective' // anti-surveillance: trace owned outside mandate
|
||||
engraving?: Engraving
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
dueAt: ISODate
|
||||
verdict?: 'confirmed' | 'revise' | 'revoke' // UI: « Ça tient / À revoir / À révoquer »
|
||||
note?: string
|
||||
decidedAt?: ISODate
|
||||
}
|
||||
|
||||
export interface Engraving {
|
||||
sha256: string
|
||||
engravedAt: ISODate
|
||||
proofLevel: 'local' // v2: ONLY 'local' — « empreinte locale — démo »
|
||||
ipfsCid?: string // reserved v3 — never displayed until real
|
||||
chainRef?: string
|
||||
}
|
||||
|
||||
// ── Inclusion & windows ──────────────────────────────────────
|
||||
export interface Concern extends Entity {
|
||||
decisionId: Id
|
||||
personId: Id
|
||||
origin: 'computed' | 'declared' // UI: « concerné·e en premier lieu » / « en second lieu »
|
||||
reason: string // « membre du cercle Forgerons » — shown on tap
|
||||
declaredNote?: string
|
||||
beforeSnapshot: boolean // before the list is arrested ⇒ vote; after ⇒ consultative voice
|
||||
priority?: 0 | 1 | 2 | 3 // « pondère tes enjeux » — per element of a dossier;
|
||||
// feeds the closing cartography, never the voting right
|
||||
}
|
||||
|
||||
export interface Assent extends Entity {
|
||||
// « Ça me va » STORED — the explicit agreement of objection windows
|
||||
decisionId: Id
|
||||
personId: Id
|
||||
recordedById?: Id // mode atelier
|
||||
}
|
||||
// OBJECTION WINDOW RULE (state.ts): at deadline with no open objection —
|
||||
// easy ⇒ adopted (silence counts as agreement); costly/irreversible ⇒ adopted ONLY if
|
||||
// ≥1 Assent from a concerned person ≠ author, otherwise EXTENSION by one notch
|
||||
// (+objectionWindowHours, Fil reminder) — never adoption by pure silence outside easy.
|
||||
|
||||
export interface Objection extends Entity {
|
||||
decisionId: Id
|
||||
personId: Id
|
||||
kind: 'content' | 'boundary' // boundary SUSPENDS the window — the frontier precedes substance
|
||||
argument: string
|
||||
status: 'open' | 'withdrawn' | 'integrated' | 'escalated'
|
||||
resolutionNote?: string
|
||||
recordedById?: Id // mode atelier: « saisi par X pour Y — atelier du <date> »
|
||||
}
|
||||
|
||||
export interface Advice extends Entity {
|
||||
decisionId: Id
|
||||
personId: Id
|
||||
position: 'favorable' | 'reserved' | 'unfavorable'
|
||||
note?: string
|
||||
recordedById?: Id // mode atelier
|
||||
}
|
||||
|
||||
// ── The power ────────────────────────────────────────────────
|
||||
export type NominationMethod =
|
||||
| 'ratified-self'
|
||||
| 'election-no-candidate'
|
||||
| 'nuanced-vote'
|
||||
| 'consent'
|
||||
| 'draw'
|
||||
| 'rotation'
|
||||
|
||||
export interface Mandate extends Entity {
|
||||
title: string
|
||||
holderId: Id
|
||||
originDecisionId: Id // the mandate IS born of a decision — clickable
|
||||
domain: { circleIds: Id[]; tags: string[] }
|
||||
startsAt: ISODate
|
||||
endsAt: ISODate // always bounded
|
||||
electorCircleId: Id // election corpus = revocation corpus
|
||||
nominationMethod: NominationMethod
|
||||
reports: MandateReport[]
|
||||
status: 'proposed' | 'active' | 'expired' | 'revoked'
|
||||
// NO aggregate/gauge: the sheet shows counted facts (« Exercice du mandat »),
|
||||
// never synthesized; the public stream = « Feux de la rampe »
|
||||
}
|
||||
export interface MandateReport {
|
||||
dueAt: ISODate
|
||||
deliveredAt?: ISODate
|
||||
content?: string
|
||||
}
|
||||
export interface MandateDraft {
|
||||
title: string
|
||||
domainCircleIds: Id[]
|
||||
domainTags: string[]
|
||||
durationDays: number
|
||||
reportEveryDays?: number
|
||||
}
|
||||
|
||||
// ── The texts ────────────────────────────────────────────────
|
||||
export interface TextDoc extends Entity {
|
||||
slug: string
|
||||
title: string
|
||||
role: 'pact' | 'reference'
|
||||
description: string // seeded Pact: « Notre contrat social — sacralisé, jamais immuable »
|
||||
provenance?: Provenance
|
||||
}
|
||||
export interface Provenance {
|
||||
sources: { title: string; url: string; date?: string; version?: string }[]
|
||||
voteRecord?: {
|
||||
url: string
|
||||
modeParams: string
|
||||
period: string
|
||||
result: {
|
||||
for: number
|
||||
against: number
|
||||
invalid?: number
|
||||
wotSize: number
|
||||
thresholdRequired: number
|
||||
status: string
|
||||
}
|
||||
}
|
||||
contributors?: string[]
|
||||
notes?: string // typos of voted texts: FLAGGED, never fixed without a vote
|
||||
}
|
||||
|
||||
export type InertiaPreset = 'low' | 'standard' | 'high' | 'max'
|
||||
// REAL wiring: low={M:50,G:0.1} standard={M:50,G:0.2} high={M:60,G:0.4} max={M:66,G:0.6} (B=0.1, C=0)
|
||||
|
||||
export interface Clause extends Entity {
|
||||
docId: Id
|
||||
section: string
|
||||
position: number
|
||||
code: string
|
||||
title: string
|
||||
inertia: InertiaPreset
|
||||
currentVersionId?: Id
|
||||
settingKey?: string // Pact clauses: e.g. 'triage.smallGroupMax'
|
||||
}
|
||||
|
||||
export interface ClauseVersion extends Entity {
|
||||
clauseId: Id
|
||||
decisionId: Id // EVERY version is born of a decision (founding seeded ones included)
|
||||
versionLabel: string
|
||||
content: string
|
||||
settingValue?: Json
|
||||
status: 'current' | 'proposed' | 'superseded' | 'rejected'
|
||||
adoptedAt?: ISODate
|
||||
}
|
||||
|
||||
// ── Modalities & vote ────────────────────────────────────────
|
||||
export type VoteMethod = 'consent' | 'binary' | 'nuanced' | 'parametric' | 'election'
|
||||
// UI: consentement · pour/contre (last-resort tool) · nuancé ·
|
||||
// « Réglage collectif » (au curseur) · élection
|
||||
|
||||
export interface Protocol extends Entity {
|
||||
name: string
|
||||
method: VoteMethod
|
||||
description: string
|
||||
durationDays: number
|
||||
ballot: 'open' | 'secret' // decided by the Pact — default 'open' (feux de la rampe).
|
||||
// v2 single-seat: « secret d'affichage — démo » (aggregate tallies and anonymized
|
||||
// comments only); real crypto arrives with sync (v3)
|
||||
formula: FormulaParams
|
||||
modeParams: string // DERIVED display DSL: 'D30M50B.1G.2S.1' — never hand-edited
|
||||
pactClauseId?: Id // referenced by the Pact ⇒ changing it = amending the Pact
|
||||
}
|
||||
export interface FormulaParams {
|
||||
majorityPct: number // M
|
||||
baseExponent: number // B (default 0.1)
|
||||
gradientExponent: number // G — the inertia
|
||||
constantBase: number // C (default 0)
|
||||
smithExponent?: number
|
||||
techcommExponent?: number
|
||||
nuancedMinParticipants?: number
|
||||
nuancedThresholdPct?: number
|
||||
parametricMinParticipants?: number // collective-tuning quorum — not reached ⇒ rejected at the gesture
|
||||
electionMinParticipants?: number // election quorum — not reached ⇒ rejected
|
||||
tieBreak?: 'runoff' | 'draw' // election tie-break DECIDED IN ADVANCE by the Pact
|
||||
// (default 'runoff': chained runoff session among tied; 'draw': draw among tied —
|
||||
// never chosen by the engine)
|
||||
}
|
||||
|
||||
export interface VoteSession extends Entity {
|
||||
decisionId: Id
|
||||
protocolId: Id
|
||||
corpusPersonIds: Id[] // SNAPSHOT at opening — UI: « liste arrêtée le … »
|
||||
corpusSize: number // W — never 0 again (letter reserved to the Atelier)
|
||||
opensAt: ISODate
|
||||
closesAt: ISODate
|
||||
status: 'open' | 'frozen' | 'closed'
|
||||
// 'frozen': PARAMETRIC only — at closesAt votes are frozen, the frozen median is shown,
|
||||
// and the session WAITS for the steward's human gesture (Fil reminder if late).
|
||||
crystallizedById?: Id // the « Cristalliser » gesture: who, when — dated and signed
|
||||
crystallizedAt?: ISODate
|
||||
outcome?: 'adopted' | 'rejected' | 'tie'
|
||||
// 'tie': tied election — the tool NEVER breaks a tie: chained runoff session
|
||||
// (restricted to tied) or draw IF the protocol planned it (tieBreak).
|
||||
// Consent/nuanced/binary: open→closed automatic at closesAt (the engine only observes a
|
||||
// threshold over human-written content). Parametric: open→frozen→closed (gesture).
|
||||
// INVARIANT: tallies, histograms AND medians ALWAYS recomputed from Vote[] — never denormalized.
|
||||
}
|
||||
|
||||
export type NuancedValue = 0 | 1 | 2 | 3 | 4 | 5
|
||||
|
||||
export interface Vote extends Entity {
|
||||
sessionId: Id
|
||||
voterId: Id
|
||||
value?: 'for' | 'against' | NuancedValue
|
||||
// nuancé UI: 0 Pas du tout · 1 Plutôt non · 2 Réservé · 3 Plutôt oui · 4 Oui · 5 Tout à fait
|
||||
values?: number[] // parametric — order of paramSpec.params EXCLUDING derived
|
||||
choicePersonId?: Id // election — designation; a vote without choicePersonId = BLANK
|
||||
// (counts for participation, not designation)
|
||||
// INVARIANT: EXACTLY ONE of value / values / choicePersonId per method (validated by the
|
||||
// store) — except an election blank (none of the three, method 'election' only)
|
||||
comment?: string // MANDATORY when negative (against, 0, 1)
|
||||
supersedesVoteId?: Id // audited re-vote — PRIVACY: the chain is NEVER displayed publicly
|
||||
// under a voter's name; visible to its AUTHOR only; even with ballot 'open', only the
|
||||
// LAST active vote appears under a name, aggregate tallies recomputed
|
||||
// (« pardonner les positions »)
|
||||
recordedById?: Id // mode atelier
|
||||
}
|
||||
|
||||
// ── Resolved Pact settings (pure function — NOT a settings table) ──
|
||||
export interface CollectiveSettings {
|
||||
// resolveSettings(pactClauses, versions)
|
||||
triage: {
|
||||
smallGroupMax: number // default 5
|
||||
collectiveMin: number // default 50
|
||||
consentMax: number // default 7
|
||||
objectionWindowHours: number // default 48
|
||||
adviceWindowHours: number // default 72
|
||||
framingDays: number // default 14 — « s'instruire et formuler des contre-propositions »
|
||||
concernEscalateRatio: number // default 0.5 — reached ⇒ MANDATORY handling (state.ts guard)
|
||||
recurrenceThreshold: number // default 3
|
||||
reviewDelayDays: number // default 90
|
||||
requireEffects: 'none' | 'structural' | 'binding'
|
||||
// key 'triage.requireEffects' — governs the matter guard of COLLECTIVE sessions:
|
||||
// 'binding' = binding AND structural (default of structured templates);
|
||||
// 'structural' = structural only; 'none' seeded in blank and informal.
|
||||
// Under guard, structural ⇒ ≥1 effect with measurable target.
|
||||
}
|
||||
protocolByRange: {
|
||||
consent: Id // MANDATORY — invariant: every collective has a Consent protocol
|
||||
// (every template seeds it, import validates it)
|
||||
nuanced?: Id
|
||||
large?: Id // ex-inertialBinary: the name no longer presumes the method.
|
||||
// Standard templates → nuanced; free-currency → inertial binary
|
||||
parametric?: Id // « Réglage collectif »
|
||||
election?: Id
|
||||
clauseByInertia?: Record<InertiaPreset, Id> // the Ğ1 heritage lives here, intact
|
||||
}
|
||||
// SPECIFIED FALLBACK (resolveSettings): any unresolved optional key falls back to consent;
|
||||
// consent unfindable (corrupted bundle) ⇒ triage routes 'advice' with the banner
|
||||
// « Aucun protocole — crée-le ou décide sur avis » — NEVER a crash.
|
||||
}
|
||||
|
||||
// ── Triage engine contracts ──────────────────────────────────
|
||||
export interface TriageInput {
|
||||
title: string
|
||||
tags: string[]
|
||||
scope: Decision['scope']
|
||||
reversibility: Reversibility
|
||||
weight: Weight
|
||||
urgent: boolean
|
||||
amendsClauseId?: Id
|
||||
}
|
||||
export interface TriageContext {
|
||||
// assembled by the store, consumed pure — testable without I/O
|
||||
myActiveMandates: Mandate[]
|
||||
otherActiveMandates: Mandate[]
|
||||
matchingClauses: Clause[] // Q0 index « déjà décidé ? » — the SAME index as Cmd+K
|
||||
similarRecentCount: number // adopted decisions sharing ≥2 tags over 90 d
|
||||
similarRecordedCount: number // 'record' entries sharing ≥2 tags — maturation
|
||||
computedConcernedIds: Id[]
|
||||
}
|
||||
export interface Verdict {
|
||||
route: DecisionRoute
|
||||
rule: TriageRule
|
||||
explanation: string // ONE French sentence — displayed ALONE, without rule code
|
||||
protocolId?: Id
|
||||
windowHours?: number
|
||||
framingDays?: number
|
||||
reviewRequired: boolean
|
||||
engravingSuggested: boolean
|
||||
conservatoryChain?: boolean
|
||||
parametricHint?: boolean // number/%/amount detected ⇒ « C'est un réglage » put forward
|
||||
alternatives: { route: DecisionRoute; label: string; cost: string }[]
|
||||
// always present: « C'est un réglage — décidez au curseur » (parametric);
|
||||
// « Déjà tranché — je le consigne » (record); binary with its cost (« deux camps, un perdant »)
|
||||
suggestion?: { kind: 'claim-mandate' | 'create-rule' | 'protocolize'; prefill: Json }
|
||||
}
|
||||
|
||||
// ── Local persistence & future sync ──────────────────────────
|
||||
export interface Bundle {
|
||||
// export/import AND seeds — the SAME code path
|
||||
schemaVersion: 2
|
||||
exportedAt: ISODate
|
||||
collective: Collective
|
||||
people: Person[]
|
||||
circles: Circle[]
|
||||
decisions: Decision[]
|
||||
concerns: Concern[]
|
||||
objections: Objection[]
|
||||
advices: Advice[]
|
||||
assents: Assent[]
|
||||
mandates: Mandate[]
|
||||
docs: TextDoc[]
|
||||
clauses: Clause[]
|
||||
versions: ClauseVersion[]
|
||||
protocols: Protocol[]
|
||||
sessions: VoteSession[]
|
||||
votes: Vote[]
|
||||
}
|
||||
// data/persistence.ts: IndexedDB (idb-keyval), key `ld2:<collectiveId>`, 500 ms debounce.
|
||||
// importBundle() validates protocolByRange.consent + sets Collective.lineage for foreign bundles.
|
||||
// Sync-ready: UUIDv7, updatedAt (LWW), archivedAt, Vote/Advice/Objection/Assent append-only.
|
||||
// Pinia store contract unchanged — the future FastAPI adapter changes NO screen.
|
||||
// Contract notes: ballot 'secret' must become cryptographic server-side (v3);
|
||||
// the privacy of supersedesVoteId chains (author-visible only) is a contract invariant.
|
||||
Reference in New Issue
Block a user