Phase 1 : socle Python, schéma SQLite + FTS5, modèles pydantic
- pyproject/uv sur Python 3.12, dépendances épinglées - migrations versionnées (PRAGMA user_version) ; 001_initial pose textes, sources, evenements, decisions_cc, insights, veille_log, veille_changements - index FTS5 à contenu externe, tokenizer unicode61 remove_diacritics 2 : « chlordecone » retrouve « chlordécone », highlight() disponible - vue v_textes : devant_cc combine le statut et les affaires CC en instance, ce qui réconcilie le §7 (Riposte reste adoptee_non_promulguee) et le §8.7 (la facette saisie_cc doit remonter ≥ 7 textes) - modèles pydantic refusant les incohérences de statut : promulguée sans numéro, navette avec numéro officiel, slug malformé - Makefile, .env.example, journalisation structlog Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3ce7d17db9
commit
31d3935bce
@@ -0,0 +1,276 @@
|
||||
-- ============================================================================
|
||||
-- Migration 001 — schéma initial de la veille législative
|
||||
--
|
||||
-- Sémantique conforme au §4 du prompt de mission. Les libellés d'énumération
|
||||
-- sont ceux du corpus de recherche : ils ne sont pas traduits ni normalisés,
|
||||
-- de façon à ce qu'une valeur en base soit toujours retrouvable dans un
|
||||
-- fichier de `data/input/`.
|
||||
-- ============================================================================
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Table principale : un texte législatif ou une décision juridictionnelle
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE textes (
|
||||
-- Identifiant lisible et stable, servant aussi d'URL : « loi-2026-491 ».
|
||||
id TEXT PRIMARY KEY,
|
||||
|
||||
-- Numéro officiel une fois attribué (« 2026-491 »). NULL tant que le texte
|
||||
-- n'est pas promulgué : c'est précisément ce qui distingue un texte adopté
|
||||
-- d'un texte en vigueur.
|
||||
numero_officiel TEXT,
|
||||
|
||||
type TEXT NOT NULL CHECK (type IN (
|
||||
'loi', 'loi_organique', 'pjl', 'ppl', 'ordonnance',
|
||||
'decision_cc', 'decret', 'accord_international')),
|
||||
|
||||
titre_court TEXT NOT NULL,
|
||||
titre_officiel TEXT,
|
||||
|
||||
-- Règle d'or du rapport : le statut prime sur l'intitulé.
|
||||
statut TEXT NOT NULL CHECK (statut IN (
|
||||
'promulguee', 'adoptee_non_promulguee', 'saisie_cc',
|
||||
'validee_cc', 'censuree_partiellement', 'navette',
|
||||
'deposee_non_examinee', 'annoncee', 'rejetee')),
|
||||
statut_date TEXT, -- date du dernier changement de statut (ISO 8601)
|
||||
|
||||
date_depot TEXT,
|
||||
date_adoption TEXT,
|
||||
date_promulgation TEXT,
|
||||
date_entree_vigueur TEXT,
|
||||
prochaine_echeance TEXT,
|
||||
prochaine_echeance_label TEXT,
|
||||
|
||||
-- Tableaux JSON. SQLite les stocke en texte ; les fonctions json_*
|
||||
-- permettent de filtrer sans table de jointure supplémentaire.
|
||||
themes TEXT NOT NULL DEFAULT '[]',
|
||||
|
||||
-- Objet JSON : { "individus": {"sens": "negatif", "note": "…"}, … }
|
||||
-- Sens admis : positif | negatif | mixte | neutre.
|
||||
impacts TEXT NOT NULL DEFAULT '{}',
|
||||
|
||||
guadeloupe_pertinence TEXT CHECK (guadeloupe_pertinence IN ('forte', 'moyenne', 'faible')),
|
||||
guadeloupe_note TEXT,
|
||||
|
||||
resume TEXT,
|
||||
points_cles TEXT NOT NULL DEFAULT '[]',
|
||||
|
||||
confiance TEXT NOT NULL DEFAULT 'medium'
|
||||
CHECK (confiance IN ('high', 'medium', 'low')),
|
||||
|
||||
-- Fichier de `data/input/` dont provient l'entrée, ou nom du collecteur.
|
||||
source_seed TEXT,
|
||||
|
||||
-- Drapeau de revue humaine : toute classification automatique le lève.
|
||||
a_verifier INTEGER NOT NULL DEFAULT 0 CHECK (a_verifier IN (0, 1)),
|
||||
motif_verification TEXT,
|
||||
|
||||
derniere_verif TEXT,
|
||||
cree_le TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
maj_le TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_textes_numero ON textes (numero_officiel)
|
||||
WHERE numero_officiel IS NOT NULL;
|
||||
CREATE INDEX idx_textes_statut ON textes (statut);
|
||||
CREATE INDEX idx_textes_type ON textes (type);
|
||||
CREATE INDEX idx_textes_gpe ON textes (guadeloupe_pertinence);
|
||||
CREATE INDEX idx_textes_promulgation ON textes (date_promulgation);
|
||||
CREATE INDEX idx_textes_echeance ON textes (prochaine_echeance);
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Sources : une citation vérifiable par fait important
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE sources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
texte_id TEXT NOT NULL REFERENCES textes (id) ON DELETE CASCADE,
|
||||
|
||||
url TEXT NOT NULL,
|
||||
titre TEXT,
|
||||
editeur TEXT,
|
||||
date_publication TEXT,
|
||||
|
||||
-- T1 = source primaire (Légifrance/JO, AN, Sénat, Conseil constitutionnel,
|
||||
-- Conseil d'État, vie-publique, préfectures) ; T2 = presse et analyses.
|
||||
tier TEXT NOT NULL DEFAULT 'T2' CHECK (tier IN ('T1', 'T2')),
|
||||
|
||||
extrait_verbatim TEXT,
|
||||
contexte TEXT,
|
||||
confiance TEXT CHECK (confiance IN ('high', 'medium', 'low')),
|
||||
|
||||
-- Marqueur d'origine dans le corpus : « dim07-24 », « phase5-3.1 », « ref-481 ».
|
||||
marqueur TEXT,
|
||||
fichier_origine TEXT,
|
||||
|
||||
cree_le TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sources_texte ON sources (texte_id);
|
||||
CREATE UNIQUE INDEX idx_sources_unicite ON sources (texte_id, url, COALESCE(marqueur, ''));
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Événements : la timeline parlementaire d'un texte
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE evenements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
texte_id TEXT NOT NULL REFERENCES textes (id) ON DELETE CASCADE,
|
||||
|
||||
date_evenement TEXT NOT NULL,
|
||||
type_etape TEXT NOT NULL CHECK (type_etape IN (
|
||||
'depot', 'adoption_1re_lecture', 'adoption_definitive',
|
||||
'commission_mixte_paritaire', 'transmission', 'saisine_cc',
|
||||
'decision_cc', 'promulgation', 'publication_jo',
|
||||
'entree_vigueur', 'rejet', 'annonce', 'autre')),
|
||||
description TEXT NOT NULL,
|
||||
source_url TEXT,
|
||||
|
||||
-- Vrai lorsque l'événement est postérieur à la date d'arrêté des données :
|
||||
-- il s'agit alors d'une échéance prévisionnelle, pas d'un fait acquis.
|
||||
previsionnel INTEGER NOT NULL DEFAULT 0 CHECK (previsionnel IN (0, 1)),
|
||||
|
||||
cree_le TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_evenements_texte ON evenements (texte_id, date_evenement);
|
||||
CREATE INDEX idx_evenements_date ON evenements (date_evenement);
|
||||
CREATE UNIQUE INDEX idx_evenements_unicite
|
||||
ON evenements (texte_id, date_evenement, type_etape, description);
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Décisions du Conseil constitutionnel
|
||||
--
|
||||
-- `date_decision IS NULL` signifie « affaire en instance » : c'est ce qui
|
||||
-- alimente la facette « saisi du Conseil constitutionnel » de l'interface.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE decisions_cc (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
texte_id TEXT REFERENCES textes (id) ON DELETE SET NULL,
|
||||
|
||||
numero_affaire TEXT NOT NULL UNIQUE, -- « 2026-915 DC »
|
||||
date_saisine TEXT,
|
||||
date_decision TEXT,
|
||||
resultat TEXT CHECK (resultat IN (
|
||||
'conforme', 'conforme_avec_reserves', 'non_conformite_partielle',
|
||||
'non_conformite_totale', 'en_instance')),
|
||||
saisissants TEXT,
|
||||
resume TEXT,
|
||||
url TEXT,
|
||||
|
||||
date_decision_attendue TEXT, -- prévisionnel, tant que la décision n'est pas rendue
|
||||
cree_le TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
maj_le TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cc_texte ON decisions_cc (texte_id);
|
||||
CREATE INDEX idx_cc_instance ON decisions_cc (date_decision);
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Analyses transversales issues de `lois-2026_insight.md`
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE insights (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
numero INTEGER NOT NULL UNIQUE,
|
||||
titre TEXT NOT NULL,
|
||||
corps TEXT NOT NULL,
|
||||
implications TEXT,
|
||||
confiance TEXT NOT NULL DEFAULT 'medium',
|
||||
derive_de TEXT NOT NULL DEFAULT '[]', -- JSON : ["dim02", "dim03", …]
|
||||
fichier_origine TEXT
|
||||
);
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Journal des exécutions du pipeline
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE veille_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
horodatage TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
mode TEXT NOT NULL CHECK (mode IN ('seed', 'update', 'dry-run')),
|
||||
duree_s REAL,
|
||||
ajouts INTEGER NOT NULL DEFAULT 0,
|
||||
modifications INTEGER NOT NULL DEFAULT 0,
|
||||
echecs INTEGER NOT NULL DEFAULT 0,
|
||||
alertes TEXT NOT NULL DEFAULT '[]', -- JSON
|
||||
rapport TEXT -- compte-rendu lisible
|
||||
);
|
||||
|
||||
CREATE INDEX idx_veille_log_date ON veille_log (horodatage DESC);
|
||||
|
||||
-- Détail des changements d'un run, pour l'encart « derniers changements ».
|
||||
CREATE TABLE veille_changements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_id INTEGER NOT NULL REFERENCES veille_log (id) ON DELETE CASCADE,
|
||||
texte_id TEXT,
|
||||
nature TEXT NOT NULL CHECK (nature IN ('ajout', 'statut', 'date', 'source', 'autre')),
|
||||
champ TEXT,
|
||||
ancienne_valeur TEXT,
|
||||
nouvelle_valeur TEXT,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_changements_run ON veille_changements (run_id);
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Recherche plein texte
|
||||
--
|
||||
-- Table FTS5 à contenu externe : l'index ne duplique pas les données, il est
|
||||
-- tenu à jour par les déclencheurs ci-dessous. `remove_diacritics 2` permet de
|
||||
-- trouver « chlordécone » en tapant « chlordecone ».
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE VIRTUAL TABLE textes_fts USING fts5 (
|
||||
titre_court,
|
||||
titre_officiel,
|
||||
resume,
|
||||
points_cles,
|
||||
guadeloupe_note,
|
||||
numero_officiel,
|
||||
content = 'textes',
|
||||
content_rowid = 'rowid',
|
||||
tokenize = "unicode61 remove_diacritics 2"
|
||||
);
|
||||
|
||||
CREATE TRIGGER textes_fts_ai AFTER INSERT ON textes BEGIN
|
||||
INSERT INTO textes_fts (rowid, titre_court, titre_officiel, resume, points_cles,
|
||||
guadeloupe_note, numero_officiel)
|
||||
VALUES (new.rowid, new.titre_court, new.titre_officiel, new.resume, new.points_cles,
|
||||
new.guadeloupe_note, new.numero_officiel);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER textes_fts_ad AFTER DELETE ON textes BEGIN
|
||||
INSERT INTO textes_fts (textes_fts, rowid, titre_court, titre_officiel, resume,
|
||||
points_cles, guadeloupe_note, numero_officiel)
|
||||
VALUES ('delete', old.rowid, old.titre_court, old.titre_officiel, old.resume,
|
||||
old.points_cles, old.guadeloupe_note, old.numero_officiel);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER textes_fts_au AFTER UPDATE ON textes BEGIN
|
||||
INSERT INTO textes_fts (textes_fts, rowid, titre_court, titre_officiel, resume,
|
||||
points_cles, guadeloupe_note, numero_officiel)
|
||||
VALUES ('delete', old.rowid, old.titre_court, old.titre_officiel, old.resume,
|
||||
old.points_cles, old.guadeloupe_note, old.numero_officiel);
|
||||
INSERT INTO textes_fts (rowid, titre_court, titre_officiel, resume, points_cles,
|
||||
guadeloupe_note, numero_officiel)
|
||||
VALUES (new.rowid, new.titre_court, new.titre_officiel, new.resume, new.points_cles,
|
||||
new.guadeloupe_note, new.numero_officiel);
|
||||
END;
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Vue de consultation : ajoute les informations dérivées dont l'interface a
|
||||
-- besoin sans dupliquer d'état en base.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
CREATE VIEW v_textes AS
|
||||
SELECT
|
||||
t.*,
|
||||
(SELECT COUNT(*) FROM sources s WHERE s.texte_id = t.id) AS nb_sources,
|
||||
(SELECT COUNT(*) FROM evenements e WHERE e.texte_id = t.id) AS nb_evenements,
|
||||
-- Un texte est « devant le Conseil constitutionnel » soit par son statut,
|
||||
-- soit parce qu'une affaire le concernant est en instance. Les deux voies
|
||||
-- sont nécessaires : le §7 du prompt impose que la loi « Riposte » garde le
|
||||
-- statut `adoptee_non_promulguee`, alors qu'elle doit apparaître dans la
|
||||
-- facette « saisi du Conseil constitutionnel ».
|
||||
CASE WHEN t.statut = 'saisie_cc'
|
||||
OR EXISTS (SELECT 1 FROM decisions_cc d
|
||||
WHERE d.texte_id = t.id AND d.date_decision IS NULL)
|
||||
THEN 1 ELSE 0 END AS devant_cc
|
||||
FROM textes t;
|
||||
Reference in New Issue
Block a user