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:
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Pure voting-formula engine — TypeScript port of backend/app/engine/.
|
||||||
|
* Python is the reference implementation; the vitest suite in
|
||||||
|
* frontend/tests/engine/ mirrors the backend pytest suite.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { wotThreshold, smithThreshold, techcommThreshold } from './threshold'
|
||||||
|
export { nuancedResult, LEVEL_LABELS, NUM_LEVELS } from './nuanced'
|
||||||
|
export type { NuancedResult } from './nuanced'
|
||||||
|
export { parseModeParams, formatModeParams } from './modeParams'
|
||||||
|
export type { ModeParams } from './modeParams'
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Mode-params DSL: parse and format compact parameter strings.
|
||||||
|
*
|
||||||
|
* Exact TypeScript port of backend/app/engine/mode_params.py
|
||||||
|
* (Python is the reference implementation for parsing; the Python
|
||||||
|
* module has no formatter, so formatModeParams() is defined as the
|
||||||
|
* canonical inverse of parseModeParams()).
|
||||||
|
*
|
||||||
|
* A mode-params string encodes voting formula parameters in a compact format.
|
||||||
|
* Example: "D30M50B.1G.2T.1"
|
||||||
|
*
|
||||||
|
* Supported codes:
|
||||||
|
* D = duration_days (int)
|
||||||
|
* M = majority_pct (int, 0-100)
|
||||||
|
* B = base_exponent (float)
|
||||||
|
* G = gradient_exponent (float)
|
||||||
|
* C = constant_base (float)
|
||||||
|
* S = smith_exponent (float)
|
||||||
|
* T = techcomm_exponent (float)
|
||||||
|
* N = ratio_multiplier (float)
|
||||||
|
* R = is_ratio_mode (bool, 0 or 1)
|
||||||
|
*
|
||||||
|
* Values may start with a dot for decimals < 1, e.g. "B.1" means base_exponent=0.1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ModeParams {
|
||||||
|
duration_days: number
|
||||||
|
majority_pct: number
|
||||||
|
base_exponent: number
|
||||||
|
gradient_exponent: number
|
||||||
|
constant_base: number
|
||||||
|
smith_exponent: number | null
|
||||||
|
techcomm_exponent: number | null
|
||||||
|
ratio_multiplier: number | null
|
||||||
|
is_ratio_mode: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type CodeType = 'int' | 'float' | 'bool'
|
||||||
|
|
||||||
|
// Ordered list of recognised codes and their target keys + types
|
||||||
|
const CODES: Record<string, { key: keyof ModeParams; type: CodeType }> = {
|
||||||
|
D: { key: 'duration_days', type: 'int' },
|
||||||
|
M: { key: 'majority_pct', type: 'int' },
|
||||||
|
B: { key: 'base_exponent', type: 'float' },
|
||||||
|
G: { key: 'gradient_exponent', type: 'float' },
|
||||||
|
C: { key: 'constant_base', type: 'float' },
|
||||||
|
S: { key: 'smith_exponent', type: 'float' },
|
||||||
|
T: { key: 'techcomm_exponent', type: 'float' },
|
||||||
|
N: { key: 'ratio_multiplier', type: 'float' },
|
||||||
|
R: { key: 'is_ratio_mode', type: 'bool' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex: a single uppercase letter followed by a numeric value (int or float,
|
||||||
|
// possibly starting with '.' for values like .1 meaning 0.1)
|
||||||
|
const PARAM_RE = /([A-Z])(\d*\.?\d+)/g
|
||||||
|
|
||||||
|
function getDefaults(): ModeParams {
|
||||||
|
return {
|
||||||
|
duration_days: 30,
|
||||||
|
majority_pct: 50,
|
||||||
|
base_exponent: 0.1,
|
||||||
|
gradient_exponent: 0.2,
|
||||||
|
constant_base: 0.0,
|
||||||
|
smith_exponent: null,
|
||||||
|
techcomm_exponent: null,
|
||||||
|
ratio_multiplier: null,
|
||||||
|
is_ratio_mode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a mode-params string into a parameter object.
|
||||||
|
*
|
||||||
|
* Defaults are applied for any code not present in the string.
|
||||||
|
*
|
||||||
|
* @param paramsStr - Compact parameter string, e.g. "D30M50B.1G.2T.1"
|
||||||
|
* @returns Parsed parameters with defaults for codes not found
|
||||||
|
* @throws Error if an unrecognised code letter is found
|
||||||
|
*/
|
||||||
|
export function parseModeParams(paramsStr: string): ModeParams {
|
||||||
|
const result = getDefaults()
|
||||||
|
|
||||||
|
if (!paramsStr || !paramsStr.trim()) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const match of paramsStr.matchAll(PARAM_RE)) {
|
||||||
|
const code = match[1]!
|
||||||
|
const rawValue = match[2]!
|
||||||
|
|
||||||
|
const entry = CODES[code]
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error(`Code de parametre inconnu : '${code}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key, type } = entry
|
||||||
|
|
||||||
|
if (type === 'int') {
|
||||||
|
// Python: int(float(raw)) truncates toward zero
|
||||||
|
;(result as Record<keyof ModeParams, unknown>)[key] = Math.trunc(parseFloat(rawValue))
|
||||||
|
} else if (type === 'float') {
|
||||||
|
;(result as Record<keyof ModeParams, unknown>)[key] = parseFloat(rawValue)
|
||||||
|
} else {
|
||||||
|
;(result as Record<keyof ModeParams, unknown>)[key] = parseFloat(rawValue) !== 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a numeric value in DSL form: 0.1 -> ".1", 1 -> "1", 2.5 -> "2.5".
|
||||||
|
*/
|
||||||
|
function formatValue(value: number): string {
|
||||||
|
const s = String(value)
|
||||||
|
return value > 0 && value < 1 ? s.replace(/^0\./, '.') : s
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a parameter object back into a canonical mode-params string.
|
||||||
|
*
|
||||||
|
* Canonical form matches the strings used across the project
|
||||||
|
* (e.g. "D30M50B.1G.2S.1"): D, M, B, G always present in that order,
|
||||||
|
* then C when non-zero, then S/T/N when set, then R1 when ratio mode.
|
||||||
|
* Round-trip: parseModeParams(formatModeParams(p)) is identical to p.
|
||||||
|
*
|
||||||
|
* @param params - Parameters to encode (missing keys take defaults)
|
||||||
|
* @returns Compact string, e.g. "D30M50B.1G.2"
|
||||||
|
*/
|
||||||
|
export function formatModeParams(params: Partial<ModeParams> = {}): string {
|
||||||
|
const p = getDefaults()
|
||||||
|
for (const key of Object.keys(p) as Array<keyof ModeParams>) {
|
||||||
|
const value = params[key]
|
||||||
|
if (value !== undefined) {
|
||||||
|
;(p[key] as ModeParams[typeof key]) = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [
|
||||||
|
`D${formatValue(p.duration_days)}`,
|
||||||
|
`M${formatValue(p.majority_pct)}`,
|
||||||
|
`B${formatValue(p.base_exponent)}`,
|
||||||
|
`G${formatValue(p.gradient_exponent)}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (p.constant_base !== 0) {
|
||||||
|
parts.push(`C${formatValue(p.constant_base)}`)
|
||||||
|
}
|
||||||
|
if (p.smith_exponent !== null) {
|
||||||
|
parts.push(`S${formatValue(p.smith_exponent)}`)
|
||||||
|
}
|
||||||
|
if (p.techcomm_exponent !== null) {
|
||||||
|
parts.push(`T${formatValue(p.techcomm_exponent)}`)
|
||||||
|
}
|
||||||
|
if (p.ratio_multiplier !== null) {
|
||||||
|
parts.push(`N${formatValue(p.ratio_multiplier)}`)
|
||||||
|
}
|
||||||
|
if (p.is_ratio_mode) {
|
||||||
|
parts.push('R1')
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('')
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Six-level nuanced vote evaluation.
|
||||||
|
*
|
||||||
|
* Exact TypeScript port of backend/app/engine/nuanced_vote.py
|
||||||
|
* (Python is the reference implementation).
|
||||||
|
*
|
||||||
|
* Levels:
|
||||||
|
* 0 - CONTRE
|
||||||
|
* 1 - PAS DU TOUT
|
||||||
|
* 2 - PAS D'ACCORD
|
||||||
|
* 3 - NEUTRE
|
||||||
|
* 4 - D'ACCORD
|
||||||
|
* 5 - TOUT A FAIT
|
||||||
|
*
|
||||||
|
* Adoption rule:
|
||||||
|
* The sum of votes at levels 3 + 4 + 5 must be >= thresholdPct% of total votes.
|
||||||
|
* A minimum number of participants is also required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const LEVEL_LABELS: Record<number, string> = {
|
||||||
|
0: 'CONTRE',
|
||||||
|
1: 'PAS DU TOUT',
|
||||||
|
2: "PAS D'ACCORD",
|
||||||
|
3: 'NEUTRE',
|
||||||
|
4: "D'ACCORD",
|
||||||
|
5: 'TOUT A FAIT',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NUM_LEVELS = 6
|
||||||
|
|
||||||
|
/** Result shape mirrors the Python dict returned by evaluate_nuanced(). */
|
||||||
|
export interface NuancedResult {
|
||||||
|
total: number
|
||||||
|
per_level_counts: Record<number, number>
|
||||||
|
positive_count: number
|
||||||
|
positive_pct: number
|
||||||
|
threshold_met: boolean
|
||||||
|
min_participants_met: boolean
|
||||||
|
adopted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round to 2 decimals with ties-to-even, mirroring Python's round(x, 2).
|
||||||
|
*/
|
||||||
|
function round2(value: number): number {
|
||||||
|
const scaled = value * 100
|
||||||
|
const floor = Math.floor(scaled)
|
||||||
|
const diff = scaled - floor
|
||||||
|
let rounded: number
|
||||||
|
if (diff > 0.5) {
|
||||||
|
rounded = floor + 1
|
||||||
|
} else if (diff < 0.5) {
|
||||||
|
rounded = floor
|
||||||
|
} else {
|
||||||
|
rounded = floor % 2 === 0 ? floor : floor + 1
|
||||||
|
}
|
||||||
|
return rounded / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate a nuanced vote from a list of individual vote levels.
|
||||||
|
*
|
||||||
|
* @param votes - List of vote levels (each 0-5), one entry per voter
|
||||||
|
* @param thresholdPct - Minimum percentage of positive votes (levels 3-5) for adoption
|
||||||
|
* @param minParticipants - Minimum number of participants required for validity
|
||||||
|
* @returns Aggregated result (counts, percentage, adoption flags)
|
||||||
|
* @throws Error if any vote value is outside the 0-5 integer range
|
||||||
|
*/
|
||||||
|
export function nuancedResult(
|
||||||
|
votes: number[],
|
||||||
|
thresholdPct: number = 80,
|
||||||
|
minParticipants: number = 59,
|
||||||
|
): NuancedResult {
|
||||||
|
// Validate vote levels (integer check added: JS has no KeyError safety net)
|
||||||
|
for (const v of votes) {
|
||||||
|
if (!Number.isInteger(v) || v < 0 || v > 5) {
|
||||||
|
throw new Error(`Niveau de vote invalide : ${v}. Les niveaux valides sont 0-5.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = votes.length
|
||||||
|
|
||||||
|
const perLevelCounts: Record<number, number> = {}
|
||||||
|
for (let level = 0; level < NUM_LEVELS; level++) {
|
||||||
|
perLevelCounts[level] = 0
|
||||||
|
}
|
||||||
|
for (const v of votes) {
|
||||||
|
perLevelCounts[v] = (perLevelCounts[v] ?? 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positive = levels 3 (NEUTRE), 4 (D'ACCORD), 5 (TOUT A FAIT)
|
||||||
|
const positiveCount = (perLevelCounts[3] ?? 0) + (perLevelCounts[4] ?? 0) + (perLevelCounts[5] ?? 0)
|
||||||
|
|
||||||
|
const positivePct = total > 0 ? (positiveCount / total) * 100.0 : 0.0
|
||||||
|
|
||||||
|
const thresholdMet = positivePct >= thresholdPct
|
||||||
|
const minParticipantsMet = total >= minParticipants
|
||||||
|
const adopted = thresholdMet && minParticipantsMet
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
per_level_counts: perLevelCounts,
|
||||||
|
positive_count: positiveCount,
|
||||||
|
positive_pct: round2(positivePct),
|
||||||
|
threshold_met: thresholdMet,
|
||||||
|
min_participants_met: minParticipantsMet,
|
||||||
|
adopted,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* WoT members threshold formula for binary votes.
|
||||||
|
*
|
||||||
|
* Exact TypeScript port of backend/app/engine/threshold.py,
|
||||||
|
* smith_threshold.py and techcomm_threshold.py (Python is the
|
||||||
|
* reference implementation — same guards, same ceil rounding).
|
||||||
|
*
|
||||||
|
* Core formula:
|
||||||
|
* Result = C + B^W + (M + (1-M) * (1 - (T/W)^G)) * max(0, T - C)
|
||||||
|
*
|
||||||
|
* Where:
|
||||||
|
* C = constantBase
|
||||||
|
* B = baseExponent
|
||||||
|
* W = wotSize (corpus of eligible voters)
|
||||||
|
* T = totalVotes (for + against)
|
||||||
|
* M = majorityRatio (majorityPct / 100)
|
||||||
|
* G = gradientExponent
|
||||||
|
*
|
||||||
|
* Inertia behaviour:
|
||||||
|
* - Low participation (T << W) -> near-unanimity required
|
||||||
|
* - High participation (T -> W) -> simple majority M suffices
|
||||||
|
*
|
||||||
|
* Reference test case:
|
||||||
|
* wotSize=7224, votesFor=97, votesAgainst=23 (total=120)
|
||||||
|
* params M50 B.1 G.2 => threshold=94, adopted (97 >= 94)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the minimum number of *for* votes required for adoption.
|
||||||
|
*
|
||||||
|
* A vote passes when `votesFor >= wotThreshold(...)`.
|
||||||
|
*
|
||||||
|
* @param wotSize - Size of the eligible voter corpus (WoT members)
|
||||||
|
* @param totalVotes - Number of votes cast (for + against)
|
||||||
|
* @param majorityPct - Majority percentage (0-100). 50 = simple majority at full participation
|
||||||
|
* @param baseExponent - B in the formula; B^W is a vanishingly small offset when W is large
|
||||||
|
* @param gradientExponent - G controls how fast the super-majority decays toward M
|
||||||
|
* @param constantBase - C, a fixed additive floor on the threshold
|
||||||
|
* @returns The ceiling of the computed threshold
|
||||||
|
*/
|
||||||
|
export function wotThreshold(
|
||||||
|
wotSize: number,
|
||||||
|
totalVotes: number,
|
||||||
|
majorityPct: number = 50,
|
||||||
|
baseExponent: number = 0.1,
|
||||||
|
gradientExponent: number = 0.2,
|
||||||
|
constantBase: number = 0.0,
|
||||||
|
): number {
|
||||||
|
if (wotSize <= 0) {
|
||||||
|
throw new Error('wotSize doit etre strictement positif')
|
||||||
|
}
|
||||||
|
if (totalVotes < 0) {
|
||||||
|
throw new Error('totalVotes ne peut pas etre negatif')
|
||||||
|
}
|
||||||
|
if (majorityPct < 0 || majorityPct > 100) {
|
||||||
|
throw new Error('majorityPct doit etre entre 0 et 100')
|
||||||
|
}
|
||||||
|
|
||||||
|
const C = constantBase
|
||||||
|
const B = baseExponent
|
||||||
|
const W = wotSize
|
||||||
|
const T = totalVotes
|
||||||
|
const M = majorityPct / 100.0
|
||||||
|
const G = gradientExponent
|
||||||
|
|
||||||
|
// Guard: if no votes, threshold is at least ceil(C + B^W)
|
||||||
|
if (T === 0) {
|
||||||
|
return Math.ceil(C + B ** W)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core formula
|
||||||
|
const participationRatio = T / W
|
||||||
|
const inertiaFactor = 1.0 - participationRatio ** G
|
||||||
|
const requiredRatio = M + (1.0 - M) * inertiaFactor
|
||||||
|
const result = C + B ** W + requiredRatio * Math.max(0.0, T - C)
|
||||||
|
|
||||||
|
return Math.ceil(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smith sub-WoT threshold criterion.
|
||||||
|
*
|
||||||
|
* Formula: ceil(smithWotSize ^ S)
|
||||||
|
*
|
||||||
|
* @param smithWotSize - Number of active Smith members (forgerons)
|
||||||
|
* @param exponent - S in the formula
|
||||||
|
* @returns Minimum Smith votes required
|
||||||
|
*/
|
||||||
|
export function smithThreshold(smithWotSize: number, exponent: number = 0.1): number {
|
||||||
|
if (smithWotSize <= 0) {
|
||||||
|
throw new Error('smithWotSize doit etre strictement positif')
|
||||||
|
}
|
||||||
|
return Math.ceil(smithWotSize ** exponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Technical Committee threshold criterion.
|
||||||
|
*
|
||||||
|
* Formula: ceil(cotecSize ^ T)
|
||||||
|
*
|
||||||
|
* @param cotecSize - Number of Technical Committee members
|
||||||
|
* @param exponent - T in the formula
|
||||||
|
* @returns Minimum TechComm votes required
|
||||||
|
*/
|
||||||
|
export function techcommThreshold(cotecSize: number, exponent: number = 0.1): number {
|
||||||
|
if (cotecSize <= 0) {
|
||||||
|
throw new Error('cotecSize doit etre strictement positif')
|
||||||
|
}
|
||||||
|
return Math.ceil(cotecSize ** exponent)
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// libreDecision v2 — lexicon: SINGLE source of French UI labels.
|
||||||
|
// Tone rules (BLUEPRINT-V2.md):
|
||||||
|
// 1. tutoyer la personne dans ses gestes ;
|
||||||
|
// 2. actes collectifs à l'infinitif (« Adopter notre Pacte ») ;
|
||||||
|
// 3. sujet grammatical des phrases générées = les personnes ou le collectif,
|
||||||
|
// JAMAIS la formule ou le système.
|
||||||
|
// The anti-lexicon vitest test (tests/lexicon.spec.ts) enforces FORBIDDEN_UI_TERMS
|
||||||
|
// over every UI-visible string (lexicon + .vue templates).
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DecisionRoute,
|
||||||
|
DecisionStatus,
|
||||||
|
InertiaPreset,
|
||||||
|
NominationMethod,
|
||||||
|
NuancedValue,
|
||||||
|
VoteMethod,
|
||||||
|
Weight,
|
||||||
|
Reversibility,
|
||||||
|
} from '~/types/domain'
|
||||||
|
|
||||||
|
/** Words that must NEVER appear in UI-visible text (case-insensitive, accent-aware).
|
||||||
|
* They may exist as code identifiers — the test only scans UI strings. */
|
||||||
|
export const FORBIDDEN_UI_TERMS = [
|
||||||
|
'déléguer',
|
||||||
|
'délégation',
|
||||||
|
'délégué',
|
||||||
|
'dette',
|
||||||
|
'retard',
|
||||||
|
'en souffrance',
|
||||||
|
'corpus',
|
||||||
|
'snapshot',
|
||||||
|
'verdict',
|
||||||
|
'triage',
|
||||||
|
'oligarchie',
|
||||||
|
'peuple adulte',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
// ── Routes — first person: the path is a gesture, not a category ──
|
||||||
|
export const ROUTE_LABELS: Record<DecisionRoute, string> = {
|
||||||
|
solo: 'Je décide',
|
||||||
|
mandate: 'Je décide, sous mandat',
|
||||||
|
transmit: 'Je transmets',
|
||||||
|
advice: "J'écoute, puis je décide",
|
||||||
|
collective: 'Nous décidons',
|
||||||
|
record: 'Je le consigne',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Short pill forms (registry filters). */
|
||||||
|
export const ROUTE_SHORT: Record<DecisionRoute, string> = {
|
||||||
|
solo: 'seul',
|
||||||
|
mandate: 'sous mandat',
|
||||||
|
transmit: 'transmis',
|
||||||
|
advice: 'sur avis',
|
||||||
|
collective: 'ensemble',
|
||||||
|
record: 'consigné',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROUTE_ICONS: Record<DecisionRoute, string> = {
|
||||||
|
solo: 'i-lucide-zap',
|
||||||
|
mandate: 'i-lucide-key-round',
|
||||||
|
transmit: 'i-lucide-send',
|
||||||
|
advice: 'i-lucide-message-circle',
|
||||||
|
collective: 'i-lucide-users',
|
||||||
|
record: 'i-lucide-notebook-pen',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ICONS = {
|
||||||
|
urgent: 'i-lucide-siren',
|
||||||
|
alreadyDecided: 'i-lucide-book-open-check',
|
||||||
|
recurrence: 'i-lucide-repeat',
|
||||||
|
parametric: 'i-lucide-sliders-horizontal',
|
||||||
|
crystallize: 'i-lucide-stamp',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// ── Decision states — plain French pills ──
|
||||||
|
export const STATUS_LABELS: Record<DecisionStatus, string> = {
|
||||||
|
draft: 'préparation',
|
||||||
|
advice: 'avis',
|
||||||
|
objection: 'objection',
|
||||||
|
framing: 'formulation',
|
||||||
|
voting: 'vote',
|
||||||
|
adopted: 'en vigueur',
|
||||||
|
rejected: 'rejeté',
|
||||||
|
revoked: 'révoqué',
|
||||||
|
transmitted: 'transmis',
|
||||||
|
closed: 'clos',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Session display state (frozen lives on VoteSession, shown as a decision badge). */
|
||||||
|
export const FROZEN_LABEL = 'figé'
|
||||||
|
export const FROZEN_BANNER = 'Votes figés — en attente de cristallisation'
|
||||||
|
|
||||||
|
// ── Weight & reversibility — the tunnel chips ──
|
||||||
|
export const WEIGHT_LABELS: Record<Weight, string> = {
|
||||||
|
light: 'rien de lourd',
|
||||||
|
binding: 'du temps ou des ressources',
|
||||||
|
structural: 'la structure, les règles, du long terme',
|
||||||
|
}
|
||||||
|
export const REVERSIBILITY_LABELS: Record<Reversibility, string> = {
|
||||||
|
easy: 'facilement réversible',
|
||||||
|
costly: 'coûteux à défaire',
|
||||||
|
irreversible: 'irréversible',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Nuanced scale — never « CONTRE » ──
|
||||||
|
export const NUANCED_LABELS: Record<NuancedValue, string> = {
|
||||||
|
0: 'Pas du tout',
|
||||||
|
1: 'Plutôt non',
|
||||||
|
2: 'Réservé',
|
||||||
|
3: 'Plutôt oui',
|
||||||
|
4: 'Oui',
|
||||||
|
5: 'Tout à fait',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Vote methods ──
|
||||||
|
export const METHOD_LABELS: Record<VoteMethod, string> = {
|
||||||
|
consent: 'Consentement',
|
||||||
|
binary: 'Pour / contre',
|
||||||
|
nuanced: 'Vote nuancé',
|
||||||
|
parametric: 'Réglage collectif',
|
||||||
|
election: 'Élection',
|
||||||
|
}
|
||||||
|
export const BINARY_COST = 'deux camps, un perdant'
|
||||||
|
export const BINARY_DESCRIPTION = "l'outil de dernier recours des très grands corps"
|
||||||
|
|
||||||
|
// ── The review — l'épreuve du réel ──
|
||||||
|
export const REVIEW_TITLE = "L'épreuve du réel"
|
||||||
|
export const REVIEW_QUESTION = 'Le réel a-t-il suivi ?'
|
||||||
|
export const REVIEW_VERDICTS = {
|
||||||
|
confirmed: 'Ça tient',
|
||||||
|
revise: 'À revoir',
|
||||||
|
revoke: 'À révoquer',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// ── Windows, assent, perimeter ──
|
||||||
|
export const WINDOW_OK = 'Ça me va'
|
||||||
|
export const WINDOW_OBJECT = "J'objecte"
|
||||||
|
export const ASSENT_MISSING = 'Il manque un accord explicite'
|
||||||
|
export const BOUNDARY_SUSPENDED = 'compte à rebours suspendu — frontière contestée'
|
||||||
|
export const CONCERN_FIRST = 'Concerné·e en premier lieu'
|
||||||
|
export const CONCERN_SECOND = "Concerné·e en second lieu — s'est déclaré·e"
|
||||||
|
export const CONCERN_ME = 'Ça me concerne'
|
||||||
|
export const PERIMETER_OVERFLOW = "Le périmètre déborde — élargis d'un cran"
|
||||||
|
export const SCOPE_WIDEN = "Élargir d'un cran"
|
||||||
|
export const SCOPE_KEEP = 'Maintenir — en motivant publiquement'
|
||||||
|
|
||||||
|
// ── Parametric — le réglage collectif ──
|
||||||
|
export const PARAMETRIC_ALT = 'C\'est un réglage (montant, taux, répartition) — décidez au curseur'
|
||||||
|
export const PARAM_CONSTRAINT_LABEL = 'contrainte'
|
||||||
|
export const PARAM_DERIVED_LABEL = 'calculé'
|
||||||
|
export const IMPACT_CARD_TITLE = 'Pour moi'
|
||||||
|
export const IMPACT_DISCLAIMER = 'estimation sur données déclarées'
|
||||||
|
export const EXPLORE_LABEL = 'Explorer'
|
||||||
|
export const EXPLORE_DISCLAIMER = 'exploration — ne compte pas'
|
||||||
|
export const CRYSTALLIZE_ACTION = 'Cristalliser'
|
||||||
|
export const CRYSTALLIZE_CARD = 'Les votes sont figés — cristallise la médiane'
|
||||||
|
export const MEDIAN_EXPLANATION =
|
||||||
|
'Le collectif retient la médiane de chaque curseur — robuste aux extrêmes, '
|
||||||
|
+ "et c'est une position que chacun aurait pu proposer."
|
||||||
|
export const BIMODAL_BANNER = (paramLabel: string) =>
|
||||||
|
`Deux positions distinctes se dessinent sur ${paramLabel} — scinder la question ou changer de référentiel ?`
|
||||||
|
export const CRYSTALLIZE_CHOICES = ['Cristalliser quand même', 'Scinder', 'Reformuler'] as const
|
||||||
|
|
||||||
|
// ── Record & observatory ──
|
||||||
|
export const RECORD_ALT = 'Déjà tranché — je le consigne'
|
||||||
|
export const RECORD_HOW = "Comment ça s'est décidé ?"
|
||||||
|
export const OBSERVATORY_TITLE = "L'Observatoire"
|
||||||
|
export const OBSERVATORY_SUBTITLE = 'comment nous décidons, dans les faits'
|
||||||
|
export const REVOKED_SECTION = "Révoquées — ce qu'on en a appris"
|
||||||
|
export const MATURATION_CARD = 'Cette pratique est mûre — protocolise-la en quelques phrases'
|
||||||
|
|
||||||
|
// ── Élection ──
|
||||||
|
export const BLANK_VOTE = 'Vote blanc'
|
||||||
|
export const ELECTION_RULE =
|
||||||
|
"La personne la plus désignée est nommée ; en cas d'égalité, vous départagez — jamais l'outil."
|
||||||
|
|
||||||
|
// ── Urgence ──
|
||||||
|
export const URGENT_TOGGLE = "C'est urgent"
|
||||||
|
export const URGENT_BADGE = 'Décidé en urgence — le collectif ratifie'
|
||||||
|
export const URGENT_REFUSED = "Irréversible : l'urgence ne peut pas contourner le collectif."
|
||||||
|
|
||||||
|
// ── Fil / Aujourd'hui ──
|
||||||
|
export const FEED_HEADER = 'À toi de décider'
|
||||||
|
export const CAPTURE_PLACEHOLDER = "Qu'est-ce que tu décides ?"
|
||||||
|
export const DOSSIER_COMPLETE_CARD = 'Le dossier est complet — clore et publier la cartographie de clôture'
|
||||||
|
|
||||||
|
// ── Fiche / tunnel ──
|
||||||
|
export const PATH_CARD_TITLE = 'Le chemin'
|
||||||
|
export const CHOOSE_OTHERWISE = 'Je choisis autrement'
|
||||||
|
export const WHY_DISCLOSURE = 'Pourquoi ?'
|
||||||
|
export const ENGAGES_LABEL = 'Ce que ça engage'
|
||||||
|
export const BASELINE_PREFIX = "Aujourd'hui :"
|
||||||
|
export const BASELINE_ARROW = "Aujourd'hui → Proposé"
|
||||||
|
export const INSTRUCT_BLOCK = "S'instruire"
|
||||||
|
export const REOPEN_HANDLE = 'Remettre en question'
|
||||||
|
export const ADOPTED_STAMP = 'Décidé'
|
||||||
|
export const ADOPTED_TOAST = "C'est décidé — et révisable, comme tout ici."
|
||||||
|
export const VOTE_PRIVACY = "Ton vote est remplaçable — ton historique n'est visible que de toi."
|
||||||
|
export const WHO_VOTES = (n: number, date: string) =>
|
||||||
|
`Qui vote : ${n} personne${n > 1 ? 's' : ''} — liste arrêtée le ${date}`
|
||||||
|
|
||||||
|
// ── Mandats ──
|
||||||
|
export const MANDATES_TITLE = 'Mandats — les pouvoirs confiés'
|
||||||
|
export const MANDATE_EXERCISE = 'Exercice du mandat'
|
||||||
|
export const MANDATE_SPOTLIGHT = 'Feux de la rampe'
|
||||||
|
export const MANDATE_SPOTLIGHT_SUB = 'toutes les décisions prises sous ce mandat — contestables'
|
||||||
|
export const MANDATE_CLAIM = 'Réclamer un mandat'
|
||||||
|
export const MANDATE_REVOKE = 'Demander la révocation'
|
||||||
|
export const NOMINATION_LABELS: Record<NominationMethod, string> = {
|
||||||
|
'ratified-self': 'Auto-désignation ratifiée',
|
||||||
|
'election-no-candidate': 'Élection sans candidat',
|
||||||
|
'nuanced-vote': 'Vote nuancé',
|
||||||
|
consent: 'Consentement',
|
||||||
|
draw: 'Tirage au sort',
|
||||||
|
rotation: 'Rotation',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Textes / Pacte ──
|
||||||
|
export const PACT_BADGE = 'les règles du jeu'
|
||||||
|
export const PACT_SUBTITLE = 'Notre contrat social — sacralisé, jamais immuable'
|
||||||
|
export const INERTIA_LABELS: Record<InertiaPreset, string> = {
|
||||||
|
low: 'inertie faible',
|
||||||
|
standard: 'inertie standard',
|
||||||
|
high: 'inertie haute',
|
||||||
|
max: 'inertie maximale',
|
||||||
|
}
|
||||||
|
export const FIRST_DECISION = 'Adopter notre Pacte'
|
||||||
|
export const CREATE_SIGNATURE = 'Osons, nous verrons'
|
||||||
|
export const OPENING_DREAM = "Une démocratie qui n'a jamais existé — à votre échelle."
|
||||||
|
export const CLOSING_LINE = 'La ville change, le puits demeure.'
|
||||||
|
|
||||||
|
// ── Preuve / gravure / données ──
|
||||||
|
export const PROOF_LOCAL = 'empreinte locale — démo'
|
||||||
|
export const SECRET_DISPLAY = "secret d'affichage — démo"
|
||||||
|
export const LINEAGE_PREFIX = 'essaimé de'
|
||||||
|
export const WORKSHOP_MODE = 'mode atelier'
|
||||||
|
export const WORKSHOP_RECORDED = (by: string, forWhom: string, date: string) =>
|
||||||
|
`saisi par ${by} pour ${forWhom} — atelier du ${date}`
|
||||||
|
export const ATTRIBUTES_HINT =
|
||||||
|
'servent uniquement ta carte Pour moi, jamais montrés aux autres'
|
||||||
|
export const SYNC_NOTE =
|
||||||
|
'Un serveur pourra se brancher plus tard, tes données ne changent pas de forme.'
|
||||||
|
export const NO_PROTOCOL_BANNER = 'Aucun protocole — crée-le ou décide sur avis'
|
||||||
|
|
||||||
|
// ── Génération des phrases de seuil (sujet = le collectif, jamais la formule) ──
|
||||||
|
export const thresholdSentence = (W: number, T: number, threshold: number) =>
|
||||||
|
`${T.toLocaleString('fr-FR')} votant${T > 1 ? 's' : ''} sur ${W.toLocaleString('fr-FR')} : `
|
||||||
|
+ `à si faible participation, il faut presque l'unanimité — ${threshold.toLocaleString('fr-FR')} pour. `
|
||||||
|
+ 'Plus de monde vote, plus le seuil descend.'
|
||||||
@@ -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.
|
||||||
Generated
+325
-1
@@ -17,6 +17,7 @@
|
|||||||
"@unocss/nuxt": "^66.6.0",
|
"@unocss/nuxt": "^66.6.0",
|
||||||
"@vueuse/nuxt": "^14.2.1",
|
"@vueuse/nuxt": "^14.2.1",
|
||||||
"@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0",
|
"@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0",
|
||||||
|
"idb-keyval": "^6.3.0",
|
||||||
"nuxt": "^4.3.1",
|
"nuxt": "^4.3.1",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"vue": "^3.5.28",
|
"vue": "^3.5.28",
|
||||||
@@ -25,7 +26,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/lucide": "^1.2.91",
|
"@iconify-json/lucide": "^1.2.91",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"unocss": "^66.6.0"
|
"unocss": "^66.6.0",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@@ -7767,6 +7769,17 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -7776,6 +7789,13 @@
|
|||||||
"@types/ms": "*"
|
"@types/ms": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
@@ -8378,6 +8398,129 @@
|
|||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.1.0",
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"chai": "^6.2.2",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.21"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker/node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@volar/language-core": {
|
"node_modules/@volar/language-core": {
|
||||||
"version": "2.4.28",
|
"version": "2.4.28",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
||||||
@@ -9355,6 +9498,16 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ast-kit": {
|
"node_modules/ast-kit": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
|
||||||
@@ -9914,6 +10067,16 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/char-regex": {
|
"node_modules/char-regex": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
|
||||||
@@ -11254,6 +11417,16 @@
|
|||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
@@ -12271,6 +12444,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/idb-keyval": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
@@ -17300,6 +17479,13 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
@@ -17541,6 +17727,13 @@
|
|||||||
"node": ">=20.16.0"
|
"node": ">=20.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/standard-as-callback": {
|
"node_modules/standard-as-callback": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||||
@@ -17974,6 +18167,13 @@
|
|||||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||||
@@ -17999,6 +18199,16 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyrainbow": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-buffer": {
|
"node_modules/to-buffer": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
|
||||||
@@ -19838,6 +20048,103 @@
|
|||||||
"@esbuild/win32-x64": "0.27.3"
|
"@esbuild/win32-x64": "0.27.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "4.1.10",
|
||||||
|
"@vitest/mocker": "4.1.10",
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"@vitest/runner": "4.1.10",
|
||||||
|
"@vitest/snapshot": "4.1.10",
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"es-module-lexer": "^2.0.0",
|
||||||
|
"expect-type": "^1.3.0",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"std-env": "^4.0.0-rc.1",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^1.0.2",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"tinyrainbow": "^3.1.0",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "4.1.10",
|
||||||
|
"@vitest/browser-preview": "4.1.10",
|
||||||
|
"@vitest/browser-webdriverio": "4.1.10",
|
||||||
|
"@vitest/coverage-istanbul": "4.1.10",
|
||||||
|
"@vitest/coverage-v8": "4.1.10",
|
||||||
|
"@vitest/ui": "4.1.10",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest/node_modules/std-env": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vscode-uri": {
|
"node_modules/vscode-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||||
@@ -20107,6 +20414,23 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/content": "^3.11.2",
|
"@nuxt/content": "^3.11.2",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"@unocss/nuxt": "^66.6.0",
|
"@unocss/nuxt": "^66.6.0",
|
||||||
"@vueuse/nuxt": "^14.2.1",
|
"@vueuse/nuxt": "^14.2.1",
|
||||||
"@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0",
|
"@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0",
|
||||||
|
"idb-keyval": "^6.3.0",
|
||||||
"nuxt": "^4.3.1",
|
"nuxt": "^4.3.1",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"vue": "^3.5.28",
|
"vue": "^3.5.28",
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/lucide": "^1.2.91",
|
"@iconify-json/lucide": "^1.2.91",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"unocss": "^66.6.0"
|
"unocss": "^66.6.0",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Ported from backend/app/tests/test_mode_params.py — every pytest case
|
||||||
|
* exists here with the same expected values, plus tests for
|
||||||
|
* formatModeParams() (no Python counterpart: defined as the canonical
|
||||||
|
* inverse of parseModeParams, matching the seed DSL strings).
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { formatModeParams, parseModeParams } from '../../app/engine/modeParams'
|
||||||
|
|
||||||
|
describe('parseModeParams', () => {
|
||||||
|
it('D30M50B.1G.2 => standard Licence G1 params', () => {
|
||||||
|
const result = parseModeParams('D30M50B.1G.2')
|
||||||
|
expect(result.duration_days).toBe(30)
|
||||||
|
expect(result.majority_pct).toBe(50)
|
||||||
|
expect(result.base_exponent).toBe(0.1)
|
||||||
|
expect(result.gradient_exponent).toBe(0.2)
|
||||||
|
// Optional criteria absent
|
||||||
|
expect(result.smith_exponent).toBeNull()
|
||||||
|
expect(result.techcomm_exponent).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('D30M50B.1G.2S.1 => standard + smith_exponent=0.1', () => {
|
||||||
|
const result = parseModeParams('D30M50B.1G.2S.1')
|
||||||
|
expect(result.duration_days).toBe(30)
|
||||||
|
expect(result.majority_pct).toBe(50)
|
||||||
|
expect(result.base_exponent).toBe(0.1)
|
||||||
|
expect(result.gradient_exponent).toBe(0.2)
|
||||||
|
expect(result.smith_exponent).toBe(0.1)
|
||||||
|
expect(result.techcomm_exponent).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('D30M50B.1G.2T.1 => standard + techcomm_exponent=0.1', () => {
|
||||||
|
const result = parseModeParams('D30M50B.1G.2T.1')
|
||||||
|
expect(result.duration_days).toBe(30)
|
||||||
|
expect(result.majority_pct).toBe(50)
|
||||||
|
expect(result.base_exponent).toBe(0.1)
|
||||||
|
expect(result.gradient_exponent).toBe(0.2)
|
||||||
|
expect(result.smith_exponent).toBeNull()
|
||||||
|
expect(result.techcomm_exponent).toBe(0.1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('D30M50B1G.5C10 => integer base, gradient=0.5, constant=10', () => {
|
||||||
|
const result = parseModeParams('D30M50B1G.5C10')
|
||||||
|
expect(result.duration_days).toBe(30)
|
||||||
|
expect(result.majority_pct).toBe(50)
|
||||||
|
expect(result.base_exponent).toBe(1.0)
|
||||||
|
expect(result.gradient_exponent).toBe(0.5)
|
||||||
|
expect(result.constant_base).toBe(10.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('empty string returns all defaults', () => {
|
||||||
|
const result = parseModeParams('')
|
||||||
|
expect(result.duration_days).toBe(30)
|
||||||
|
expect(result.majority_pct).toBe(50)
|
||||||
|
expect(result.base_exponent).toBe(0.1)
|
||||||
|
expect(result.gradient_exponent).toBe(0.2)
|
||||||
|
expect(result.constant_base).toBe(0.0)
|
||||||
|
expect(result.smith_exponent).toBeNull()
|
||||||
|
expect(result.techcomm_exponent).toBeNull()
|
||||||
|
expect(result.ratio_multiplier).toBeNull()
|
||||||
|
expect(result.is_ratio_mode).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('whitespace-only string treated as empty', () => {
|
||||||
|
const result = parseModeParams(' ')
|
||||||
|
expect(result.duration_days).toBe(30)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('result exposes exactly the expected keys', () => {
|
||||||
|
const result = parseModeParams('D30M50B.1G.2')
|
||||||
|
const expectedKeys = [
|
||||||
|
'duration_days',
|
||||||
|
'majority_pct',
|
||||||
|
'base_exponent',
|
||||||
|
'gradient_exponent',
|
||||||
|
'constant_base',
|
||||||
|
'smith_exponent',
|
||||||
|
'techcomm_exponent',
|
||||||
|
'ratio_multiplier',
|
||||||
|
'is_ratio_mode',
|
||||||
|
].sort()
|
||||||
|
expect(Object.keys(result).sort()).toEqual(expectedKeys)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('unknown code letter throws (Python parity)', () => {
|
||||||
|
expect(() => parseModeParams('D30X5')).toThrow(/inconnu/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('D7R1N2.5 => ratio mode with multiplier (Python cross-check)', () => {
|
||||||
|
const result = parseModeParams('D7R1N2.5')
|
||||||
|
expect(result.duration_days).toBe(7)
|
||||||
|
expect(result.is_ratio_mode).toBe(true)
|
||||||
|
expect(result.ratio_multiplier).toBe(2.5)
|
||||||
|
// Untouched defaults
|
||||||
|
expect(result.majority_pct).toBe(50)
|
||||||
|
expect(result.base_exponent).toBe(0.1)
|
||||||
|
expect(result.gradient_exponent).toBe(0.2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatModeParams', () => {
|
||||||
|
it('defaults format to the canonical standard string', () => {
|
||||||
|
expect(formatModeParams()).toBe('D30M50B.1G.2')
|
||||||
|
expect(formatModeParams({})).toBe('D30M50B.1G.2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('round-trips the seed DSL strings', () => {
|
||||||
|
for (const s of ['D30M50B.1G.2', 'D30M50B.1G.2S.1', 'D30M50B.1G.2T.1']) {
|
||||||
|
expect(formatModeParams(parseModeParams(s))).toBe(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats integer base and constant', () => {
|
||||||
|
expect(formatModeParams(parseModeParams('D30M50B1G.5C10'))).toBe('D30M50B1G.5C10')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats ratio mode and multiplier', () => {
|
||||||
|
expect(formatModeParams(parseModeParams('D7R1N2.5'))).toBe('D7M50B.1G.2N2.5R1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parse(format(p)) is identical to p', () => {
|
||||||
|
for (const s of ['D30M50B.1G.2S.1', 'D30M50B1G.5C10', 'D7R1N2.5', 'D90M66B.2G.3S.2T.1']) {
|
||||||
|
const p = parseModeParams(s)
|
||||||
|
expect(parseModeParams(formatModeParams(p))).toEqual(p)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Ported from backend/app/tests/test_nuanced.py — every pytest case
|
||||||
|
* exists here with the same expected values.
|
||||||
|
*
|
||||||
|
* Levels: 0-CONTRE, 1-PAS DU TOUT, 2-PAS D'ACCORD, 3-NEUTRE, 4-D'ACCORD, 5-TOUT A FAIT
|
||||||
|
* Positive = levels 3 + 4 + 5
|
||||||
|
* Adoption requires: positive_pct >= threshold (80%) AND total >= min_participants (59).
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { nuancedResult } from '../../app/engine/nuanced'
|
||||||
|
|
||||||
|
/** Build a votes array from level repetitions, e.g. fill([5, 20], [4, 20]). */
|
||||||
|
function fill(...groups: Array<[level: number, count: number]>): number[] {
|
||||||
|
const votes: number[] = []
|
||||||
|
for (const [level, count] of groups) {
|
||||||
|
for (let i = 0; i < count; i++) votes.push(level)
|
||||||
|
}
|
||||||
|
return votes
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('nuancedResult — adoption', () => {
|
||||||
|
it('59 positive + 10 negative = 69 total => adopted (85.51% >= 80%)', () => {
|
||||||
|
const votes = fill([5, 20], [4, 20], [3, 19], [2, 5], [1, 3], [0, 2])
|
||||||
|
const result = nuancedResult(votes, 80, 59)
|
||||||
|
|
||||||
|
expect(result.total).toBe(69)
|
||||||
|
expect(result.positive_count).toBe(59)
|
||||||
|
expect(result.positive_pct).toBeCloseTo(85.51, 1)
|
||||||
|
// Python cross-check: round(59/69*100, 2) == 85.51
|
||||||
|
expect(result.positive_pct).toBe(85.51)
|
||||||
|
expect(result.threshold_met).toBe(true)
|
||||||
|
expect(result.min_participants_met).toBe(true)
|
||||||
|
expect(result.adopted).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('all 59 voters at level 5 => 100% positive, adopted', () => {
|
||||||
|
const votes = fill([5, 59])
|
||||||
|
const result = nuancedResult(votes, 80, 59)
|
||||||
|
|
||||||
|
expect(result.total).toBe(59)
|
||||||
|
expect(result.positive_count).toBe(59)
|
||||||
|
expect(result.positive_pct).toBe(100.0)
|
||||||
|
expect(result.adopted).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('nuancedResult — rejection', () => {
|
||||||
|
it('40 positive + 30 negative = 70 total => threshold not met (57.14% < 80%)', () => {
|
||||||
|
const votes = fill([5, 15], [4, 15], [3, 10], [2, 10], [1, 10], [0, 10])
|
||||||
|
const result = nuancedResult(votes, 80, 59)
|
||||||
|
|
||||||
|
expect(result.total).toBe(70)
|
||||||
|
expect(result.positive_count).toBe(40)
|
||||||
|
expect(result.positive_pct).toBeCloseTo(57.14, 1)
|
||||||
|
expect(result.threshold_met).toBe(false)
|
||||||
|
expect(result.min_participants_met).toBe(true) // 70 >= 59
|
||||||
|
expect(result.adopted).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('55 total < 59 min participants => rejected despite 90.9% positive', () => {
|
||||||
|
const votes = fill([5, 30], [4, 10], [3, 10], [1, 3], [0, 2])
|
||||||
|
const result = nuancedResult(votes, 80, 59)
|
||||||
|
|
||||||
|
expect(result.total).toBe(55)
|
||||||
|
expect(result.positive_count).toBe(50)
|
||||||
|
expect(result.positive_pct).toBeGreaterThan(80)
|
||||||
|
expect(result.threshold_met).toBe(true)
|
||||||
|
expect(result.min_participants_met).toBe(false)
|
||||||
|
expect(result.adopted).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('nuancedResult — edge cases', () => {
|
||||||
|
it('exactly 80% positive votes passes the threshold', () => {
|
||||||
|
// 80 positive out of 100 = exactly 80%
|
||||||
|
const votes = fill([5, 40], [4, 20], [3, 20], [2, 10], [1, 5], [0, 5])
|
||||||
|
const result = nuancedResult(votes, 80, 59)
|
||||||
|
|
||||||
|
expect(result.total).toBe(100)
|
||||||
|
expect(result.positive_count).toBe(80)
|
||||||
|
expect(result.positive_pct).toBe(80.0)
|
||||||
|
expect(result.threshold_met).toBe(true)
|
||||||
|
expect(result.min_participants_met).toBe(true)
|
||||||
|
expect(result.adopted).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('79 positive out of 100 = 79% < 80% => rejected', () => {
|
||||||
|
const votes = fill([5, 39], [4, 20], [3, 20], [2, 11], [1, 5], [0, 5])
|
||||||
|
const result = nuancedResult(votes, 80, 59)
|
||||||
|
|
||||||
|
expect(result.total).toBe(100)
|
||||||
|
expect(result.positive_count).toBe(79)
|
||||||
|
expect(result.positive_pct).toBe(79.0)
|
||||||
|
expect(result.threshold_met).toBe(false)
|
||||||
|
expect(result.adopted).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('zero votes => not adopted', () => {
|
||||||
|
const result = nuancedResult([], 80, 59)
|
||||||
|
expect(result.total).toBe(0)
|
||||||
|
expect(result.positive_count).toBe(0)
|
||||||
|
expect(result.positive_pct).toBe(0.0)
|
||||||
|
expect(result.adopted).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vote level outside 0-5 throws', () => {
|
||||||
|
expect(() => nuancedResult([5, 3, 6])).toThrow(/invalide/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('per-level breakdown is correct', () => {
|
||||||
|
const votes = [0, 1, 2, 3, 4, 5, 5, 4, 3]
|
||||||
|
const result = nuancedResult(votes, 50, 1)
|
||||||
|
|
||||||
|
expect(result.per_level_counts).toEqual({ 0: 1, 1: 1, 2: 1, 3: 2, 4: 2, 5: 2 })
|
||||||
|
expect(result.positive_count).toBe(6) // 2+2+2
|
||||||
|
expect(result.total).toBe(9)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* Ported from backend/app/tests/test_threshold.py — every pytest case
|
||||||
|
* exists here with the same expected values. Exact integer expectations
|
||||||
|
* were cross-checked by executing the Python engine (backend/.venv).
|
||||||
|
*
|
||||||
|
* Real-world reference case:
|
||||||
|
* Vote Engagement Forgeron v2.0.0 (Feb 2026)
|
||||||
|
* wotSize=7224, votesFor=97, votesAgainst=23, total=120
|
||||||
|
* params M=50, B=0.1, G=0.2 => threshold=94 => adopted (97 >= 94)
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { smithThreshold, techcommThreshold, wotThreshold } from '../../app/engine/threshold'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WoT threshold: real-world vote Forgeron
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('wotThreshold — Forgeron reference case', () => {
|
||||||
|
it('forgeron vote passes (97 for out of 120 total, wot=7224)', () => {
|
||||||
|
const threshold = wotThreshold(7224, 120, 50, 0.1, 0.2)
|
||||||
|
// With low participation (120/7224 ~ 1.66%), near-unanimity is required.
|
||||||
|
// The historical threshold was 94, and 97 >= 94.
|
||||||
|
expect(97).toBeGreaterThanOrEqual(threshold)
|
||||||
|
// The threshold should be high relative to total votes (inertia effect)
|
||||||
|
expect(threshold).toBeGreaterThan(60)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forgeron threshold value is in a reasonable range', () => {
|
||||||
|
const threshold = wotThreshold(7224, 120, 50, 0.1, 0.2)
|
||||||
|
// At ~1.66% participation, inertia should push threshold close to 78-95%
|
||||||
|
// of total votes.
|
||||||
|
expect(threshold).toBeGreaterThanOrEqual(80)
|
||||||
|
expect(threshold).toBeLessThanOrEqual(120)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forgeron threshold is exactly 94 (Python cross-check)', () => {
|
||||||
|
expect(wotThreshold(7224, 120, 50, 0.1, 0.2)).toBe(94)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WoT threshold: low participation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('wotThreshold — low participation', () => {
|
||||||
|
it('10 votes out of 7224 requires near-unanimity', () => {
|
||||||
|
const threshold = wotThreshold(7224, 10, 50, 0.1, 0.2)
|
||||||
|
// With participation ratio 10/7224 ~ 0.14%, threshold should be
|
||||||
|
// very close to totalVotes (near-unanimity).
|
||||||
|
expect(threshold).toBeGreaterThanOrEqual(9)
|
||||||
|
expect(threshold).toBeLessThanOrEqual(10)
|
||||||
|
// Python cross-check: exact value
|
||||||
|
expect(threshold).toBe(9)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WoT threshold: high participation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('wotThreshold — high participation', () => {
|
||||||
|
it('3000 votes out of 7224 approaches simple majority', () => {
|
||||||
|
const threshold = wotThreshold(7224, 3000, 50, 0.1, 0.2)
|
||||||
|
// With ~42% participation, the inertia factor diminishes.
|
||||||
|
const simpleMajority = Math.ceil(3000 * 0.5)
|
||||||
|
expect(threshold).toBeGreaterThanOrEqual(simpleMajority)
|
||||||
|
// Should be noticeably less than near-unanimity
|
||||||
|
expect(threshold).toBeLessThan(2700)
|
||||||
|
// Python cross-check: exact value
|
||||||
|
expect(threshold).toBe(1742)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WoT threshold: edge cases
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('wotThreshold — edge cases', () => {
|
||||||
|
it('zero total votes => ceil(C + B^W)', () => {
|
||||||
|
const threshold = wotThreshold(7224, 0, 50, 0.1, 0.2)
|
||||||
|
// B^W = 0.1^7224 is effectively 0
|
||||||
|
const expected = Math.ceil(0.0 + 0.1 ** 7224)
|
||||||
|
expect(threshold).toBe(expected)
|
||||||
|
expect(threshold).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on wotSize = 0', () => {
|
||||||
|
expect(() => wotThreshold(0, 10)).toThrow(/wotSize/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on negative totalVotes', () => {
|
||||||
|
expect(() => wotThreshold(100, -1)).toThrow(/totalVotes/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on majorityPct out of range', () => {
|
||||||
|
expect(() => wotThreshold(100, 10, 150)).toThrow(/majorityPct/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WoT threshold: Python↔TS parity (values executed from backend/.venv)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('wotThreshold — Python parity cases', () => {
|
||||||
|
it('T > W (100 wot, 150 votes) => 69', () => {
|
||||||
|
expect(wotThreshold(100, 150, 50, 0.1, 0.2)).toBe(69)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('T = W (full participation) => simple majority 50', () => {
|
||||||
|
expect(wotThreshold(100, 100, 50, 0.1, 0.2)).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('constantBase C=10 on Forgeron numbers => 96', () => {
|
||||||
|
expect(wotThreshold(7224, 120, 50, 0.1, 0.2, 10.0)).toBe(96)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('majorityPct 80 on Forgeron numbers => 110', () => {
|
||||||
|
expect(wotThreshold(7224, 120, 80, 0.1, 0.2)).toBe(110)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('minimal corpus W=1, T=1 => 1', () => {
|
||||||
|
expect(wotThreshold(1, 1, 50, 0.1, 0.2)).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('W=10, T=5 => 3', () => {
|
||||||
|
expect(wotThreshold(10, 5, 50, 0.1, 0.2)).toBe(3)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Smith threshold
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('smithThreshold — ceil(smithSize ^ S)', () => {
|
||||||
|
it('smithSize=20, exponent=0.1 => ceil(20^0.1) = 2', () => {
|
||||||
|
const result = smithThreshold(20, 0.1)
|
||||||
|
expect(result).toBe(Math.ceil(20 ** 0.1))
|
||||||
|
// 20^0.1 ~ 1.35, ceil => 2
|
||||||
|
expect(result).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('smithSize=1 => ceil(1^0.1) = 1', () => {
|
||||||
|
expect(smithThreshold(1, 0.1)).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on smithSize = 0', () => {
|
||||||
|
expect(() => smithThreshold(0)).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('smithSize=1000, exponent=0.5 => 32 (Python cross-check)', () => {
|
||||||
|
expect(smithThreshold(1000, 0.5)).toBe(32)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TechComm threshold
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('techcommThreshold — ceil(cotecSize ^ T)', () => {
|
||||||
|
it('cotecSize=5, exponent=0.1 => ceil(5^0.1) = 2', () => {
|
||||||
|
const result = techcommThreshold(5, 0.1)
|
||||||
|
expect(result).toBe(Math.ceil(5 ** 0.1))
|
||||||
|
// 5^0.1 ~ 1.175, ceil => 2
|
||||||
|
expect(result).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cotecSize=1 => 1', () => {
|
||||||
|
expect(techcommThreshold(1, 0.1)).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on cotecSize = 0', () => {
|
||||||
|
expect(() => techcommThreshold(0)).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~': fileURLToPath(new URL('./app', import.meta.url)),
|
||||||
|
'@': fileURLToPath(new URL('./app', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
include: ['tests/**/*.spec.ts'],
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user