monl-compiler 0.9.0b9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- monl/__init__.py +21 -0
- monl/app_templates.py +529 -0
- monl/artifacts.py +176 -0
- monl/assets_tool/__init__.py +30 -0
- monl/assets_tool/commandes.py +213 -0
- monl/assets_tool/edition.py +228 -0
- monl/assets_tool/fondations.py +44 -0
- monl/assets_tool/resolution.py +129 -0
- monl/assets_tool/specio.py +64 -0
- monl/ast_validator/__init__.py +32 -0
- monl/ast_validator/acces.py +332 -0
- monl/ast_validator/assets.py +224 -0
- monl/ast_validator/audit.py +84 -0
- monl/ast_validator/capacites.py +274 -0
- monl/ast_validator/capacites_de_liste.py +136 -0
- monl/ast_validator/champs.py +241 -0
- monl/ast_validator/champs_calcules.py +309 -0
- monl/ast_validator/champs_serveur.py +195 -0
- monl/ast_validator/collisions.py +189 -0
- monl/ast_validator/commerce.py +252 -0
- monl/ast_validator/core.py +199 -0
- monl/ast_validator/cycle_de_vie.py +144 -0
- monl/ast_validator/migrations.py +106 -0
- monl/ast_validator/presentation.py +116 -0
- monl/ast_validator/socle.py +99 -0
- monl/ast_validator/uploads.py +137 -0
- monl/cli/__init__.py +45 -0
- monl/cli/__main__.py +10 -0
- monl/cli/coherence.py +211 -0
- monl/cli/consommation.py +119 -0
- monl/cli/construction.py +197 -0
- monl/cli/conteneur.py +75 -0
- monl/cli/contenu_editorial.py +95 -0
- monl/cli/couverture.py +198 -0
- monl/cli/couverture_fetch.py +150 -0
- monl/cli/delta.py +348 -0
- monl/cli/dispatch.py +268 -0
- monl/cli/emplacement.py +156 -0
- monl/cli/lancement.py +80 -0
- monl/cli/nomenclature.py +134 -0
- monl/cli/retouche.py +215 -0
- monl/cli/signature.py +220 -0
- monl/content_import.py +148 -0
- monl/content_tool.py +281 -0
- monl/design_skills.py +89 -0
- monl/design_system/__init__.py +48 -0
- monl/design_system/manifeste.py +104 -0
- monl/design_system/marqueurs.py +123 -0
- monl/design_system/noms.py +17 -0
- monl/design_system/profil.py +140 -0
- monl/design_system/rendu.py +325 -0
- monl/dialogue_engine/__init__.py +30 -0
- monl/dialogue_engine/commerce.py +214 -0
- monl/dialogue_engine/comptes.py +116 -0
- monl/dialogue_engine/core.py +54 -0
- monl/dialogue_engine/direction.py +167 -0
- monl/dialogue_engine/emission.py +109 -0
- monl/dialogue_engine/emission_parts.py +330 -0
- monl/dialogue_engine/fondations.py +102 -0
- monl/dialogue_engine/libre.py +205 -0
- monl/dialogue_engine/parcours.py +254 -0
- monl/dialogue_engine/questions.py +154 -0
- monl/errors.py +47 -0
- monl/frontend_ai/__init__.py +175 -0
- monl/frontend_ai/agents.py +393 -0
- monl/frontend_ai/controles_design.py +218 -0
- monl/frontend_ai/controles_fichiers.py +207 -0
- monl/frontend_ai/etages.py +318 -0
- monl/frontend_ai/fondations.py +29 -0
- monl/frontend_ai/fournisseurs.py +352 -0
- monl/frontend_ai/images.py +236 -0
- monl/frontend_ai/orchestration.py +266 -0
- monl/frontend_ai/redaction.py +72 -0
- monl/frontend_ai/reponse.py +205 -0
- monl/frontend_ai/squelette.py +103 -0
- monl/frontend_contract/__init__.py +32 -0
- monl/frontend_contract/assemblage.py +168 -0
- monl/frontend_contract/brief.py +366 -0
- monl/frontend_contract/champs.py +99 -0
- monl/frontend_contract/contrat_auth.py +163 -0
- monl/frontend_contract/contrat_entites.py +249 -0
- monl/frontend_contract/contrat_paiement.py +106 -0
- monl/frontend_contract/contrat_routes.py +163 -0
- monl/frontend_contract/emission.py +40 -0
- monl/frontend_contract/fondations.py +75 -0
- monl/frontend_contract/marqueurs.py +49 -0
- monl/frontend_contract/notes_de_contrat.py +132 -0
- monl/frontend_contract/projet.py +139 -0
- monl/frontend_contract/roles_de_champs.py +148 -0
- monl/generator/__init__.py +9 -0
- monl/generator/admin_cli.py +324 -0
- monl/generator/calculs.py +111 -0
- monl/generator/core.py +295 -0
- monl/generator/emitters.py +46 -0
- monl/generator/modele.py +119 -0
- monl/generator/paiement.py +60 -0
- monl/generator/pipeline.py +171 -0
- monl/generator/prealables.py +83 -0
- monl/generator/proprietaire.py +298 -0
- monl/generator/routes.py +40 -0
- monl/generator/routes_acces.py +97 -0
- monl/generator/routes_creation.py +391 -0
- monl/generator/routes_lecture.py +307 -0
- monl/generator/routes_lecture_filtree.py +296 -0
- monl/generator/routes_modification.py +352 -0
- monl/generator/routes_orchestration.py +55 -0
- monl/generator/routes_paiement.py +366 -0
- monl/generator/routes_prestataires.py +233 -0
- monl/generator/routes_suppression.py +243 -0
- monl/generator/routes_uploads.py +64 -0
- monl/generator/runtime.py +90 -0
- monl/generator/runtime_annexes.py +164 -0
- monl/generator/runtime_connexion.py +265 -0
- monl/generator/runtime_fonctions_auth.py +341 -0
- monl/generator/runtime_jetons.py +195 -0
- monl/generator/runtime_migrations.py +247 -0
- monl/generator/runtime_montage.py +323 -0
- monl/generator/runtime_pool.py +63 -0
- monl/generator/runtime_preparation.py +207 -0
- monl/generator/runtime_socle.py +372 -0
- monl/generator/sandbox.py +17 -0
- monl/generator/schemas.py +181 -0
- monl/generator/sql.py +100 -0
- monl/generator/sql_colonnes.py +238 -0
- monl/generator/sql_schema.py +194 -0
- monl/image_ai.py +289 -0
- monl/ir.py +334 -0
- monl/ir_types.py +187 -0
- monl/main.py +99 -0
- monl/parser/__init__.py +19 -0
- monl/parser/erreurs.py +82 -0
- monl/parser/grammaire.py +455 -0
- monl/parser/lecture.py +94 -0
- monl/parser/transformer.py +31 -0
- monl/parser/transformer_blocs.py +162 -0
- monl/parser/transformer_regles.py +163 -0
- monl/parser/transformer_structure.py +128 -0
- monl/planning.py +149 -0
- monl/policies.py +113 -0
- monl/section_substance.py +206 -0
- monl/serving.py +55 -0
- monl/smoke_test/__init__.py +105 -0
- monl/smoke_test/etapes.py +280 -0
- monl/smoke_test/fichiers_reclames.py +94 -0
- monl/smoke_test/fondations.py +190 -0
- monl/smoke_test/sondes.py +77 -0
- monl/tui.py +283 -0
- monl/ui_patterns.py +242 -0
- monl/usage.py +293 -0
- monl/validation_pipeline/__init__.py +15 -0
- monl/validation_pipeline/contrats.py +84 -0
- monl/validation_pipeline/defaut.py +68 -0
- monl/validation_pipeline/passes.py +302 -0
- monl_compiler-0.9.0b9.dist-info/METADATA +567 -0
- monl_compiler-0.9.0b9.dist-info/RECORD +222 -0
- monl_compiler-0.9.0b9.dist-info/WHEEL +5 -0
- monl_compiler-0.9.0b9.dist-info/entry_points.txt +4 -0
- monl_compiler-0.9.0b9.dist-info/licenses/LICENSE +105 -0
- monl_compiler-0.9.0b9.dist-info/top_level.txt +2 -0
- monl_platform/__init__.py +10 -0
- monl_platform/__main__.py +111 -0
- monl_platform/account.py +163 -0
- monl_platform/administration.py +321 -0
- monl_platform/app.py +80 -0
- monl_platform/app_api_routes.py +282 -0
- monl_platform/app_http.py +220 -0
- monl_platform/app_lifecycle.py +66 -0
- monl_platform/app_pages.py +163 -0
- monl_platform/app_templates.py +59 -0
- monl_platform/backup_healthcheck.py +81 -0
- monl_platform/brand.py +274 -0
- monl_platform/builder_auth_routes.py +130 -0
- monl_platform/builder_build_routes.py +73 -0
- monl_platform/builder_host.py +65 -0
- monl_platform/builder_routes.py +31 -0
- monl_platform/builder_runtime.py +147 -0
- monl_platform/coloration.py +123 -0
- monl_platform/compilation.py +83 -0
- monl_platform/console.py +27 -0
- monl_platform/console_script.py +405 -0
- monl_platform/console_template.py +343 -0
- monl_platform/dialogue.py +145 -0
- monl_platform/docs_page.py +121 -0
- monl_platform/downloads.py +111 -0
- monl_platform/evolution.py +152 -0
- monl_platform/examples.py +336 -0
- monl_platform/guide.py +214 -0
- monl_platform/guide_data.py +212 -0
- monl_platform/guide_template.py +117 -0
- monl_platform/hosting.py +366 -0
- monl_platform/hosting_admission.py +93 -0
- monl_platform/identity.py +33 -0
- monl_platform/identity_admin.py +186 -0
- monl_platform/identity_auth.py +126 -0
- monl_platform/identity_credentials.py +50 -0
- monl_platform/identity_database.py +123 -0
- monl_platform/identity_keys.py +59 -0
- monl_platform/identity_limits.py +43 -0
- monl_platform/identity_primitives.py +14 -0
- monl_platform/identity_projects.py +59 -0
- monl_platform/identity_recovery.py +89 -0
- monl_platform/journal.py +135 -0
- monl_platform/landing.py +261 -0
- monl_platform/landing_cas.py +202 -0
- monl_platform/landing_pourquoi.py +299 -0
- monl_platform/landing_vitrine.py +123 -0
- monl_platform/legal.py +298 -0
- monl_platform/mcp_page.py +48 -0
- monl_platform/mcp_server.py +257 -0
- monl_platform/oauth.py +321 -0
- monl_platform/paths.py +56 -0
- monl_platform/security.py +57 -0
- monl_platform/service.py +291 -0
- monl_platform/session.py +45 -0
- monl_platform/static/favicon.ico +0 -0
- monl_platform/static/monl-social.png +0 -0
- monl_platform/static/monl-wordmark.png +0 -0
- monl_platform/store.py +20 -0
- monl_platform/store_core.py +315 -0
- monl_platform/store_projects.py +159 -0
- monl_platform/theme.py +220 -0
- monl_platform/theme_fragments.py +396 -0
monl/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""monl — compilateur d'intention logicielle et orchestrateur frontend.
|
|
2
|
+
|
|
3
|
+
Le paquet expose la chaîne complète : parseur (grammaire Lark), validateur
|
|
4
|
+
et audit de sécurité, générateur de backend, contrat frontend, dialogue
|
|
5
|
+
guidé et interface en ligne de commande.
|
|
6
|
+
|
|
7
|
+
POINT 65 : jusqu'à la bêta 3, ces modules vivaient à plat dans src/ et
|
|
8
|
+
n'étaient importables qu'en manipulant sys.path — dans le shim d'entrée
|
|
9
|
+
comme en tête de chaque fichier de tests. C'est désormais un vrai paquet :
|
|
10
|
+
`from monl.parser import parse_monl_string` fonctionne partout où monl est
|
|
11
|
+
installé, sans préparatif.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# POINT 85 : ce numéro n'avait pas bougé des points 74 à 85 — cinq briques, un
|
|
15
|
+
# outil, un correctif de sécurité. Il ne disait donc rien, et le point 81 a dû
|
|
16
|
+
# construire sa détection d'artefacts périmés sur une RÉGÉNÉRATION plutôt que
|
|
17
|
+
# sur lui. Il reste secondaire par construction : la version n'est pas une
|
|
18
|
+
# preuve, la recompilation en est une. Mais un numéro faux est pire qu'un
|
|
19
|
+
# numéro secondaire — il est désormais enregistré dans monl.json, pour que
|
|
20
|
+
# l'avertissement du point 81 puisse dire AVEC QUOI le projet a été construit.
|
|
21
|
+
__version__ = "0.9.0-beta.9"
|
monl/app_templates.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
2
|
+
# CATALOGUE DE MODÈLES D'APPLICATIONS — point 45 du journal. Le dialogue
|
|
3
|
+
# ne part plus d'une page blanche : il ouvre sur les 10 types
|
|
4
|
+
# d'applications les plus construits par les développeurs web. Choisir un
|
|
5
|
+
# modèle pré-remplit entités, acteurs, règles et données de démonstration
|
|
6
|
+
# RÉALISTES, puis des questions de suivi PROPRES AU MODÈLE affinent
|
|
7
|
+
# (catégories ? stock ? commentaires ? likes ?). « Partir de zéro » reste
|
|
8
|
+
# disponible et conserve le dialogue libre intact.
|
|
9
|
+
#
|
|
10
|
+
# Chaque modèle est de la DONNÉE, pas du code : le dialogue les assemble
|
|
11
|
+
# via le même émetteur déterministe, et la spec finale repasse par le vrai
|
|
12
|
+
# parseur + l'audit AST — un modèle du catalogue ne peut pas produire une
|
|
13
|
+
# spec cassée sans casser les tests (chaque modèle est compilé dans
|
|
14
|
+
# tests/test_app_templates.py, chemins « tout non » ET « tout oui »).
|
|
15
|
+
#
|
|
16
|
+
# Structure d'un modèle :
|
|
17
|
+
# name / hint : ce qu'affiche le menu
|
|
18
|
+
# entities : {Nom: {"fields": [(champ, Type)],
|
|
19
|
+
# "manager": acteur, "readers": [acteurs],
|
|
20
|
+
# "public_read": bool, "public_create": bool,
|
|
21
|
+
# "owned": bool}} # ownedBy par le manager
|
|
22
|
+
# actors : liste d'acteurs
|
|
23
|
+
# relations : [(source, type, cible)] déclarées en plus de
|
|
24
|
+
# celles que 'owned' crée automatiquement
|
|
25
|
+
# extra_rules : lignes de règles émises telles quelles (briques
|
|
26
|
+
# avancées : increments, hidden, categorized…)
|
|
27
|
+
# seeds : {Entité: [ {champ: valeur} ]} — données de démo
|
|
28
|
+
# réalistes (sinon repli sur le seed générique)
|
|
29
|
+
# sections : [{"title": titre de rubrique, "ask": ce qu'on
|
|
30
|
+
# attend dedans}] — rubriques éditoriales que les
|
|
31
|
+
# recensements publics donnent comme attendues sur un
|
|
32
|
+
# site de ce genre (point 61). Le dialogue en demande
|
|
33
|
+
# DIRECTEMENT le texte, il ne demande plus s'il en
|
|
34
|
+
# faut ; une réponse vide passe la rubrique. Liste
|
|
35
|
+
# vide = outil interne, aucune rubrique standard.
|
|
36
|
+
# followups : [{"ask": question o/n, "effects": {...}}] où les
|
|
37
|
+
# effets fusionnent dans le modèle si "o" :
|
|
38
|
+
# add_fields / add_entities / add_relations /
|
|
39
|
+
# add_rules / add_seeds
|
|
40
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
def _img(_seed=None):
|
|
43
|
+
"""Champ d'illustration d'une fiche de démonstration : VIDE, et c'est le
|
|
44
|
+
sujet de la brique.
|
|
45
|
+
|
|
46
|
+
Ces fiches portaient une URL `picsum.photos` (et `loremflickr` dès qu'un
|
|
47
|
+
sujet était choisi). Le résultat contredisait la promesse la plus nette du
|
|
48
|
+
projet : une application monl est AUTONOME et son compilateur n'appelle
|
|
49
|
+
jamais l'extérieur — mais la toute première page que le prospect ouvrait
|
|
50
|
+
allait chercher ses images chez un tiers. Hors ligne, sur une connexion
|
|
51
|
+
lente ou sur un forfait de données compté, la vitrine s'ouvrait cassée.
|
|
52
|
+
|
|
53
|
+
Trois raisons de laisser VIDE plutôt que de mettre autre chose :
|
|
54
|
+
|
|
55
|
+
* une image en `data:` URI tiendrait dans le champ, mais mettrait 250
|
|
56
|
+
caractères illisibles dans chaque ligne `seed` de la spec — or la spec
|
|
57
|
+
est faite pour être lue et modifiée à la main, et `monl content export`
|
|
58
|
+
la déverse en CSV ;
|
|
59
|
+
* une photo livrée avec monl serait un choix VISUEL du compilateur, ce que
|
|
60
|
+
le point 72 lui interdit explicitement ;
|
|
61
|
+
* le vrai chemin d'une vraie photo existe déjà et il est documenté :
|
|
62
|
+
`monl assets add <fichier> --for "<fiche>"` (brique 13, point 84), qui
|
|
63
|
+
vérifie le fichier à la compilation au lieu d'espérer un serveur distant.
|
|
64
|
+
|
|
65
|
+
Le sujet d'illustration demandé par le dialogue n'est pas perdu : il part
|
|
66
|
+
dans le brief du contrat, pour que l'IA d'interface sache ce que les
|
|
67
|
+
visuels doivent évoquer (voir `_emit_spec`).
|
|
68
|
+
"""
|
|
69
|
+
return ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
TEMPLATES = [
|
|
73
|
+
{
|
|
74
|
+
"name": "Portfolio / site vitrine",
|
|
75
|
+
"hint": "galerie publique de projets + zone d'administration",
|
|
76
|
+
"actors": ["Admin"],
|
|
77
|
+
"entities": {
|
|
78
|
+
"Project": {"fields": [("title", "String"), ("description", "Text"),
|
|
79
|
+
("imageUrl", "String")],
|
|
80
|
+
"manager": "Admin", "readers": [], "public_read": True,
|
|
81
|
+
"public_create": False, "owned": False},
|
|
82
|
+
# ACQUIS (point 60) : « a clear contact method » figure dans toutes
|
|
83
|
+
# les listes d'essentiels d'un portfolio — c'était une question.
|
|
84
|
+
"Message": {"fields": [("author", "String"), ("email", "Email"),
|
|
85
|
+
("content", "Text")],
|
|
86
|
+
"manager": "Admin", "readers": [], "public_read": False,
|
|
87
|
+
"public_create": True, "owned": False},
|
|
88
|
+
},
|
|
89
|
+
"relations": [], "extra_rules": [],
|
|
90
|
+
# POINT 61 : « about » et une offre lisible figurent dans toutes les
|
|
91
|
+
# listes d'essentiels d'un portfolio, au même titre que la galerie.
|
|
92
|
+
"sections": [
|
|
93
|
+
{"title": "À propos",
|
|
94
|
+
"ask": "qui vous êtes, depuis quand, ce qui distingue votre travail"},
|
|
95
|
+
{"title": "Services",
|
|
96
|
+
"ask": "ce que vous proposez concrètement, et pour qui"},
|
|
97
|
+
],
|
|
98
|
+
"seeds": {"Project": [
|
|
99
|
+
{"title": "Refonte Aurora", "description": "Identité complète pour une marque de cosmétiques.", "imageUrl": _img("aurora")},
|
|
100
|
+
{"title": "App Meridian", "description": "Application mobile de suivi d'habitudes.", "imageUrl": _img("meridian")},
|
|
101
|
+
{"title": "Site Horizon", "description": "Site éditorial pour un festival de musique.", "imageUrl": _img("horizon")},
|
|
102
|
+
]},
|
|
103
|
+
"followups": [
|
|
104
|
+
{"ask": "Classer les projets par catégorie ?",
|
|
105
|
+
"effects": {"add_fields": {"Project": [("category", "String")]},
|
|
106
|
+
"add_seed_fields": {"Project": {"category": ["Identité", "Produit", "Web"]}}}},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "Blog",
|
|
111
|
+
"hint": "articles publics, commentaires des lecteurs",
|
|
112
|
+
"actors": ["Author", "Reader", "Moderator"],
|
|
113
|
+
"entities": {
|
|
114
|
+
# ACQUIS (point 60) : l'auteur humanise le billet, la date dit si
|
|
115
|
+
# l'information est encore d'actualité. Les deux sont donnés comme
|
|
116
|
+
# essentiels par l'anatomie d'un article ; la date était une
|
|
117
|
+
# question, l'auteur manquait purement et simplement.
|
|
118
|
+
"Article": {"fields": [("title", "String"), ("content", "Text"),
|
|
119
|
+
("imageUrl", "String"), ("author", "String"),
|
|
120
|
+
("publishedOn", "String"), ("status", "String")],
|
|
121
|
+
"manager": "Author", "readers": [], "public_read": True,
|
|
122
|
+
"public_create": False, "owned": False},
|
|
123
|
+
"Report": {"fields": [("reason", "Text"), ("status", "String")],
|
|
124
|
+
"manager": "Moderator", "readers": [], "public_read": False,
|
|
125
|
+
"public_create": False, "owned": False},
|
|
126
|
+
},
|
|
127
|
+
"relations": [("Article", "hasMany", "Report")],
|
|
128
|
+
"extra_rules": [
|
|
129
|
+
'rule Article.status oneOf "published", "hidden"',
|
|
130
|
+
'rule Article.Read publicWhen status "published"',
|
|
131
|
+
'rule Article.Read sharedBy Moderator',
|
|
132
|
+
'rule Article.Update sharedBy Author, Moderator',
|
|
133
|
+
'rule Report.status oneOf "open", "resolved", "dismissed"',
|
|
134
|
+
'rule Report.Create sharedBy Reader, Moderator',
|
|
135
|
+
],
|
|
136
|
+
"extra_workflows": [
|
|
137
|
+
{"name": "SubmitReport", "actor": "Reader",
|
|
138
|
+
"actions": [("Create", "Report")]},
|
|
139
|
+
{"name": "ModerateBlog", "actor": "Moderator",
|
|
140
|
+
"actions": [("Read", "Article"), ("Update", "Article"),
|
|
141
|
+
("Read", "Report"), ("Update", "Report"),
|
|
142
|
+
("Delete", "Report")]},
|
|
143
|
+
],
|
|
144
|
+
# POINT 61 : la bio de l'auteur est donnée comme page centrale d'un
|
|
145
|
+
# site d'écriture ; la ligne éditoriale dit au lecteur s'il est au bon
|
|
146
|
+
# endroit — ce qu'aucune liste d'articles ne raconte.
|
|
147
|
+
"sections": [
|
|
148
|
+
{"title": "À propos de l'auteur",
|
|
149
|
+
"ask": "qui écrit ici, et pourquoi on devrait vous lire"},
|
|
150
|
+
{"title": "Ligne éditoriale",
|
|
151
|
+
"ask": "de quoi parle ce blog, à quel rythme, pour quel lecteur"},
|
|
152
|
+
],
|
|
153
|
+
"seeds": {"Article": [
|
|
154
|
+
{"title": "Pourquoi j'ai quitté les frameworks", "content": "Retour d'expérience après un an de vanilla.", "imageUrl": _img("blog1"), "author": "Camille Roy", "publishedOn": "2026-05-12", "status": "published"},
|
|
155
|
+
{"title": "Le guide du télétravail durable", "content": "Trois ans de distance, ce qui marche vraiment.", "imageUrl": _img("blog2"), "author": "Camille Roy", "publishedOn": "2026-06-03", "status": "published"},
|
|
156
|
+
{"title": "Apprendre en public", "content": "Documenter ses progrès change tout.", "imageUrl": _img("blog3"), "author": "Sacha Nedel", "publishedOn": "2026-07-01", "status": "published"},
|
|
157
|
+
]},
|
|
158
|
+
"followups": [
|
|
159
|
+
{"ask": "Permettre aux lecteurs inscrits de commenter (chacun gère ses commentaires) ?",
|
|
160
|
+
"effects": {"add_actors": ["Reader"],
|
|
161
|
+
"add_entities": {"Comment": {"fields": [("content", "Text")],
|
|
162
|
+
"manager": "Reader", "readers": ["Author"],
|
|
163
|
+
"public_read": True, "public_create": False, "owned": True}},
|
|
164
|
+
"add_relations": [("Article", "hasMany", "Comment")]}},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"name": "Boutique en ligne",
|
|
169
|
+
"hint": "catalogue public, commandes des clients",
|
|
170
|
+
"actors": ["Admin", "Customer"],
|
|
171
|
+
"entities": {
|
|
172
|
+
# ACQUIS (point 60) : la disponibilité figure au-dessus de la ligne
|
|
173
|
+
# de flottaison d'une fiche produit, au même titre que le nom, le
|
|
174
|
+
# prix et l'image. C'était une question.
|
|
175
|
+
"Product": {"fields": [("name", "String"), ("price", "Money"),
|
|
176
|
+
("description", "Text"), ("imageUrl", "String"),
|
|
177
|
+
("stock", "Integer")],
|
|
178
|
+
"manager": "Admin", "readers": ["Customer"], "public_read": True,
|
|
179
|
+
"public_create": False, "owned": False},
|
|
180
|
+
"Order": {"fields": [("total", "Money"), ("status", "String")],
|
|
181
|
+
"manager": "Customer", "readers": ["Admin"], "public_read": False,
|
|
182
|
+
"public_create": False, "owned": True},
|
|
183
|
+
},
|
|
184
|
+
"relations": [], "extra_rules": [],
|
|
185
|
+
# POINT 61 : livraison/retours et FAQ sont les deux textes que les
|
|
186
|
+
# recensements e-commerce placent au-dessus du reste — ils lèvent les
|
|
187
|
+
# objections d'achat, qu'aucune fiche produit ne peut porter.
|
|
188
|
+
"sections": [
|
|
189
|
+
{"title": "À propos de la boutique",
|
|
190
|
+
"ask": "qui fabrique ou sélectionne, d'où viennent les produits"},
|
|
191
|
+
{"title": "Livraison et retours",
|
|
192
|
+
"ask": "délais, frais, conditions de retour"},
|
|
193
|
+
{"title": "Questions fréquentes",
|
|
194
|
+
"ask": "les questions que les clients posent avant d'acheter"},
|
|
195
|
+
],
|
|
196
|
+
"seeds": {"Product": [
|
|
197
|
+
{"name": "Théière Kyoto", "price": 39.5, "description": "Fonte émaillée, 0,8 L.", "imageUrl": _img("shop1"), "stock": 12},
|
|
198
|
+
{"name": "Tasse Duo", "price": 18.0, "description": "Grès artisanal, lot de deux.", "imageUrl": _img("shop2"), "stock": 40},
|
|
199
|
+
{"name": "Thé vert Sencha", "price": 12.5, "description": "Récolte de printemps, 100 g.", "imageUrl": _img("shop3"), "stock": 87},
|
|
200
|
+
]},
|
|
201
|
+
"followups": [
|
|
202
|
+
{"ask": "Classer les produits par catégorie ?",
|
|
203
|
+
"effects": {"add_fields": {"Product": [("category", "String")]},
|
|
204
|
+
"add_seed_fields": {"Product": {"category": ["Théières", "Tasses", "Thés"]}}}},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "Gestion de tâches",
|
|
209
|
+
"hint": "chaque membre gère ses propres tâches (kanban)",
|
|
210
|
+
"actors": ["Member"],
|
|
211
|
+
"entities": {
|
|
212
|
+
# ACQUIS (point 60) : « title, assignee, due date, and a simple
|
|
213
|
+
# priority signal right on the card » — les deux étaient des
|
|
214
|
+
# questions alors qu'aucune carte kanban ne s'en passe.
|
|
215
|
+
"Task": {"fields": [("title", "String"), ("status", "String"),
|
|
216
|
+
("priority", "String"), ("dueDate", "String"),
|
|
217
|
+
("assignee", "String")],
|
|
218
|
+
"manager": "Member", "readers": [], "public_read": False,
|
|
219
|
+
"public_create": False, "owned": True},
|
|
220
|
+
},
|
|
221
|
+
# POINT 61 : outil interne, ouvert sur un tableau et non sur une page
|
|
222
|
+
# d'accueil. Aucune rubrique standard ne s'impose — le dialogue
|
|
223
|
+
# retombe donc sur l'offre générique plutôt que d'inventer un « à
|
|
224
|
+
# propos » à un kanban d'équipe.
|
|
225
|
+
"relations": [],
|
|
226
|
+
"extra_rules": [
|
|
227
|
+
'rule Task.status oneOf "à faire", "en cours", "terminée"',
|
|
228
|
+
'rule Task.priority oneOf "basse", "moyenne", "haute"',
|
|
229
|
+
],
|
|
230
|
+
"seeds": {}, "sections": [],
|
|
231
|
+
"followups": [
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "Forum / réseau social",
|
|
236
|
+
"hint": "publications, commentaires, appréciations",
|
|
237
|
+
"actors": ["Member", "Moderator"],
|
|
238
|
+
"entities": {
|
|
239
|
+
"Post": {"fields": [("content", "Text"), ("likes", "Integer"),
|
|
240
|
+
("status", "String")],
|
|
241
|
+
"manager": "Member", "readers": [], "public_read": True,
|
|
242
|
+
"public_create": False, "owned": True},
|
|
243
|
+
# Acquis : un signalement ouvre une file de modération ; le
|
|
244
|
+
# modérateur change le statut du post, et publicWhen empêche qu'un
|
|
245
|
+
# post masqué reste lisible par son URL.
|
|
246
|
+
"Report": {"fields": [("reason", "Text"), ("status", "String")],
|
|
247
|
+
"manager": "Moderator", "readers": [], "public_read": False,
|
|
248
|
+
"public_create": False, "owned": False},
|
|
249
|
+
# Le like est possédé par son compte : cela permet à oncePer de
|
|
250
|
+
# composer une unicité solide (compte + post), sans fingerprint
|
|
251
|
+
# fourni par le navigateur.
|
|
252
|
+
"Like": {"fields": [("note", "String")],
|
|
253
|
+
"manager": "Member", "readers": [], "public_read": False,
|
|
254
|
+
"public_create": False, "owned": True},
|
|
255
|
+
},
|
|
256
|
+
"relations": [("Post", "hasMany", "Like"),
|
|
257
|
+
("Member", "hasMany", "Like"),
|
|
258
|
+
("Post", "hasMany", "Report")],
|
|
259
|
+
"extra_rules": [
|
|
260
|
+
'rule Post.status oneOf "published", "hidden"',
|
|
261
|
+
'rule Post.Read publicWhen status "published"',
|
|
262
|
+
'rule Post.Read sharedBy Moderator',
|
|
263
|
+
'rule Like.Create increments Post.likes by 1',
|
|
264
|
+
'rule Like.Create oncePer Member, Post',
|
|
265
|
+
'rule Report.status oneOf "open", "resolved", "dismissed"',
|
|
266
|
+
'rule Report.Create sharedBy Member, Moderator',
|
|
267
|
+
'rule Post.Update sharedBy Member, Moderator',
|
|
268
|
+
],
|
|
269
|
+
"extra_workflows": [
|
|
270
|
+
{"name": "ReportPost", "actor": "Member",
|
|
271
|
+
"actions": [("Create", "Report")]},
|
|
272
|
+
{"name": "ModerateCommunity", "actor": "Moderator",
|
|
273
|
+
"actions": [("Read", "Post"), ("Update", "Post"),
|
|
274
|
+
("Read", "Report"), ("Update", "Report"),
|
|
275
|
+
("Delete", "Report")]},
|
|
276
|
+
],
|
|
277
|
+
# POINT 61 : des règles écrites et visibles depuis l'accueil sont le
|
|
278
|
+
# premier levier de modération cité par les guides de communauté ;
|
|
279
|
+
# le « à propos » dit à qui la communauté s'adresse.
|
|
280
|
+
"sections": [
|
|
281
|
+
{"title": "À propos de la communauté",
|
|
282
|
+
"ask": "qui se retrouve ici et autour de quoi"},
|
|
283
|
+
{"title": "Règles de la communauté",
|
|
284
|
+
"ask": "ce qui est attendu, ce qui est interdit, ce qui arrive en cas d'écart"},
|
|
285
|
+
],
|
|
286
|
+
"seeds": {"Post": [
|
|
287
|
+
{"content": "Premier fil de la communauté — présentez-vous ici.", "likes": 24, "status": "published"},
|
|
288
|
+
{"content": "Quels outils utilisez-vous au quotidien ?", "likes": 51, "status": "published"},
|
|
289
|
+
{"content": "Retour sur le meetup de jeudi, merci à tous !", "likes": 13, "status": "published"},
|
|
290
|
+
]},
|
|
291
|
+
"followups": [
|
|
292
|
+
{"ask": "Permettre les commentaires (chacun gère les siens) ?",
|
|
293
|
+
"effects": {"add_entities": {"Comment": {"fields": [("content", "Text")],
|
|
294
|
+
"manager": "Member", "readers": [], "public_read": True,
|
|
295
|
+
"public_create": False, "owned": True}},
|
|
296
|
+
"add_relations": [("Post", "hasMany", "Comment")]}},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"name": "Petites annonces",
|
|
301
|
+
"hint": "annonces publiques, chaque vendeur gère les siennes",
|
|
302
|
+
"actors": ["Seller", "Buyer"],
|
|
303
|
+
"entities": {
|
|
304
|
+
# ACQUIS (point 60) : l'acheteur regarde le lieu pour savoir si
|
|
305
|
+
# l'objet est proche, et attend de pouvoir joindre le vendeur.
|
|
306
|
+
# Les deux étaient des questions.
|
|
307
|
+
"Listing": {"fields": [("title", "String"), ("price", "Money"),
|
|
308
|
+
("description", "Text"), ("imageUrl", "String"),
|
|
309
|
+
("location", "String")],
|
|
310
|
+
"manager": "Seller", "readers": [], "public_read": True,
|
|
311
|
+
"public_create": False, "owned": True},
|
|
312
|
+
"Inquiry": {"fields": [("email", "Email"), ("content", "Text")],
|
|
313
|
+
"manager": "Seller", "readers": [], "public_read": False,
|
|
314
|
+
"public_create": True, "owned": False},
|
|
315
|
+
# Transaction métier : l'acheteur possède sa demande, son montant
|
|
316
|
+
# est calculé depuis l'annonce, puis le vendeur renseigne la
|
|
317
|
+
# livraison sur la route dédiée après paiement.
|
|
318
|
+
"Purchase": {"fields": [("status", "String"), ("quantity", "Integer"),
|
|
319
|
+
("deliveryAddress", "Text"), ("total", "Money"),
|
|
320
|
+
("deliveryStatus", "String"),
|
|
321
|
+
("trackingNumber", "String")],
|
|
322
|
+
"manager": "Buyer", "readers": ["Seller"],
|
|
323
|
+
"public_read": False, "public_create": False, "owned": True},
|
|
324
|
+
},
|
|
325
|
+
# PAS de fiche photo ici : une photo d'annonce doit être vue par les
|
|
326
|
+
# ACHETEURS, et monl refuse (à juste titre) qu'un fichier déposé soit
|
|
327
|
+
# lisible publiquement. Une photo privée sur une annonce publique
|
|
328
|
+
# donnerait un catalogue dont personne ne voit les images — mesuré :
|
|
329
|
+
# l'acheteur récolte 403 sur la route du fichier. Le dépôt reste
|
|
330
|
+
# proposé là où « privé » est le but (un justificatif de dépense).
|
|
331
|
+
"relations": [("Listing", "hasMany", "Purchase")],
|
|
332
|
+
"extra_rules": [
|
|
333
|
+
'rule Purchase.status oneOf "en attente", "payée", "annulée"',
|
|
334
|
+
'rule Purchase.quantity min 1',
|
|
335
|
+
'rule Purchase.deliveryStatus oneOf "à préparer", "expédiée", "livrée"',
|
|
336
|
+
'rule Purchase.deliveryStatus writableAfterPayment Seller',
|
|
337
|
+
'rule Purchase.trackingNumber writableAfterPayment Seller',
|
|
338
|
+
],
|
|
339
|
+
"extra_workflows": [],
|
|
340
|
+
# POINT 61 : une place de marché entre particuliers doit dire ce
|
|
341
|
+
# qu'elle prend en charge et ce qu'elle laisse aux deux parties — les
|
|
342
|
+
# guides de sécurité en font le point de départ de tout le reste.
|
|
343
|
+
"sections": [
|
|
344
|
+
{"title": "Comment ça marche",
|
|
345
|
+
"ask": "publier, contacter un vendeur, conclure — en quelques phrases"},
|
|
346
|
+
{"title": "Conseils de sécurité",
|
|
347
|
+
"ask": "lieux de rencontre, moyens de paiement, signalement d'une annonce"},
|
|
348
|
+
],
|
|
349
|
+
"seeds": {"Listing": [
|
|
350
|
+
{"title": "Vélo de ville", "price": 120.0, "description": "Bon état, révisé en mai.", "imageUrl": _img("annonce1"), "location": "Lyon"},
|
|
351
|
+
{"title": "Bureau en chêne", "price": 85.0, "description": "140 × 70, à venir chercher.", "imageUrl": _img("annonce2"), "location": "Nantes"},
|
|
352
|
+
{"title": "Appareil photo argentique", "price": 60.0, "description": "Testé, fonctionne.", "imageUrl": _img("annonce3"), "location": "Lille"},
|
|
353
|
+
]},
|
|
354
|
+
"followups": [
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"name": "Réservation de rendez-vous",
|
|
359
|
+
"hint": "prestations publiques, réservations des clients",
|
|
360
|
+
"actors": ["Admin", "Client"],
|
|
361
|
+
"entities": {
|
|
362
|
+
# ACQUIS (point 60) : nom, description, durée et prix forment le
|
|
363
|
+
# socle d'une prestation ; « clear descriptions increase conversion
|
|
364
|
+
# and reduce no-shows ». C'était l'unique question du modèle.
|
|
365
|
+
"Service": {"fields": [("name", "String"), ("duration", "Integer"),
|
|
366
|
+
("price", "Money"), ("description", "Text")],
|
|
367
|
+
"manager": "Admin", "readers": ["Client"], "public_read": True,
|
|
368
|
+
"public_create": False, "owned": False},
|
|
369
|
+
"Booking": {"fields": [("date", "String"), ("notes", "Text")],
|
|
370
|
+
"manager": "Client", "readers": ["Admin"], "public_read": False,
|
|
371
|
+
"public_create": False, "owned": True},
|
|
372
|
+
},
|
|
373
|
+
"relations": [("Service", "hasMany", "Booking")], "extra_rules": [],
|
|
374
|
+
# POINT 61 : la politique d'annulation est donnée comme devant figurer
|
|
375
|
+
# AVEC le formulaire de réservation, pas dans un coin ; horaires et
|
|
376
|
+
# accès sont l'autre information qu'on cherche avant de réserver.
|
|
377
|
+
"sections": [
|
|
378
|
+
{"title": "À propos",
|
|
379
|
+
"ask": "qui vous êtes, votre approche, votre équipe"},
|
|
380
|
+
{"title": "Horaires et accès",
|
|
381
|
+
"ask": "jours et heures d'ouverture, adresse, comment venir"},
|
|
382
|
+
{"title": "Politique d'annulation",
|
|
383
|
+
"ask": "délai pour annuler ou déplacer, frais éventuels, comment prévenir"},
|
|
384
|
+
],
|
|
385
|
+
"seeds": {"Service": [
|
|
386
|
+
{"name": "Coupe & coiffage", "duration": 45, "price": 38.0, "description": "Shampoing, coupe et coiffage inclus."},
|
|
387
|
+
{"name": "Coloration", "duration": 90, "price": 72.0, "description": "Couleur complète, produit professionnel."},
|
|
388
|
+
{"name": "Soin barbe", "duration": 30, "price": 22.0, "description": "Taille, contours et soin à l'huile."},
|
|
389
|
+
]},
|
|
390
|
+
"followups": [],
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"name": "Inventaire / gestion de stock",
|
|
394
|
+
"hint": "articles internes, quantités, emplacements",
|
|
395
|
+
"actors": ["Admin"],
|
|
396
|
+
"entities": {
|
|
397
|
+
"Item": {"fields": [("name", "String"), ("quantity", "Integer"),
|
|
398
|
+
("location", "String")],
|
|
399
|
+
"manager": "Admin", "readers": [], "public_read": False,
|
|
400
|
+
"public_create": False, "owned": False},
|
|
401
|
+
"StockReceipt": {"fields": [("quantity", "Integer"), ("reason", "Text"),
|
|
402
|
+
("occurredAt", "DateTime")],
|
|
403
|
+
"manager": "Admin", "readers": [], "public_read": False,
|
|
404
|
+
"public_create": False, "owned": False},
|
|
405
|
+
"StockIssue": {"fields": [("quantity", "Integer"), ("reason", "Text"),
|
|
406
|
+
("occurredAt", "DateTime")],
|
|
407
|
+
"manager": "Admin", "readers": [], "public_read": False,
|
|
408
|
+
"public_create": False, "owned": False},
|
|
409
|
+
},
|
|
410
|
+
# POINT 61 : outil interne — voir la note du modèle « Gestion de tâches ».
|
|
411
|
+
"relations": [("Item", "hasMany", "StockReceipt"),
|
|
412
|
+
("Item", "hasMany", "StockIssue")],
|
|
413
|
+
"extra_rules": [
|
|
414
|
+
'rule Item.quantity min 0',
|
|
415
|
+
'rule StockReceipt.quantity min 1',
|
|
416
|
+
'rule StockReceipt.Create increments Item.quantity by quantity',
|
|
417
|
+
'rule StockReceipt.occurredAt timestamp',
|
|
418
|
+
'rule StockIssue.quantity min 1',
|
|
419
|
+
'rule StockIssue.Create decrements Item.quantity by quantity',
|
|
420
|
+
'rule StockIssue.occurredAt timestamp',
|
|
421
|
+
],
|
|
422
|
+
"seeds": {"Item": [
|
|
423
|
+
{"name": "Cartons d'expédition", "quantity": 84, "location": "A-01"},
|
|
424
|
+
{"name": "Étiquettes thermiques", "quantity": 240, "location": "A-02"},
|
|
425
|
+
{"name": "Câbles USB-C", "quantity": 18, "location": "B-04"},
|
|
426
|
+
]}, "sections": [],
|
|
427
|
+
"followups": [
|
|
428
|
+
{"ask": "Suivre les fournisseurs (entité liée aux articles) ?",
|
|
429
|
+
"effects": {"add_entities": {"Supplier": {"fields": [("name", "String"), ("email", "Email")],
|
|
430
|
+
"manager": "Admin", "readers": [], "public_read": False,
|
|
431
|
+
"public_create": False, "owned": False}},
|
|
432
|
+
"add_relations": [("Supplier", "hasMany", "Item")]}},
|
|
433
|
+
{"ask": "Ajouter un seuil d'alerte par article ?",
|
|
434
|
+
"effects": {"add_fields": {"Item": [("alertThreshold", "Integer")]}}},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
"name": "Suivi de dépenses personnelles",
|
|
439
|
+
"hint": "chacun ne voit et ne gère que ses dépenses",
|
|
440
|
+
"actors": ["User"],
|
|
441
|
+
"entities": {
|
|
442
|
+
"Expense": {"fields": [("label", "String"), ("amount", "Money"),
|
|
443
|
+
("spentOn", "String"), ("category", "String")],
|
|
444
|
+
"manager": "User", "readers": [], "public_read": False,
|
|
445
|
+
"public_create": False, "owned": True},
|
|
446
|
+
"Budget": {"fields": [("name", "String"), ("limit", "Money"),
|
|
447
|
+
("spent", "Money")],
|
|
448
|
+
"manager": "User", "readers": [], "public_read": False,
|
|
449
|
+
"public_create": False, "owned": True},
|
|
450
|
+
},
|
|
451
|
+
# POINT 61 : outil personnel, sans visiteur à convaincre.
|
|
452
|
+
"relations": [("Budget", "hasMany", "Expense")],
|
|
453
|
+
"extra_rules": [
|
|
454
|
+
'rule Expense.amount min 0',
|
|
455
|
+
'rule Budget.limit min 0',
|
|
456
|
+
'rule Budget.spent sumOf Expense.amount',
|
|
457
|
+
],
|
|
458
|
+
# Un budget personnel sert à piloter les dépenses, pas à encaisser un
|
|
459
|
+
# paiement. Cette décision évite que le détecteur générique transforme
|
|
460
|
+
# les deux montants du suivi en faux catalogue de paiement.
|
|
461
|
+
"accept_payments": False,
|
|
462
|
+
"seeds": {}, "sections": [], "followups": [],
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"name": "Classement communautaire",
|
|
466
|
+
"hint": "liste publique, votes qui font monter le score",
|
|
467
|
+
"actors": ["Participant"],
|
|
468
|
+
"entities": {
|
|
469
|
+
"Entry": {"fields": [("name", "String"), ("tagline", "Text"),
|
|
470
|
+
("score", "Integer"), ("submittedOn", "DateTime")],
|
|
471
|
+
"manager": "Participant", "readers": [], "public_read": True,
|
|
472
|
+
"public_create": False, "owned": False},
|
|
473
|
+
"Vote": {"fields": [("note", "String")],
|
|
474
|
+
"manager": "Participant", "readers": [], "public_read": False,
|
|
475
|
+
"public_create": False, "owned": False},
|
|
476
|
+
},
|
|
477
|
+
"relations": [("Participant", "hasMany", "Vote"),
|
|
478
|
+
("Entry", "hasMany", "Vote")],
|
|
479
|
+
"extra_rules": [
|
|
480
|
+
"rule Vote.Create increments Entry.score by 1",
|
|
481
|
+
"rule Vote.Create oncePer Participant, Entry",
|
|
482
|
+
"rule Entry.submittedOn timestamp",
|
|
483
|
+
],
|
|
484
|
+
# POINT 61 : un classement n'est crédible que si la règle du vote est
|
|
485
|
+
# écrite — qui peut voter, combien de fois, comment on départage.
|
|
486
|
+
"sections": [
|
|
487
|
+
{"title": "À propos du classement",
|
|
488
|
+
"ask": "ce qui est classé, par qui, dans quel but"},
|
|
489
|
+
{"title": "Comment fonctionne le vote",
|
|
490
|
+
"ask": "qui peut voter, combien de fois, comment on départage une égalité"},
|
|
491
|
+
],
|
|
492
|
+
"seeds": {"Entry": [
|
|
493
|
+
{"name": "Projet Solaris", "tagline": "Panneaux solaires imprimés en 3D.", "score": 128},
|
|
494
|
+
{"name": "Réseau Maillage", "tagline": "Internet communautaire hors réseau.", "score": 203},
|
|
495
|
+
{"name": "Atelier Récup", "tagline": "Réparation d'objets entre voisins.", "score": 45},
|
|
496
|
+
]},
|
|
497
|
+
"followups": [],
|
|
498
|
+
},
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
FREE_MODE_LABEL = "Partir de zéro (décrire librement mes entités)"
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def apply_effects(template, effects):
|
|
505
|
+
"""Fusionne les effets d'une question de suivi acceptée dans le modèle.
|
|
506
|
+
Pure fonction sur des dicts — testable sans dialogue."""
|
|
507
|
+
for ent, fields in effects.get("add_fields", {}).items():
|
|
508
|
+
template["entities"][ent]["fields"].extend(fields)
|
|
509
|
+
for ent, meta in effects.get("add_entities", {}).items():
|
|
510
|
+
template["entities"][ent] = meta
|
|
511
|
+
for actor in effects.get("add_actors", []):
|
|
512
|
+
if actor not in template["actors"]:
|
|
513
|
+
template["actors"].append(actor)
|
|
514
|
+
template["relations"].extend(effects.get("add_relations", []))
|
|
515
|
+
template["extra_rules"].extend(effects.get("add_rules", []))
|
|
516
|
+
for ent, rows in effects.get("add_seeds", {}).items():
|
|
517
|
+
template["seeds"].setdefault(ent, []).extend(rows)
|
|
518
|
+
# Enrichir les lignes de seed existantes avec les nouveaux champs
|
|
519
|
+
# (une valeur réaliste par ligne, dans l'ordre).
|
|
520
|
+
for ent, per_field in effects.get("add_seed_fields", {}).items():
|
|
521
|
+
for field, values in per_field.items():
|
|
522
|
+
# strict=True : un modèle qui fournit 2 valeurs pour 3 lignes de
|
|
523
|
+
# seed doit ÉCHOUER ici, pas produire en silence une ligne à qui
|
|
524
|
+
# il manque un champ que les autres ont (le catalogue est testé
|
|
525
|
+
# modèle par modèle, la CI attrape donc l'erreur immédiatement).
|
|
526
|
+
for row, value in zip(template["seeds"].get(ent, []), values,
|
|
527
|
+
strict=True):
|
|
528
|
+
row[field] = value
|
|
529
|
+
return template
|