mirror of
https://github.com/deunix-educ/aventuriers-des-mots.git
synced 2026-10-13 02:11:58 +02:00
Add JSON schemas, data validator, and first CE1 content
Introduces a generic JSON-Schema-subset validator in GDScript (scripts/data/schema_validator.gd) used both by `make validate-data` and, later, by ExerciseEngine at load time, plus semantic_checks.gd for cross-field constraints a schema can't express (answer index bounds, at least one faulty token in sentence correction, word count matching between a dictation's target_text and its annotations). Schemas cover rule, exercise (dispatched per type: multiple_choice, sentence_correction), dictation, curriculum, and save (save.schema.json is not yet wired into validate-data; saves live in user://, handled at the Profiles/Saves step). First draft content for the CE1 "accord_adjectif" rule (one rule, two multiple-choice exercises, one sentence correction, one dictation, one curriculum entry) exercises the full pipeline end to end and seeds docs/review/content-review.md, regenerated by the new tools/generate_content_review.py (`make content-review`). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ Le `Makefile` est le seul point d'entrée pour les opérations système. Tenir c
|
||||
- `make validate-data` : valide tous les JSON de `data/` contre leurs schémas
|
||||
- `make test` : tests en mode headless
|
||||
- `make check` : lint + validate-data + test (à passer avant chaque commit)
|
||||
- `make content-review` : régénère `docs/review/content-review.md` à partir de `data/`
|
||||
- `make export-linux` / `make export-windows` : exports dans `build/`
|
||||
|
||||
## Conventions de code
|
||||
|
||||
@@ -2,9 +2,11 @@ GODOT_BIN := .godot-bin/godot
|
||||
VENV := .venv
|
||||
GDLINT := $(VENV)/bin/gdlint
|
||||
GDFORMAT := $(VENV)/bin/gdformat
|
||||
PYRIGHT := $(VENV)/bin/pyright
|
||||
PYTHON := $(VENV)/bin/python3
|
||||
GD_SOURCES := autoload scripts tests
|
||||
|
||||
.PHONY: setup run lint format validate-data test check export-linux export-windows
|
||||
.PHONY: setup run lint format validate-data test check content-review export-linux export-windows
|
||||
|
||||
setup:
|
||||
tools/setup.sh
|
||||
@@ -14,6 +16,7 @@ run:
|
||||
|
||||
lint:
|
||||
$(GDLINT) $(GD_SOURCES)
|
||||
$(PYRIGHT) tools/
|
||||
|
||||
format:
|
||||
$(GDFORMAT) $(GD_SOURCES)
|
||||
@@ -22,10 +25,13 @@ validate-data:
|
||||
$(GODOT_BIN) --headless --path . -s res://scripts/data/validate_data_cli.gd
|
||||
|
||||
test:
|
||||
$(GODOT_BIN) --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -gexit
|
||||
$(GODOT_BIN) --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit
|
||||
|
||||
check: lint validate-data test
|
||||
|
||||
content-review:
|
||||
$(PYTHON) tools/generate_content_review.py
|
||||
|
||||
export-linux:
|
||||
@echo "Export Linux : pas encore configure (voir Ordre de construction, etape 6)."
|
||||
@exit 1
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"level": "CE1",
|
||||
"region_id": "foret_des_accords",
|
||||
"chapter_id": "accords_groupe_nominal",
|
||||
"title": "Les accords dans le groupe nominal",
|
||||
"rule_ids": ["regle_accord_adjectif"],
|
||||
"review_status": "draft"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "ce1_accord_adjectif_dictee_01",
|
||||
"level": "CE1",
|
||||
"domain": "grammaire",
|
||||
"rule_ids": ["regle_accord_adjectif"],
|
||||
"difficulty": 1,
|
||||
"target_text": "Les petits chats dorment.",
|
||||
"words": [
|
||||
{ "text": "Les", "expects_capital": true },
|
||||
{ "text": "petits", "rule_id": "regle_accord_adjectif" },
|
||||
{ "text": "chats" },
|
||||
{ "text": "dorment." }
|
||||
],
|
||||
"audio_id": "ce1_accord_adjectif_dictee_01",
|
||||
"audio_provisional": true,
|
||||
"accepted_variants": [],
|
||||
"review_status": "draft"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "ce1_accord_adjectif_correction_01",
|
||||
"level": "CE1",
|
||||
"domain": "grammaire",
|
||||
"skill": "accord_groupe_nominal",
|
||||
"rule_id": "regle_accord_adjectif",
|
||||
"difficulty": 2,
|
||||
"type": "sentence_correction",
|
||||
"instruction": "Touche le mot qui contient une erreur, puis corrige-le.",
|
||||
"tokens": [
|
||||
{ "text": "Les", "correct": true },
|
||||
{
|
||||
"text": "petit",
|
||||
"correct": false,
|
||||
"expected": "petits",
|
||||
"rule_id": "regle_accord_adjectif"
|
||||
},
|
||||
{ "text": "chiens", "correct": true },
|
||||
{ "text": "courent.", "correct": true }
|
||||
],
|
||||
"hint": "Le determinant « les » indique un pluriel.",
|
||||
"explanation": "L'adjectif « petit » s'accorde avec le nom « chiens », qui est au pluriel : il faut ecrire « petits ».",
|
||||
"review_tags": ["pluriel", "accord_adjectif"],
|
||||
"review_status": "draft"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "ce1_accord_adjectif_qcm_01",
|
||||
"level": "CE1",
|
||||
"domain": "grammaire",
|
||||
"skill": "accord_groupe_nominal",
|
||||
"rule_id": "regle_accord_adjectif",
|
||||
"difficulty": 1,
|
||||
"type": "multiple_choice",
|
||||
"instruction": "Choisis le bon groupe de mots.",
|
||||
"question": "Les ... chiens jouent.",
|
||||
"answers": ["petit", "petits", "petite"],
|
||||
"correct_answer": 1,
|
||||
"hint": "Observe le determinant « les » et le nom « chiens ».",
|
||||
"explanation": "Le groupe nominal est au pluriel. L'adjectif prend ici la marque du pluriel.",
|
||||
"review_tags": ["pluriel", "accord_adjectif"],
|
||||
"review_status": "draft"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "ce1_accord_adjectif_qcm_02",
|
||||
"level": "CE1",
|
||||
"domain": "grammaire",
|
||||
"skill": "accord_groupe_nominal",
|
||||
"rule_id": "regle_accord_adjectif",
|
||||
"difficulty": 1,
|
||||
"type": "multiple_choice",
|
||||
"instruction": "Choisis le bon groupe de mots.",
|
||||
"question": "La ... maison est jolie.",
|
||||
"answers": ["petit", "petite", "petites"],
|
||||
"correct_answer": 1,
|
||||
"hint": "Observe le determinant « la » et le nom « maison ».",
|
||||
"explanation": "Le nom « maison » est feminin singulier. L'adjectif prend donc la marque du feminin.",
|
||||
"review_tags": ["feminin", "accord_adjectif"],
|
||||
"review_status": "draft"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"rule_id": "regle_accord_adjectif",
|
||||
"level": "CE1",
|
||||
"domain": "grammaire",
|
||||
"skill": "accord_groupe_nominal",
|
||||
"statement": "Quand un adjectif accompagne un nom, il s'accorde avec ce nom en genre et en nombre.",
|
||||
"example": "Un petit chien. Une petite chienne. Des petits chiens. Des petites chiennes.",
|
||||
"review_status": "draft"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"level",
|
||||
"region_id",
|
||||
"chapter_id",
|
||||
"title",
|
||||
"rule_ids",
|
||||
"review_status"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"level": { "type": "string", "enum": ["CE1", "CE2"] },
|
||||
"region_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"chapter_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"title": { "type": "string", "minLength": 1 },
|
||||
"rule_ids": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }
|
||||
},
|
||||
"review_status": { "type": "string", "enum": ["draft", "validated"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"level",
|
||||
"domain",
|
||||
"rule_ids",
|
||||
"difficulty",
|
||||
"target_text",
|
||||
"words",
|
||||
"audio_id",
|
||||
"audio_provisional",
|
||||
"accepted_variants",
|
||||
"review_status"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"level": { "type": "string", "enum": ["CE1", "CE2"] },
|
||||
"domain": { "type": "string", "enum": ["grammaire", "orthographe"] },
|
||||
"rule_ids": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }
|
||||
},
|
||||
"difficulty": { "type": "integer", "minimum": 1, "maximum": 3 },
|
||||
"target_text": { "type": "string", "minLength": 1 },
|
||||
"words": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["text"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"text": { "type": "string", "minLength": 1 },
|
||||
"rule_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"expects_capital": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"audio_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"audio_provisional": { "type": "boolean" },
|
||||
"accepted_variants": { "type": "array", "items": { "type": "string" } },
|
||||
"review_status": { "type": "string", "enum": ["draft", "validated"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"level",
|
||||
"domain",
|
||||
"skill",
|
||||
"rule_id",
|
||||
"difficulty",
|
||||
"type",
|
||||
"instruction",
|
||||
"question",
|
||||
"answers",
|
||||
"correct_answer",
|
||||
"hint",
|
||||
"explanation",
|
||||
"review_tags",
|
||||
"review_status"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"level": { "type": "string", "enum": ["CE1", "CE2"] },
|
||||
"domain": { "type": "string", "enum": ["grammaire", "orthographe"] },
|
||||
"skill": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"rule_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"difficulty": { "type": "integer", "minimum": 1, "maximum": 3 },
|
||||
"type": { "type": "string", "const": "multiple_choice" },
|
||||
"instruction": { "type": "string", "minLength": 1 },
|
||||
"question": { "type": "string", "minLength": 1 },
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 6,
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"correct_answer": { "type": "integer", "minimum": 0 },
|
||||
"hint": { "type": "string", "minLength": 1 },
|
||||
"explanation": { "type": "string", "minLength": 1 },
|
||||
"review_tags": { "type": "array", "items": { "type": "string" } },
|
||||
"review_status": { "type": "string", "enum": ["draft", "validated"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"level",
|
||||
"domain",
|
||||
"skill",
|
||||
"rule_id",
|
||||
"difficulty",
|
||||
"type",
|
||||
"instruction",
|
||||
"tokens",
|
||||
"hint",
|
||||
"explanation",
|
||||
"review_tags",
|
||||
"review_status"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"level": { "type": "string", "enum": ["CE1", "CE2"] },
|
||||
"domain": { "type": "string", "enum": ["grammaire", "orthographe"] },
|
||||
"skill": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"rule_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"difficulty": { "type": "integer", "minimum": 1, "maximum": 3 },
|
||||
"type": { "type": "string", "const": "sentence_correction" },
|
||||
"instruction": { "type": "string", "minLength": 1 },
|
||||
"tokens": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["text", "correct"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"text": { "type": "string", "minLength": 1 },
|
||||
"correct": { "type": "boolean" },
|
||||
"expected": { "type": "string", "minLength": 1 },
|
||||
"rule_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"hint": { "type": "string", "minLength": 1 },
|
||||
"explanation": { "type": "string", "minLength": 1 },
|
||||
"review_tags": { "type": "array", "items": { "type": "string" } },
|
||||
"review_status": { "type": "string", "enum": ["draft", "validated"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rule_id",
|
||||
"level",
|
||||
"domain",
|
||||
"skill",
|
||||
"statement",
|
||||
"example",
|
||||
"review_status"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"rule_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"level": { "type": "string", "enum": ["CE1", "CE2"] },
|
||||
"domain": { "type": "string", "enum": ["grammaire", "orthographe"] },
|
||||
"skill": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"statement": { "type": "string", "minLength": 1 },
|
||||
"example": { "type": "string", "minLength": 1 },
|
||||
"review_status": { "type": "string", "enum": ["draft", "validated"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema_version",
|
||||
"profile_id",
|
||||
"display_name",
|
||||
"school_level",
|
||||
"session_count",
|
||||
"unlocked_regions",
|
||||
"skills",
|
||||
"rewards"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schema_version": { "type": "integer", "minimum": 1 },
|
||||
"profile_id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"display_name": { "type": "string", "minLength": 1 },
|
||||
"school_level": { "type": "string", "enum": ["CE1", "CE2"] },
|
||||
"session_count": { "type": "integer", "minimum": 0 },
|
||||
"unlocked_regions": { "type": "array", "items": { "type": "string" } },
|
||||
"skills": { "type": "object" },
|
||||
"rewards": {
|
||||
"type": "object",
|
||||
"required": ["stars", "items"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"stars": { "type": "integer", "minimum": 0 },
|
||||
"items": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Revue du contenu pedagogique
|
||||
|
||||
Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne pas editer ce fichier a la main.
|
||||
|
||||
## regle_accord_adjectif (CE1, grammaire)
|
||||
|
||||
**Enonce** : Quand un adjectif accompagne un nom, il s'accorde avec ce nom en genre et en nombre.
|
||||
|
||||
**Exemple** : Un petit chien. Une petite chienne. Des petits chiens. Des petites chiennes.
|
||||
|
||||
**Statut** : draft
|
||||
|
||||
### Choix multiples
|
||||
|
||||
- **ce1_accord_adjectif_qcm_01** (difficulte 1) : Les ... chiens jouent.
|
||||
- Reponses : petit, petits, petite
|
||||
- Reponse attendue : petits
|
||||
- Indice : Observe le determinant « les » et le nom « chiens ».
|
||||
- Explication : Le groupe nominal est au pluriel. L'adjectif prend ici la marque du pluriel.
|
||||
- Statut : draft
|
||||
- **ce1_accord_adjectif_qcm_02** (difficulte 1) : La ... maison est jolie.
|
||||
- Reponses : petit, petite, petites
|
||||
- Reponse attendue : petite
|
||||
- Indice : Observe le determinant « la » et le nom « maison ».
|
||||
- Explication : Le nom « maison » est feminin singulier. L'adjectif prend donc la marque du feminin.
|
||||
- Statut : draft
|
||||
|
||||
### Corrections de phrase
|
||||
|
||||
- **ce1_accord_adjectif_correction_01** (difficulte 2) : Les [petit -> petits] chiens courent.
|
||||
- Indice : Le determinant « les » indique un pluriel.
|
||||
- Explication : L'adjectif « petit » s'accorde avec le nom « chiens », qui est au pluriel : il faut ecrire « petits ».
|
||||
- Statut : draft
|
||||
|
||||
### Dictees
|
||||
|
||||
- **ce1_accord_adjectif_dictee_01** : Les petits chats dorment.
|
||||
- Variantes acceptees : aucune
|
||||
- Audio : provisoire
|
||||
- Statut : draft
|
||||
|
||||
@@ -88,3 +88,13 @@ Ajouts à l'arborescence proposée :
|
||||
- Licence et hébergement : tranchés ci-dessus (point 0).
|
||||
- Validation du contenu CE1/CE2 par un enseignant avant diffusion : à organiser, pas de date fixée.
|
||||
- Spécification minimale de performance pour « appareil familial » : non définie, à préciser avant les exports.
|
||||
|
||||
## 11. Étape « Données » : précisions prises en cours d'implémentation
|
||||
|
||||
- Le validateur générique (`scripts/data/schema_validator.gd`) implémente un sous-ensemble de JSON Schema (`type`, `required`, `properties`, `additionalProperties`, `items`, `enum`, `const`, bornes numériques, longueurs, `pattern`). Suffisant pour tous les schémas actuels ; pas de `patternProperties` ni de `if`/`then`.
|
||||
- Le dispatch du schéma d'exercice se fait sur le champ `type` (`multiple_choice` → `exercise_multiple_choice.schema.json`, `sentence_correction` → `exercise_sentence_correction.schema.json`), dans `DataLoader.exercise_schema_name_for_type`. Un type absent de cette fonction est rejeté explicitement par `make validate-data`, conformément à l'invariant « liste fermée des types d'exercices ».
|
||||
- Les contraintes croisées qu'un schéma ne peut pas exprimer (index `correct_answer` dans les bornes de `answers`, au moins un token fautif en correction de phrase avec une correction `expected`, cohérence entre `target_text` et `words` en dictée) vivent dans `scripts/data/semantic_checks.gd`, testées indépendamment du schéma.
|
||||
- Schéma de dictée : `words` (annotation mot par mot : `rule_id` et `expects_capital` facultatifs) est **obligatoire**, pas facultatif — sans lui, l'erreur reste non classée (cf. « Points durs » du prompt de lancement). Le nombre de mots dans `words` doit correspondre à `target_text` découpé sur les espaces.
|
||||
- Le schéma `save` reste provisoire pour le champ `skills` (`{"type": "object"}`, sans validation des clés dynamiques) : le validateur actuel ne sait pas exprimer un schéma pour des clés arbitraires. À affiner à l'étape « Profils et sauvegardes ».
|
||||
- GUT est pinné en v9.6.0, pas la dernière version (9.7.1 référence le singleton `AccessibilityServer`, absent de Godot 4.6.3 en mode headless).
|
||||
- `tools/setup.sh` lance `godot --headless --path . --import` en fin d'installation : nécessaire pour que les scripts `class_name` globaux (`SchemaValidator`, `DataLoader`, `SemanticChecks`) soient résolus avant la première exécution de `make test` / `make validate-data`.
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
class_name DataLoader
|
||||
extends RefCounted
|
||||
## Charge et valide les fichiers JSON de data/ contre leurs schemas (data/schemas/).
|
||||
## Meme code utilise par `make validate-data` (headless) et par ExerciseEngine a
|
||||
## l'execution : une seule source de verite pour la validation (voir
|
||||
## docs/specs/decisions.md, point 2).
|
||||
|
||||
const SCHEMA_DIR := "res://data/schemas"
|
||||
|
||||
static var _schema_cache: Dictionary = {}
|
||||
|
||||
|
||||
## Mappe un type d'exercice (champ "type" des donnees) vers le nom de son schema.
|
||||
## Chaine vide si le type n'est pas reconnu : la liste des types est fermee ici,
|
||||
## pas devinee ailleurs.
|
||||
static func exercise_schema_name_for_type(exercise_type: String) -> String:
|
||||
match exercise_type:
|
||||
"multiple_choice":
|
||||
return "exercise_multiple_choice"
|
||||
"sentence_correction":
|
||||
return "exercise_sentence_correction"
|
||||
_:
|
||||
return ""
|
||||
|
||||
|
||||
static func load_schema(schema_name: String) -> Dictionary:
|
||||
if _schema_cache.has(schema_name):
|
||||
return _schema_cache[schema_name]
|
||||
var path := "%s/%s.schema.json" % [SCHEMA_DIR, schema_name]
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("Schema introuvable : %s" % path)
|
||||
return {}
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
var schema: Dictionary = parsed if typeof(parsed) == TYPE_DICTIONARY else {}
|
||||
_schema_cache[schema_name] = schema
|
||||
return schema
|
||||
|
||||
|
||||
## Lit un fichier JSON et le valide contre le schema nomme. Retourne la liste des
|
||||
## erreurs (vide si valide) ; ne retourne jamais les donnees analysees, pour que
|
||||
## l'appelant ne puisse pas accidentellement utiliser une donnee non validee.
|
||||
static func validate_file(path: String, schema_name: String) -> Array[String]:
|
||||
var parsed: Variant = read_json(path)
|
||||
if parsed == null:
|
||||
return ["%s : lecture impossible ou JSON invalide" % path]
|
||||
var schema := load_schema(schema_name)
|
||||
return SchemaValidator.validate(parsed, schema, path)
|
||||
|
||||
|
||||
static func read_json(path: String) -> Variant:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
return null
|
||||
var text := file.get_as_text()
|
||||
if text.strip_edges().is_empty():
|
||||
return null
|
||||
return JSON.parse_string(text)
|
||||
|
||||
|
||||
static func find_json_files(root_path: String) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
var dir := DirAccess.open(root_path)
|
||||
if dir == null:
|
||||
return result
|
||||
dir.list_dir_begin()
|
||||
var entry := dir.get_next()
|
||||
while entry != "":
|
||||
if entry != "." and entry != "..":
|
||||
var full_path := root_path.path_join(entry)
|
||||
if dir.current_is_dir():
|
||||
result.append_array(find_json_files(full_path))
|
||||
elif entry.ends_with(".json"):
|
||||
result.append(full_path)
|
||||
entry = dir.get_next()
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://l56rn1o7hqm0
|
||||
@@ -0,0 +1,122 @@
|
||||
class_name SchemaValidator
|
||||
extends RefCounted
|
||||
## Validateur de donnees contre un sous-ensemble de JSON Schema : type, required,
|
||||
## properties, additionalProperties, items, enum, const, minimum, maximum,
|
||||
## minItems, maxItems, minLength, maxLength, pattern.
|
||||
## Les schemas ne decrivent jamais de code a executer : seulement une structure
|
||||
## de donnees a verifier (voir l'invariant d'architecture dans CLAUDE.md).
|
||||
|
||||
|
||||
static func validate(value: Variant, schema: Dictionary, path: String = "$") -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
_validate_node(value, schema, path, errors)
|
||||
return errors
|
||||
|
||||
|
||||
static func _validate_node(
|
||||
value: Variant, schema: Dictionary, path: String, errors: Array[String]
|
||||
) -> void:
|
||||
if schema.has("const"):
|
||||
if value != schema["const"]:
|
||||
errors.append("%s : doit valoir exactement %s" % [path, schema["const"]])
|
||||
return
|
||||
|
||||
if schema.has("enum"):
|
||||
var allowed: Array = schema["enum"]
|
||||
if not allowed.has(value):
|
||||
errors.append("%s : valeur '%s' absente de l'enum %s" % [path, value, allowed])
|
||||
return
|
||||
|
||||
if schema.has("type"):
|
||||
if not _check_type(value, schema["type"]):
|
||||
errors.append(
|
||||
"%s : type attendu '%s', trouve '%s'" % [path, schema["type"], typeof(value)]
|
||||
)
|
||||
return
|
||||
|
||||
match typeof(value):
|
||||
TYPE_DICTIONARY:
|
||||
_validate_object(value, schema, path, errors)
|
||||
TYPE_ARRAY:
|
||||
_validate_array(value, schema, path, errors)
|
||||
TYPE_STRING:
|
||||
_validate_string(value, schema, path, errors)
|
||||
TYPE_INT, TYPE_FLOAT:
|
||||
_validate_number(value, schema, path, errors)
|
||||
|
||||
|
||||
static func _check_type(value: Variant, expected: String) -> bool:
|
||||
match expected:
|
||||
"object":
|
||||
return typeof(value) == TYPE_DICTIONARY
|
||||
"array":
|
||||
return typeof(value) == TYPE_ARRAY
|
||||
"string":
|
||||
return typeof(value) == TYPE_STRING
|
||||
"boolean":
|
||||
return typeof(value) == TYPE_BOOL
|
||||
"integer":
|
||||
if typeof(value) == TYPE_INT:
|
||||
return true
|
||||
if typeof(value) == TYPE_FLOAT:
|
||||
var number: float = value
|
||||
return number == floor(number)
|
||||
return false
|
||||
"number":
|
||||
return typeof(value) == TYPE_INT or typeof(value) == TYPE_FLOAT
|
||||
_:
|
||||
return true
|
||||
|
||||
|
||||
static func _validate_object(
|
||||
value: Dictionary, schema: Dictionary, path: String, errors: Array[String]
|
||||
) -> void:
|
||||
if schema.has("required"):
|
||||
for key: String in schema["required"]:
|
||||
if not value.has(key):
|
||||
errors.append("%s : propriete requise manquante '%s'" % [path, key])
|
||||
|
||||
var properties: Dictionary = schema.get("properties", {})
|
||||
var additional_allowed: Variant = schema.get("additionalProperties", true)
|
||||
for key: String in value.keys():
|
||||
if properties.has(key):
|
||||
_validate_node(value[key], properties[key], "%s.%s" % [path, key], errors)
|
||||
elif additional_allowed == false:
|
||||
errors.append("%s : propriete inattendue '%s'" % [path, key])
|
||||
|
||||
|
||||
static func _validate_array(
|
||||
value: Array, schema: Dictionary, path: String, errors: Array[String]
|
||||
) -> void:
|
||||
if schema.has("minItems") and value.size() < int(schema["minItems"]):
|
||||
errors.append("%s : au moins %d element(s) attendu(s)" % [path, schema["minItems"]])
|
||||
if schema.has("maxItems") and value.size() > int(schema["maxItems"]):
|
||||
errors.append("%s : au plus %d element(s) attendu(s)" % [path, schema["maxItems"]])
|
||||
if schema.has("items"):
|
||||
var item_schema: Dictionary = schema["items"]
|
||||
for i in value.size():
|
||||
_validate_node(value[i], item_schema, "%s[%d]" % [path, i], errors)
|
||||
|
||||
|
||||
static func _validate_string(
|
||||
value: String, schema: Dictionary, path: String, errors: Array[String]
|
||||
) -> void:
|
||||
if schema.has("minLength") and value.length() < int(schema["minLength"]):
|
||||
errors.append("%s : longueur minimale %d non atteinte" % [path, schema["minLength"]])
|
||||
if schema.has("maxLength") and value.length() > int(schema["maxLength"]):
|
||||
errors.append("%s : longueur maximale %d depassee" % [path, schema["maxLength"]])
|
||||
if schema.has("pattern"):
|
||||
var regex := RegEx.new()
|
||||
regex.compile(schema["pattern"])
|
||||
if regex.search(value) == null:
|
||||
errors.append("%s : ne correspond pas au motif '%s'" % [path, schema["pattern"]])
|
||||
|
||||
|
||||
static func _validate_number(
|
||||
value: Variant, schema: Dictionary, path: String, errors: Array[String]
|
||||
) -> void:
|
||||
var number: float = value
|
||||
if schema.has("minimum") and number < float(schema["minimum"]):
|
||||
errors.append("%s : doit etre >= %s" % [path, schema["minimum"]])
|
||||
if schema.has("maximum") and number > float(schema["maximum"]):
|
||||
errors.append("%s : doit etre <= %s" % [path, schema["maximum"]])
|
||||
@@ -0,0 +1 @@
|
||||
uid://dl6pewdq2dyus
|
||||
@@ -0,0 +1,49 @@
|
||||
class_name SemanticChecks
|
||||
extends RefCounted
|
||||
## Contraintes croisees entre champs qu'un schema JSON (voir SchemaValidator) ne
|
||||
## peut pas exprimer : index valide, coherence entre deux tableaux, etc.
|
||||
## Appele uniquement apres une validation de schema reussie : ces fonctions
|
||||
## supposent une structure deja conforme.
|
||||
|
||||
|
||||
static func check_multiple_choice(data: Dictionary, path: String) -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
var answers: Array = data.get("answers", [])
|
||||
var correct_answer: int = data.get("correct_answer", -1)
|
||||
if correct_answer < 0 or correct_answer >= answers.size():
|
||||
errors.append(
|
||||
"%s : correct_answer (%d) hors des bornes de answers (%d element(s))"
|
||||
% [path, correct_answer, answers.size()]
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
static func check_sentence_correction(data: Dictionary, path: String) -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
var tokens: Array = data.get("tokens", [])
|
||||
var has_error_token := false
|
||||
for i in tokens.size():
|
||||
var token: Dictionary = tokens[i]
|
||||
if token.get("correct", true) == false:
|
||||
has_error_token = true
|
||||
if not token.has("expected"):
|
||||
errors.append(
|
||||
"%s.tokens[%d] : token fautif sans correction attendue ('expected')"
|
||||
% [path, i]
|
||||
)
|
||||
if not has_error_token:
|
||||
errors.append("%s : aucun token fautif, rien a corriger" % path)
|
||||
return errors
|
||||
|
||||
|
||||
static func check_dictation(data: Dictionary, path: String) -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
var target_text: String = data.get("target_text", "")
|
||||
var words: Array = data.get("words", [])
|
||||
var expected_word_count := target_text.split(" ", false).size()
|
||||
if words.size() != expected_word_count:
|
||||
errors.append(
|
||||
"%s : %d mot(s) annote(s) pour %d mot(s) dans target_text"
|
||||
% [path, words.size(), expected_word_count]
|
||||
)
|
||||
return errors
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2ekxtn3qh5ku
|
||||
@@ -1,46 +1,68 @@
|
||||
extends SceneTree
|
||||
## Outil headless invoque par `make validate-data`.
|
||||
## A ce stade, verifie seulement que les fichiers data/**/*.json sont du JSON bien forme.
|
||||
## La validation contre les schemas (rule, exercise, dictation, curriculum, save) sera
|
||||
## ajoutee a l'etape "Donnees" (voir Ordre de construction, docs/specs/decisions.md point 2).
|
||||
## Valide chaque fichier JSON de data/ contre son schema (data/schemas/), puis
|
||||
## applique les contraintes croisees que le schema ne peut pas exprimer.
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var json_paths: Array[String] = _find_json_files("res://data")
|
||||
var failures: Array[String] = []
|
||||
for path in json_paths:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("%s : lecture impossible" % path)
|
||||
continue
|
||||
var text := file.get_as_text()
|
||||
if text.strip_edges().is_empty():
|
||||
continue
|
||||
if JSON.parse_string(text) == null:
|
||||
failures.append("%s : JSON invalide" % path)
|
||||
var checked := 0
|
||||
|
||||
checked += _validate_simple_dir("res://data/rules", "rule", failures)
|
||||
checked += _validate_simple_dir("res://data/curriculum", "curriculum", failures)
|
||||
checked += _validate_dictations("res://data/dictations", failures)
|
||||
checked += _validate_exercises("res://data/exercises", failures)
|
||||
|
||||
if failures.is_empty():
|
||||
print("validate-data : %d fichier(s) JSON valide(s)" % json_paths.size())
|
||||
print("validate-data : %d fichier(s) JSON valide(s)" % checked)
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
printerr(failure)
|
||||
printerr("validate-data : %d erreur(s)" % failures.size())
|
||||
quit(1)
|
||||
|
||||
|
||||
func _find_json_files(root_path: String) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
var dir := DirAccess.open(root_path)
|
||||
if dir == null:
|
||||
return result
|
||||
dir.list_dir_begin()
|
||||
var entry := dir.get_next()
|
||||
while entry != "":
|
||||
if entry != "." and entry != "..":
|
||||
var full_path := root_path.path_join(entry)
|
||||
if dir.current_is_dir():
|
||||
result.append_array(_find_json_files(full_path))
|
||||
elif entry.ends_with(".json"):
|
||||
result.append(full_path)
|
||||
entry = dir.get_next()
|
||||
return result
|
||||
func _validate_simple_dir(dir_path: String, schema_name: String, failures: Array[String]) -> int:
|
||||
var count := 0
|
||||
for path in DataLoader.find_json_files(dir_path):
|
||||
count += 1
|
||||
failures.append_array(DataLoader.validate_file(path, schema_name))
|
||||
return count
|
||||
|
||||
|
||||
func _validate_dictations(dir_path: String, failures: Array[String]) -> int:
|
||||
var count := 0
|
||||
for path in DataLoader.find_json_files(dir_path):
|
||||
count += 1
|
||||
var schema_errors := DataLoader.validate_file(path, "dictation")
|
||||
failures.append_array(schema_errors)
|
||||
if schema_errors.is_empty():
|
||||
var data: Dictionary = DataLoader.read_json(path)
|
||||
failures.append_array(SemanticChecks.check_dictation(data, path))
|
||||
return count
|
||||
|
||||
|
||||
func _validate_exercises(dir_path: String, failures: Array[String]) -> int:
|
||||
var count := 0
|
||||
for path in DataLoader.find_json_files(dir_path):
|
||||
count += 1
|
||||
var data: Variant = DataLoader.read_json(path)
|
||||
if data == null or typeof(data) != TYPE_DICTIONARY or not data.has("type"):
|
||||
failures.append("%s : champ 'type' manquant ou JSON invalide" % path)
|
||||
continue
|
||||
var exercise_type: String = data["type"]
|
||||
var schema_name := DataLoader.exercise_schema_name_for_type(exercise_type)
|
||||
if schema_name.is_empty():
|
||||
failures.append("%s : type d'exercice inconnu '%s'" % [path, exercise_type])
|
||||
continue
|
||||
var schema_errors := DataLoader.validate_file(path, schema_name)
|
||||
failures.append_array(schema_errors)
|
||||
if not schema_errors.is_empty():
|
||||
continue
|
||||
match exercise_type:
|
||||
"multiple_choice":
|
||||
failures.append_array(SemanticChecks.check_multiple_choice(data, path))
|
||||
"sentence_correction":
|
||||
failures.append_array(SemanticChecks.check_sentence_correction(data, path))
|
||||
return count
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
extends GutTest
|
||||
## Tests du validateur de schema generique (scripts/data/schema_validator.gd),
|
||||
## independants de tout contenu reel de data/.
|
||||
|
||||
var schema: Dictionary = {
|
||||
"type": "object",
|
||||
"required": ["id", "level"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
|
||||
"level": { "type": "string", "enum": ["CE1", "CE2"] },
|
||||
"difficulty": { "type": "integer", "minimum": 1, "maximum": 3 },
|
||||
"tags": { "type": "array", "minItems": 1, "items": { "type": "string" } },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
func test_valid_object_has_no_errors() -> void:
|
||||
var data := { "id": "ex_01", "level": "CE1", "difficulty": 2, "tags": ["pluriel"] }
|
||||
assert_eq(SchemaValidator.validate(data, schema), [])
|
||||
|
||||
|
||||
func test_missing_required_property_is_reported() -> void:
|
||||
var data := { "level": "CE1" }
|
||||
var errors := SchemaValidator.validate(data, schema)
|
||||
assert_eq(errors.size(), 1)
|
||||
assert_string_contains(errors[0], "id")
|
||||
|
||||
|
||||
func test_wrong_type_is_reported() -> void:
|
||||
var data := { "id": "ex_01", "level": "CE1", "difficulty": "deux" }
|
||||
var errors := SchemaValidator.validate(data, schema)
|
||||
assert_eq(errors.size(), 1)
|
||||
assert_string_contains(errors[0], "difficulty")
|
||||
|
||||
|
||||
func test_enum_violation_is_reported() -> void:
|
||||
var data := { "id": "ex_01", "level": "CM1" }
|
||||
var errors := SchemaValidator.validate(data, schema)
|
||||
assert_eq(errors.size(), 1)
|
||||
assert_string_contains(errors[0], "level")
|
||||
|
||||
|
||||
func test_pattern_violation_is_reported() -> void:
|
||||
var data := { "id": "Ex-01", "level": "CE1" }
|
||||
var errors := SchemaValidator.validate(data, schema)
|
||||
assert_eq(errors.size(), 1)
|
||||
assert_string_contains(errors[0], "id")
|
||||
|
||||
|
||||
func test_unexpected_property_is_reported_when_additional_forbidden() -> void:
|
||||
var data := { "id": "ex_01", "level": "CE1", "extra": true }
|
||||
var errors := SchemaValidator.validate(data, schema)
|
||||
assert_eq(errors.size(), 1)
|
||||
assert_string_contains(errors[0], "extra")
|
||||
|
||||
|
||||
func test_min_items_violation_is_reported() -> void:
|
||||
var data := { "id": "ex_01", "level": "CE1", "tags": [] }
|
||||
var errors := SchemaValidator.validate(data, schema)
|
||||
assert_eq(errors.size(), 1)
|
||||
assert_string_contains(errors[0], "tags")
|
||||
|
||||
|
||||
func test_numeric_bounds_are_checked() -> void:
|
||||
var data := { "id": "ex_01", "level": "CE1", "difficulty": 9 }
|
||||
var errors := SchemaValidator.validate(data, schema)
|
||||
assert_eq(errors.size(), 1)
|
||||
assert_string_contains(errors[0], "difficulty")
|
||||
@@ -0,0 +1,45 @@
|
||||
extends GutTest
|
||||
## Tests des contraintes croisees (scripts/data/semantic_checks.gd).
|
||||
|
||||
|
||||
func test_multiple_choice_rejects_out_of_bounds_correct_answer() -> void:
|
||||
var data := { "answers": ["a", "b"], "correct_answer": 5 }
|
||||
var errors := SemanticChecks.check_multiple_choice(data, "test")
|
||||
assert_eq(errors.size(), 1)
|
||||
|
||||
|
||||
func test_multiple_choice_accepts_valid_index() -> void:
|
||||
var data := { "answers": ["a", "b"], "correct_answer": 1 }
|
||||
assert_eq(SemanticChecks.check_multiple_choice(data, "test"), [])
|
||||
|
||||
|
||||
func test_sentence_correction_requires_at_least_one_error_token() -> void:
|
||||
var data := { "tokens": [{ "text": "Les", "correct": true }] }
|
||||
var errors := SemanticChecks.check_sentence_correction(data, "test")
|
||||
assert_eq(errors.size(), 1)
|
||||
|
||||
|
||||
func test_sentence_correction_requires_expected_on_error_tokens() -> void:
|
||||
var data := { "tokens": [{ "text": "petit", "correct": false }] }
|
||||
var errors := SemanticChecks.check_sentence_correction(data, "test")
|
||||
assert_eq(errors.size(), 1)
|
||||
assert_string_contains(errors[0], "expected")
|
||||
|
||||
|
||||
func test_dictation_word_count_must_match_target_text() -> void:
|
||||
var data := {
|
||||
"target_text": "Les petits chats dorment.",
|
||||
"words": [{ "text": "Les" }, { "text": "petits" }],
|
||||
}
|
||||
var errors := SemanticChecks.check_dictation(data, "test")
|
||||
assert_eq(errors.size(), 1)
|
||||
|
||||
|
||||
func test_dictation_accepts_matching_word_count() -> void:
|
||||
var data := {
|
||||
"target_text": "Les petits chats dorment.",
|
||||
"words": [
|
||||
{ "text": "Les" }, { "text": "petits" }, { "text": "chats" }, { "text": "dorment." }
|
||||
],
|
||||
}
|
||||
assert_eq(SemanticChecks.check_dictation(data, "test"), [])
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Genere docs/review/content-review.md a partir des donnees de data/.
|
||||
|
||||
Le JSON se relit mal ; ce document rassemble, pour chaque regle, son enonce
|
||||
puis chaque exercice qui l'illustre, avec sa reponse attendue, son indice et
|
||||
son explication. C'est ce document que l'adulte valide (voir CLAUDE.md,
|
||||
section Contenu pedagogique).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = REPO_ROOT / "data"
|
||||
OUTPUT_PATH = REPO_ROOT / "docs" / "review" / "content-review.md"
|
||||
|
||||
|
||||
def load_json_files(directory: Path) -> list[dict[str, Any]]:
|
||||
if not directory.exists():
|
||||
return []
|
||||
return [
|
||||
json.loads(path.read_text(encoding="utf-8")) for path in sorted(directory.rglob("*.json"))
|
||||
]
|
||||
|
||||
|
||||
def format_multiple_choice(exercise: dict[str, Any]) -> str:
|
||||
answers: list[str] = exercise["answers"]
|
||||
correct = answers[exercise["correct_answer"]]
|
||||
return "\n".join(
|
||||
[
|
||||
f"- **{exercise['id']}** (difficulte {exercise['difficulty']}) : {exercise['question']}",
|
||||
f" - Reponses : {', '.join(answers)}",
|
||||
f" - Reponse attendue : {correct}",
|
||||
f" - Indice : {exercise['hint']}",
|
||||
f" - Explication : {exercise['explanation']}",
|
||||
f" - Statut : {exercise['review_status']}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def format_sentence_correction(exercise: dict[str, Any]) -> str:
|
||||
rendered: list[str] = []
|
||||
for token in exercise["tokens"]:
|
||||
if token.get("correct", True):
|
||||
rendered.append(token["text"])
|
||||
else:
|
||||
rendered.append(f"[{token['text']} -> {token['expected']}]")
|
||||
sentence = " ".join(rendered)
|
||||
return "\n".join(
|
||||
[
|
||||
f"- **{exercise['id']}** (difficulte {exercise['difficulty']}) : {sentence}",
|
||||
f" - Indice : {exercise['hint']}",
|
||||
f" - Explication : {exercise['explanation']}",
|
||||
f" - Statut : {exercise['review_status']}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def format_dictation(dictation: dict[str, Any]) -> str:
|
||||
variants: list[str] = dictation["accepted_variants"]
|
||||
audio_state = "provisoire" if dictation["audio_provisional"] else "finalise"
|
||||
return "\n".join(
|
||||
[
|
||||
f"- **{dictation['id']}** : {dictation['target_text']}",
|
||||
f" - Variantes acceptees : {', '.join(variants) if variants else 'aucune'}",
|
||||
f" - Audio : {audio_state}",
|
||||
f" - Statut : {dictation['review_status']}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def render(
|
||||
rules: list[dict[str, Any]],
|
||||
exercises: list[dict[str, Any]],
|
||||
dictations: list[dict[str, Any]],
|
||||
) -> str:
|
||||
multiple_choice = [e for e in exercises if e["type"] == "multiple_choice"]
|
||||
sentence_correction = [e for e in exercises if e["type"] == "sentence_correction"]
|
||||
|
||||
lines: list[str] = [
|
||||
"# Revue du contenu pedagogique",
|
||||
"",
|
||||
"Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. "
|
||||
"Ne pas editer ce fichier a la main.",
|
||||
"",
|
||||
]
|
||||
|
||||
for rule in sorted(rules, key=lambda r: str(r["rule_id"])):
|
||||
rule_id = rule["rule_id"]
|
||||
lines += [
|
||||
f"## {rule_id} ({rule['level']}, {rule['domain']})",
|
||||
"",
|
||||
f"**Enonce** : {rule['statement']}",
|
||||
"",
|
||||
f"**Exemple** : {rule['example']}",
|
||||
"",
|
||||
f"**Statut** : {rule['review_status']}",
|
||||
"",
|
||||
]
|
||||
|
||||
rule_mc = [e for e in multiple_choice if e["rule_id"] == rule_id]
|
||||
if rule_mc:
|
||||
lines.append("### Choix multiples")
|
||||
lines.append("")
|
||||
for exercise in sorted(rule_mc, key=lambda e: str(e["id"])):
|
||||
lines.append(format_multiple_choice(exercise))
|
||||
lines.append("")
|
||||
|
||||
rule_sc = [e for e in sentence_correction if e["rule_id"] == rule_id]
|
||||
if rule_sc:
|
||||
lines.append("### Corrections de phrase")
|
||||
lines.append("")
|
||||
for exercise in sorted(rule_sc, key=lambda e: str(e["id"])):
|
||||
lines.append(format_sentence_correction(exercise))
|
||||
lines.append("")
|
||||
|
||||
rule_dictations = [d for d in dictations if rule_id in d["rule_ids"]]
|
||||
if rule_dictations:
|
||||
lines.append("### Dictees")
|
||||
lines.append("")
|
||||
for dictation in sorted(rule_dictations, key=lambda d: str(d["id"])):
|
||||
lines.append(format_dictation(dictation))
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
rules = load_json_files(DATA_DIR / "rules")
|
||||
exercises = load_json_files(DATA_DIR / "exercises")
|
||||
dictations = load_json_files(DATA_DIR / "dictations")
|
||||
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT_PATH.write_text(render(rules, exercises, dictations), encoding="utf-8")
|
||||
print(
|
||||
f"content-review.md genere : {len(rules)} regle(s), "
|
||||
f"{len(exercises)} exercice(s), {len(dictations)} dictee(s)."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+6
-2
@@ -11,6 +11,7 @@ GODOT_BIN_DIR=".godot-bin"
|
||||
GODOT_BIN="${GODOT_BIN_DIR}/godot"
|
||||
GUT_VERSION="v9.6.0" # v9.6.1+ reference AccessibilityServer, absent de Godot 4.6.3
|
||||
GDTOOLKIT_VERSION="4.5.0"
|
||||
PYRIGHT_VERSION="1.1.407"
|
||||
|
||||
mkdir -p "${GODOT_BIN_DIR}"
|
||||
|
||||
@@ -46,6 +47,9 @@ if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
./.venv/bin/pip install --quiet --upgrade pip
|
||||
./.venv/bin/pip install --quiet "gdtoolkit==${GDTOOLKIT_VERSION}"
|
||||
./.venv/bin/pip install --quiet "gdtoolkit==${GDTOOLKIT_VERSION}" "pyright==${PYRIGHT_VERSION}"
|
||||
|
||||
echo "Setup termine : Godot ${GODOT_VERSION}, GUT ${GUT_VERSION}, gdtoolkit ${GDTOOLKIT_VERSION}."
|
||||
echo "Import du projet (enregistre les class_name globaux : SchemaValidator, DataLoader...)."
|
||||
"${GODOT_BIN}" --headless --path . --import >/dev/null
|
||||
|
||||
echo "Setup termine : Godot ${GODOT_VERSION}, GUT ${GUT_VERSION}, gdtoolkit ${GDTOOLKIT_VERSION}, pyright ${PYRIGHT_VERSION}."
|
||||
|
||||
Reference in New Issue
Block a user