Compare commits

...
Author SHA1 Message Date
Archicratia b9629b43ff fix(actions): silence anno workflow on proposer tickets
CI / build-and-anchors (push) Successful in 49s
CI / build-and-anchors (pull_request) Successful in 44s
SMOKE / smoke (push) Successful in 9s
2026-03-16 11:06:11 +01:00
Archicratia f2e4ae5ac2 fix(actions): make proposer queue runtime-safe
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 42s
SMOKE / smoke (push) Successful in 7s
2026-03-16 00:58:10 +01:00
Archicratia 71baf0f6da Merge pull request 'fix(actions): repair proposer workflow yaml' (#253) from hotfix/fix-proposer-workflow into main
CI / build-and-anchors (push) Successful in 48s
Proposer Apply (Queue) / apply-proposer (push) Failing after 4s
Deploy staging+live (annotations) / deploy (push) Successful in 1m0s
SMOKE / smoke (push) Successful in 4s
Reviewed-on: #253
2026-03-16 00:42:17 +01:00
Archicratia d02b6fc347 fix(actions): repair proposer workflow yaml
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 45s
SMOKE / smoke (push) Successful in 9s
2026-03-16 00:38:46 +01:00
Archicratia 431f1e347b Merge pull request 'chore(editorial): harden proposer queue and apply-ticket' (#252) from chore/editorial-hardening-v1-clean into main
CI / build-and-anchors (push) Successful in 1m6s
Deploy staging+live (annotations) / deploy (push) Waiting to run
SMOKE / smoke (push) Successful in 39s
Reviewed-on: #252
2026-03-16 00:02:31 +01:00
Archicratia ab6f45ed5c chore(editorial): harden proposer queue and apply-ticket
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 46s
SMOKE / smoke (push) Successful in 6s
2026-03-15 23:58:11 +01:00
Archicratia 02c060d239 Merge pull request 'chore/reset-from-docx-clean-base' (#251) from chore/reset-from-docx-clean-base into main
CI / build-and-anchors (push) Successful in 45s
SMOKE / smoke (push) Successful in 8s
Deploy staging+live (annotations) / deploy (push) Successful in 9m50s
Reviewed-on: #251
2026-03-15 21:19:24 +01:00
Archicratia be2029de82 ci(anchors): accept intentional chapitre 1 anchor reset
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 44s
SMOKE / smoke (push) Successful in 4s
2026-03-15 21:11:20 +01:00
Archicratia e148eaeaf3 content(archicrat-ia): refresh chapitre 1 from official docx
CI / build-and-anchors (push) Failing after 42s
SMOKE / smoke (push) Successful in 7s
2026-03-15 20:50:35 +01:00
Archicratia c63a1e6ce4 chore(reset): reimport chapitre 1 from docx clean base
CI / build-and-anchors (push) Successful in 44s
SMOKE / smoke (push) Successful in 7s
2026-03-15 20:33:49 +01:00
Archicratia b3a73a7781 Merge pull request 'chore(reset): reimport chapitre 1 from docx clean base' (#250) from chore/reset-from-docx-clean-base into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 9m32s
SMOKE / smoke (push) Successful in 8s
Reviewed-on: #250
2026-03-15 15:31:37 +01:00
Archicratia 1968585d0f chore(reset): reimport chapitre 1 from docx clean base
CI / build-and-anchors (push) Successful in 57s
CI / build-and-anchors (pull_request) Successful in 50s
SMOKE / smoke (push) Successful in 5s
2026-03-15 15:29:09 +01:00
Archicratia b33c758411 Merge pull request 'proposer: apply ticket #236' (#243) from bot/proposer-236-20260315-112808 into main
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 12m1s
CI / build-and-anchors (pull_request) Successful in 43s
SMOKE / smoke (push) Successful in 4s
Reviewed-on: #243
2026-03-15 12:55:33 +01:00
Archicratia afa543125c Merge pull request 'proposer: apply ticket #235' (#242) from bot/proposer-235-20260315-112515 into main
CI / build-and-anchors (push) Successful in 51s
Deploy staging+live (annotations) / deploy (push) Has started running
SMOKE / smoke (push) Successful in 7s
Reviewed-on: #242
2026-03-15 12:47:26 +01:00
archicratie-bot 0d0252cac0 edit: apply ticket #236 (/archicrat-ia/chapitre-1/#p-400-8959d62e)
CI / build-and-anchors (push) Successful in 45s
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-15 11:28:36 +00:00
archicratie-bot a8bd9aeed5 edit: apply ticket #235 (/archicrat-ia/chapitre-1/#p-139-caa4e99d)
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 47s
SMOKE / smoke (push) Successful in 3s
2026-03-15 11:25:43 +00:00
Archicratia d277c61afd Merge pull request 'proposer: apply ticket #229' (#232) from bot/proposer-229-20260315-105537 into main
CI / build-and-anchors (push) Successful in 51s
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Successful in 10s
Reviewed-on: #232
2026-03-15 12:24:34 +01:00
archicratie-bot 86479952d1 edit: apply ticket #229 (/archicrat-ia/chapitre-1/#p-10-1a706744)
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 42s
SMOKE / smoke (push) Successful in 5s
2026-03-15 10:56:03 +00:00
Archicratia c94024a8ae Merge pull request 'feat(glossaire): add advanced external paradigms from chapter 3' (#228) from feat/glossaire-archicrations-cartographie into main
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 9m53s
SMOKE / smoke (push) Successful in 7s
Reviewed-on: #228
2026-03-15 10:08:48 +01:00
Archicratia 70611d16f8 feat(glossaire): add advanced external paradigms from chapter 3
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 44s
SMOKE / smoke (push) Successful in 11s
2026-03-15 10:05:32 +01:00
Archicratia 354db231b8 Merge pull request 'feat(glossaire): enrichit la cartographie archicratique et les archicrations' (#227) from feat/glossaire-archicrations-cartographie into main
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 9m37s
SMOKE / smoke (push) Successful in 14s
Reviewed-on: #227
2026-03-14 19:57:38 +01:00
Archicratia 9d8d60d00f feat(glossaire): enrichit la cartographie archicratique et les archicrations
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 38s
SMOKE / smoke (push) Successful in 4s
2026-03-14 19:55:16 +01:00
Archicratia f5d25abbec Merge pull request 'feat(glossaire): add advanced external paradigms from chapter 3' (#226) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 7m13s
SMOKE / smoke (push) Successful in 10s
Reviewed-on: #226
2026-03-13 18:55:10 +01:00
Archicratia 8e9f7314f5 feat(glossaire): add advanced external paradigms from chapter 3
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 40s
SMOKE / smoke (push) Successful in 3s
2026-03-13 18:48:33 +01:00
Archicratia 03b88b944d Merge pull request 'feat(glossaire): integrate paradigms portal into glossary navigation' (#225) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 43s
SMOKE / smoke (push) Successful in 21s
Deploy staging+live (annotations) / deploy (push) Successful in 7m38s
Reviewed-on: #225
2026-03-13 18:06:43 +01:00
Archicratia 385c36f660 feat(glossaire): integrate paradigms portal into glossary navigation
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Successful in 7s
2026-03-13 18:04:13 +01:00
Archicratia cfa092cd38 Merge pull request 'feat(glossaire): introduce doctrine as ontology type' (#224) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 8m36s
SMOKE / smoke (push) Successful in 11s
Reviewed-on: #224
2026-03-13 16:47:52 +01:00
Archicratia 1a762f8f54 feat(glossaire): introduce doctrine as ontology type
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (pull_request) Successful in 38s
2026-03-13 16:45:29 +01:00
Archicratia fbdaf72775 Merge pull request 'refactor(glossaire): introduce dedicated GlossaryLayout wrapper' (#223) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Successful in 8m12s
SMOKE / smoke (push) Successful in 10s
Reviewed-on: #223
2026-03-13 15:15:32 +01:00
Archicratia 67128a9ca1 refactor(glossaire): introduce dedicated GlossaryLayout wrapper
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 4s
2026-03-13 15:11:39 +01:00
Archicratia 898759db3d Merge pull request 'feat(glossaire): refactor core archicratic concepts' (#222) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 8m6s
SMOKE / smoke (push) Successful in 9s
Reviewed-on: #222
2026-03-13 14:36:40 +01:00
Archicratia 4f009a9557 feat(glossaire): refactor core archicratic concepts
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 37s
SMOKE / smoke (push) Successful in 4s
2026-03-13 14:32:41 +01:00
Archicratia 378d0981f0 Merge pull request 'refactor(glossaire): disable reading-only controls in glossary' (#221) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Successful in 9m36s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #221
2026-03-12 19:27:16 +01:00
Archicratia 8f3702f803 refactor(glossaire): disable reading-only controls in glossary
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Successful in 5s
2026-03-12 19:23:50 +01:00
Archicratia cfd303fc85 Merge pull request 'Nouveaux Ajouts - Glossaire' (#220) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 8m26s
SMOKE / smoke (push) Successful in 8s
Reviewed-on: #220
2026-03-12 17:45:03 +01:00
Archicratia 0fc0976f8a refactor(glossaire): polish contextual aside navigation
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-12 17:42:16 +01:00
Archicratia e247ea8ead Merge pull request 'feat(glossaire): strengthen conceptual and paradigmatic cross-links' (#219) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 7m49s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #219
2026-03-12 14:44:17 +01:00
Archicratia 0c57c4bc6d feat(glossaire): strengthen conceptual and paradigmatic cross-links
CI / build-and-anchors (push) Successful in 37s
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-12 14:39:46 +01:00
Archicratia 9b7998e1c3 Merge pull request 'feat(glossaire): add external paradigms and theoretical landscape' (#218) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 7m44s
SMOKE / smoke (push) Successful in 6s
Reviewed-on: #218
2026-03-12 13:57:00 +01:00
Archicratia 8997a00413 feat(glossaire): add external paradigms and theoretical landscape
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 10s
2026-03-12 13:53:11 +01:00
Archicratia a2e6f6185f Merge pull request 'fix(glossaire): adjust section anchor offset for sticky header' (#217) from feat/glossaire-paradigmes-externes into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 9m8s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #217
2026-03-12 13:35:39 +01:00
Archicratia c2715b01d7 fix(glossaire): adjust section anchor offset for sticky header
CI / build-and-anchors (push) Successful in 39s
CI / build-and-anchors (pull_request) Successful in 43s
SMOKE / smoke (push) Successful in 5s
2026-03-12 13:31:07 +01:00
Archicratia 6f09dfcd12 Merge pull request 'feat(glossaire): extend taxonomy and align Astro 6 content config' (#216) from feat/glossaire-paradigmes into main
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 8m58s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #216
2026-03-12 12:08:15 +01:00
Archicratia bb9f55a3b5 feat(glossaire): extend taxonomy and align Astro 6 content config
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Successful in 6s
2026-03-12 12:04:46 +01:00
Archicratia 298ee7492c Merge pull request 'EditionToc Modif' (#215) from chore/astro6-upgrade into main
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 7m16s
SMOKE / smoke (push) Successful in 9s
Reviewed-on: #215
2026-03-11 17:58:17 +01:00
Archicratia 37cb836246 chore(astro): upgrade to Astro 6 and fix dynamic collection routes
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 44s
SMOKE / smoke (push) Successful in 3s
2026-03-11 17:56:53 +01:00
Archicratia 19e3318125 Merge pull request 'chore(astro): upgrade to astro 6 with legacy content compatibility' (#214) from chore/astro6-upgrade into main
CI / build-and-anchors (push) Successful in 47s
SMOKE / smoke (push) Successful in 9s
Deploy staging+live (annotations) / deploy (push) Successful in 9m33s
Reviewed-on: #214
2026-03-11 17:19:04 +01:00
Archicratia 683b02f4a0 chore(astro): upgrade to astro 6 with legacy content compatibility
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 38s
SMOKE / smoke (push) Successful in 11s
2026-03-11 17:17:02 +01:00
Archicratia 20aecc30b1 Merge pull request 'feat(glossary): add core glossarial taxonomy and foundational entries' (#213) from feat/glossaire-core2 into main
CI / build-and-anchors (push) Successful in 45s
SMOKE / smoke (push) Successful in 18s
Deploy staging+live (annotations) / deploy (push) Successful in 8m42s
Reviewed-on: #213
2026-03-11 13:04:24 +01:00
Archicratia daf57aa152 feat(glossary): add core glossarial taxonomy and foundational entries
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 45s
SMOKE / smoke (push) Successful in 5s
2026-03-11 12:50:17 +01:00
Archicratia bfd693de92 Merge pull request 'refactor(import): unify core editorial manifest' (#212) from refactor/unify-manifest-core into main
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Successful in 8m0s
SMOKE / smoke (push) Successful in 10s
Reviewed-on: #212
2026-03-11 11:36:47 +01:00
Archicratia ea2ad0017b refactor(import): unify core editorial manifest
CI / build-and-anchors (push) Successful in 35s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 3s
2026-03-11 11:33:37 +01:00
Archicratia 82e7473cac Merge pull request 'fix(content): declare commencer collection and remove implicit ia collection' (#211) from fix/content-collections-stability into main
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 8m57s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #211
2026-03-11 11:09:39 +01:00
Archicratia 315523e80f refactor(site): depublish non-core sections and refresh anchors baseline
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 40s
SMOKE / smoke (push) Successful in 6s
2026-03-11 11:07:21 +01:00
Archicratia 569b6de154 fix(content): declare commencer collection and remove implicit ia collection
CI / build-and-anchors (push) Failing after 41s
CI / build-and-anchors (pull_request) Failing after 46s
SMOKE / smoke (push) Successful in 5s
2026-03-11 10:54:27 +01:00
Archicratia 95f8159554 Merge pull request 'proposer: apply ticket #209' (#210) from bot/proposer-209-20260311-090431 into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 8m16s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #210
2026-03-11 10:06:49 +01:00
archicratie-bot 5698c494f1 edit: apply ticket #209 (/cas-ia/introduction/#p-16-615e3d61)
CI / build-and-anchors (push) Successful in 37s
CI / build-and-anchors (pull_request) Successful in 43s
SMOKE / smoke (push) Successful in 3s
2026-03-11 09:04:53 +00:00
Archicratia e640e66b8d Merge pull request 'proposer: apply ticket #207' (#208) from bot/proposer-207-20260311-082736 into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 7m40s
SMOKE / smoke (push) Successful in 6s
Reviewed-on: #208
2026-03-11 09:29:21 +01:00
archicratie-bot 9be7d170c6 edit: apply ticket #207 (/cas-ia/introduction/#p-10-ceba29a2)
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 40s
SMOKE / smoke (push) Successful in 2s
2026-03-11 08:27:58 +00:00
Archicratia c2c98c516b Merge pull request 'refactor(editorial): recentrer le site sur le noyau archicratique' (#206) from refactor/recentrage-noyau-archicratique into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 8m54s
SMOKE / smoke (push) Successful in 9s
Reviewed-on: #206
2026-03-10 20:43:16 +01:00
Archicratia 32554f5998 refactor(editorial): recentrer le site sur le noyau archicratique
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 6s
2026-03-10 20:36:33 +01:00
Archicratia 308f4f92bc Merge pull request 'chore: merge main into fix branch' (#205) from fix/annotations-index-fail-open-20260304-223909 into main
CI / build-and-anchors (push) Successful in 35s
Deploy staging+live (annotations) / deploy (push) Successful in 53s
SMOKE / smoke (push) Successful in 4s
Reviewed-on: #205
2026-03-06 11:18:04 +01:00
Archicratia 4dfd3b026b chore: merge main into fix branch
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 38s
SMOKE / smoke (push) Successful in 5s
2026-03-06 11:14:48 +01:00
Archicratia c93f274f41 Merge pull request 'fix(annotations): fail-open when src/annotations is missing' (#204) from fix/annotations-index-fail-open-20260304-223909 into main
CI / build-and-anchors (push) Successful in 46s
Deploy staging+live (annotations) / deploy (push) Successful in 8m58s
SMOKE / smoke (push) Successful in 14s
Reviewed-on: #204
2026-03-04 22:42:13 +01:00
Archicratia dfa311fb5b fix(annotations): fail-open when src/annotations is missing
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Successful in 18s
2026-03-04 22:39:09 +01:00
Archicratia 3ef1dc2801 Merge pull request 'fix(annotations): fail-open when src/annotations missing + keep dir tracked' (#203) from chore/remove-test-anno-media-20260304-204810 into main
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 8m47s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #203
2026-03-04 21:39:04 +01:00
Archicratia 435e41ed4d fix(annotations): fail-open when src/annotations missing + keep dir tracked
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Successful in 7s
2026-03-04 21:31:53 +01:00
Archicratia 8825932159 Merge pull request 'chore: keep public/media directory (gitkeep)' (#202) from chore/remove-test-anno-media-20260304-204810 into main
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 34s
SMOKE / smoke (push) Successful in 10s
Reviewed-on: #202
2026-03-04 21:06:45 +01:00
Archicratia b55decbea4 chore: keep public/media directory (gitkeep)
CI / build-and-anchors (push) Successful in 46s
CI / build-and-anchors (pull_request) Successful in 38s
SMOKE / smoke (push) Successful in 4s
2026-03-04 21:03:55 +01:00
Archicratia 414a848db3 Merge pull request 'chore: keep src/annotations directory (gitkeep)' (#201) from chore/remove-test-anno-media-20260304-204810 into main
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 39s
SMOKE / smoke (push) Successful in 5s
Reviewed-on: #201
2026-03-04 21:03:39 +01:00
Archicratia cbd4f3a57f chore: keep src/annotations directory (gitkeep)
CI / build-and-anchors (push) Successful in 36s
CI / build-and-anchors (pull_request) Successful in 36s
SMOKE / smoke (push) Successful in 5s
2026-03-04 21:02:19 +01:00
Archicratia 49f8d6a95e Merge pull request 'chore: remove test annotations and media' (#200) from chore/remove-test-anno-media-20260304-204810 into main
CI / build-and-anchors (push) Failing after 34s
Deploy staging+live (annotations) / deploy (push) Successful in 47s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #200
2026-03-04 20:59:21 +01:00
Archicratia 5afa5cbfda chore: remove test annotations and media
CI / build-and-anchors (push) Failing after 32s
CI / build-and-anchors (pull_request) Failing after 34s
SMOKE / smoke (push) Successful in 3s
2026-03-04 20:48:26 +01:00
Archicratia a1b1df38ba Merge pull request 'chore: remove test annotations and media' (#196) from chore/remove-test-anno-media-20260304-202811 into main
CI / build-and-anchors (push) Failing after 33s
Deploy staging+live (annotations) / deploy (push) Failing after 54s
CI / build-and-anchors (pull_request) Failing after 30s
SMOKE / smoke (push) Successful in 4s
Reviewed-on: #196
2026-03-04 20:35:47 +01:00
Archicratia d3f7d74da7 Merge pull request 'chore: remove test annotations and media' (#197) from chore/remove-test-anno-media-20260304-202451 into main
CI / build-and-anchors (push) Has started running
Deploy staging+live (annotations) / deploy (push) Has started running
SMOKE / smoke (push) Successful in 8s
Reviewed-on: #197
2026-03-04 20:35:40 +01:00
Archicratia 6919190107 chore: remove test annotations and media
CI / build-and-anchors (push) Failing after 36s
CI / build-and-anchors (pull_request) Failing after 36s
SMOKE / smoke (push) Successful in 6s
2026-03-04 20:28:29 +01:00
Archicratia 021ef5abd7 chore: remove test annotations and media
CI / build-and-anchors (push) Failing after 31s
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (pull_request) Failing after 38s
2026-03-04 20:25:03 +01:00
Archicratia 76cdc85f9c Merge pull request 'ci(deploy): treat src/public/package changes as FULL rebuild' (#193) from chore/supprimer-annotations-20260304-191252 into main
CI / build-and-anchors (push) Failing after 35s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #193
2026-03-04 19:22:36 +01:00
Archicratia f2f6df2127 ci(deploy): treat src/public/package changes as FULL rebuild
CI / build-and-anchors (push) Failing after 36s
CI / build-and-anchors (pull_request) Failing after 37s
SMOKE / smoke (push) Successful in 4s
2026-03-04 19:13:25 +01:00
Archicratia dfe13757f7 Merge pull request 'ci(deploy): treat src/public/package changes as FULL rebuild' (#191) from chore/fix-deploy-gate-full-src-public-20260304-181357 into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 34s
SMOKE / smoke (push) Successful in 8s
Reviewed-on: #191
2026-03-04 18:16:30 +01:00
Archicratia 148ac997df ci(deploy): treat src/public/package changes as FULL rebuild
CI / build-and-anchors (push) Successful in 40s
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (pull_request) Successful in 32s
2026-03-04 18:14:48 +01:00
Archicratia 84492d2741 Merge pull request 'style(mobile): widen reading in landscape on small screens (css-only)' (#190) from feat/mobile-landscape-reading-YYYYMMDD into main
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 34s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #190
2026-03-04 17:39:11 +01:00
Archicratia 81baadd57f style(mobile): widen reading in landscape on small screens (css-only)
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 36s
SMOKE / smoke (push) Successful in 9s
2026-03-04 17:36:56 +01:00
Archicratia 63d0ffc5fc Merge pull request 'chore: <résumé clair de la modif>' (#189) from chore/xxx-20260303-221031 into main
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 29s
SMOKE / smoke (push) Successful in 7s
Reviewed-on: #189
2026-03-03 22:15:31 +01:00
Archicratia 24143fc2c4 chore: <résumé clair de la modif>
CI / build-and-anchors (push) Successful in 36s
CI / build-and-anchors (pull_request) Successful in 37s
SMOKE / smoke (push) Successful in 3s
2026-03-03 22:12:01 +01:00
Archicratia 55370b704f Merge pull request 'chore: cleanup testA/testB markers' (#188) from chore/cleanup-testA-testB-20260303-213115 into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 8m35s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #188
2026-03-03 21:38:00 +01:00
Archicratia b8a3ce1337 chore: cleanup testA/testB markers
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 40s
SMOKE / smoke (push) Successful in 5s
2026-03-03 21:36:26 +01:00
Archicratia 7f9baedf41 Merge pull request 'test: B hotpatch-auto gate (touch src/annotations)' (#187) from testB-hotpatch-auto-20260303-211919 into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 38s
SMOKE / smoke (push) Successful in 10s
Reviewed-on: #187
2026-03-03 21:22:04 +01:00
Archicratia 1adbe1c7a3 test: B hotpatch-auto gate (touch src/annotations)
CI / build-and-anchors (push) Successful in 37s
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (pull_request) Successful in 37s
2026-03-03 21:20:07 +01:00
Archicratia 107a26352f Merge pull request 'test: A full-auto gate (touch src/content)' (#186) from testA-full-auto-20260303-205324 into main
CI / build-and-anchors (push) Successful in 46s
SMOKE / smoke (push) Successful in 16s
Deploy staging+live (annotations) / deploy (push) Successful in 7m5s
Reviewed-on: #186
2026-03-03 20:57:09 +01:00
Archicratia 1c2b9ddbb6 test: A full-auto gate (touch src/content)
CI / build-and-anchors (push) Successful in 47s
SMOKE / smoke (push) Successful in 10s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-03 20:54:46 +01:00
Archicratia be99460d4d Merge pull request 'ci(deploy): fix gate bash + robust merge-proof diff + full/hotpatch auto' (#185) from chore/fix-deploy-gate-bash-20260303-204603 into main
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Successful in 44s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #185
2026-03-03 20:48:02 +01:00
Archicratia 9e1b704aa6 ci(deploy): fix gate bash + robust merge-proof diff + full/hotpatch auto
CI / build-and-anchors (push) Successful in 35s
CI / build-and-anchors (pull_request) Successful in 44s
SMOKE / smoke (push) Successful in 4s
2026-03-03 20:46:03 +01:00
Archicratia 941fbf5845 Merge pull request 'ci(deploy): make gate merge-proof (diff before..after)' (#184) from chore/deploy-gate-mergeproof-20260303-195827 into main
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Failing after 35s
SMOKE / smoke (push) Successful in 6s
Reviewed-on: #184
2026-03-03 20:00:37 +01:00
Archicratia 0b4a31a432 ci(deploy): make gate merge-proof (diff before..after)
CI / build-and-anchors (push) Successful in 43s
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (pull_request) Successful in 37s
2026-03-03 19:58:27 +01:00
Archicratia c617dc3979 Merge pull request 'test: B hotpatch-auto gate (touch src/annotations)' (#183) from testB-hotpatch-auto-20260303-183745 into main
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 7m50s
SMOKE / smoke (push) Successful in 7s
Reviewed-on: #183
2026-03-03 18:40:39 +01:00
Archicratia 1b95161de0 test: B hotpatch-auto gate (touch src/annotations)
CI / build-and-anchors (push) Successful in 42s
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-03 18:38:52 +01:00
Archicratia ebd976bd46 Merge pull request 'chore: cleanup testA/testB markers' (#182) from chore/cleanup-testA-testB-20260303-175846 into main
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 8m55s
SMOKE / smoke (push) Successful in 11s
Reviewed-on: #182
2026-03-03 18:01:40 +01:00
Archicratia f8d57d8fe0 chore: cleanup testA/testB markers
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 38s
SMOKE / smoke (push) Successful in 6s
2026-03-03 18:00:01 +01:00
Archicratia 09a4d2c472 Merge pull request 'test: B hotpatch-auto gate (touch src/annotations)' (#181) from testB-hotpatch-auto-20260303-174037 into main
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 9s
Deploy staging+live (annotations) / deploy (push) Successful in 8m0s
Reviewed-on: #181
2026-03-03 17:43:31 +01:00
Archicratia 1f6dc874d0 test: B hotpatch-auto gate (touch src/annotations)
CI / build-and-anchors (push) Successful in 36s
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (pull_request) Successful in 36s
2026-03-03 17:42:04 +01:00
Archicratia 4dd63945ee Merge pull request 'test: A full-auto gate (touch src/content)' (#180) from testA-full-auto-20260303-173032 into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #180
2026-03-03 17:36:14 +01:00
Archicratia ba64b0694b test: A full-auto gate (touch src/content)
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Successful in 4s
2026-03-03 17:34:32 +01:00
Archicratia 58e5ceda59 Merge pull request 'ci(deploy): auto FULL when content/anchors/pages/scripts change' (#179) from chore/deploy-gate-full-on-content-anchors-pages-scripts-20260303-171645 into main
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Successful in 7m35s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #179
2026-03-03 17:20:36 +01:00
Archicratia 08f826ee01 ci(deploy): auto FULL when content/anchors/pages/scripts change
CI / build-and-anchors (push) Successful in 46s
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-03 17:16:45 +01:00
Archicratia 3358d280ec Merge pull request 'edit: apply ticket #174 (/archicrat-ia/chapitre-3/#p-1-60c7ea48)' (#178) from chore/migrate-content-archicrat-ia-root-20260303-132407 into main
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
SMOKE / smoke (push) Successful in 9s
Reviewed-on: #178
2026-03-03 15:04:07 +01:00
Archicratia 9cb0d5e416 content: wire archicrat-ia as first-class collection (routes + toc + schema)
CI / build-and-anchors (push) Successful in 38s
CI / build-and-anchors (pull_request) Successful in 37s
SMOKE / smoke (push) Successful in 5s
2026-03-03 15:02:50 +01:00
Archicratia a46f058917 edit: apply ticket #174 (/archicrat-ia/chapitre-3/#p-1-60c7ea48)
CI / build-and-anchors (push) Failing after 39s
CI / build-and-anchors (pull_request) Failing after 36s
SMOKE / smoke (push) Successful in 6s
2026-03-03 14:27:35 +01:00
Archicratia 604b2199da Merge pull request 'ci: fix proposer apply workflow (checkout before APP_DIR detect)' (#177) from chore/fix-proposer-apply-checkout-order-20260303-122611 into main
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 1m6s
SMOKE / smoke (push) Successful in 14s
Reviewed-on: #177
2026-03-03 12:32:34 +01:00
Archicratia d153f71be6 ci: fix proposer apply workflow (checkout before APP_DIR detect)
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 5s
2026-03-03 12:26:11 +01:00
Archicratia 8f64e4b098 Merge pull request 'ci: fix proposer workflow (auto APP_DIR + guards)' (#176) from chore/fix-proposer-workflow-appdir-20260303-115843 into main
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 1m10s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #176
2026-03-03 12:01:18 +01:00
Archicratia 459bf195d8 ci: fix proposer workflow (auto APP_DIR + guards)
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 42s
SMOKE / smoke (push) Successful in 7s
2026-03-03 11:58:43 +01:00
Archicratia 0c46b0d19b Merge pull request 'ci: add Proposer Apply workflow (apply-ticket -> PR bot)' (#175) from chore/proposer-apply-workflow-20260302-234255 into main
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 11s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
Reviewed-on: #175
2026-03-02 23:49:57 +01:00
Archicratia bfbdc7b688 ci: add Proposer Apply workflow (apply-ticket -> PR bot)
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 43s
SMOKE / smoke (push) Successful in 6s
2026-03-02 23:42:55 +01:00
Archicratia 8fd53dd4d2 Merge pull request 'anno: apply ticket #172' (#173) from bot/anno-172-20260302-200155 into main
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #173
2026-03-02 21:03:36 +01:00
archicratie-bot c8bbee4f74 anno: apply ticket #172 (archicrat-ia/chapitre-3#p-1-60c7ea48 type/reference)
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 5s
2026-03-02 20:01:55 +00:00
Archicratia 04cdf54eb7 Merge pull request 'anno: apply ticket #169' (#171) from bot/anno-169-20260302-195320 into main
CI / build-and-anchors (push) Successful in 43s
SMOKE / smoke (push) Successful in 11s
Deploy staging+live (annotations) / deploy (push) Successful in 56s
Reviewed-on: #171
2026-03-02 20:59:08 +01:00
archicratie-bot d6bf645ae9 anno: apply ticket #169 (archicrat-ia/chapitre-3#p-0-ace27175 type/reference)
CI / build-and-anchors (push) Successful in 47s
CI / build-and-anchors (pull_request) Successful in 42s
SMOKE / smoke (push) Successful in 4s
2026-03-02 19:53:21 +00:00
Archicratia 1ca6bcbd81 Merge pull request 'ci: make anno apply/reject gates API-hard (approved/rejected label present)' (#170) from chore/fix-anno-apply-approved-gate-v1 into main
CI / build-and-anchors (push) Successful in 42s
SMOKE / smoke (push) Successful in 13s
Deploy staging+live (annotations) / deploy (push) Successful in 46s
Reviewed-on: #170
2026-03-02 20:17:58 +01:00
Archicratia dec5f8eba7 ci: make anno apply/reject gates API-hard (approved/rejected label present)
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 7s
2026-03-02 20:12:29 +01:00
Archicratia 716c887045 Merge pull request 'ci: fix auto-label (no array fallback, retries, post-verify)' (#167) from chore/fix-auto-label-422-v1 into main
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 45s
SMOKE / smoke (push) Successful in 9s
Reviewed-on: #167
2026-03-02 19:37:43 +01:00
Archicratia 9b1789a164 ci: fix auto-label (no array fallback, retries, post-verify)
CI / build-and-anchors (push) Successful in 50s
CI / build-and-anchors (pull_request) Successful in 43s
SMOKE / smoke (push) Successful in 7s
2026-03-02 19:36:09 +01:00
Archicratia 17fa39c7ff Merge pull request 'ci: hard-gate anno apply/reject + fix JSON parsing' (#164) from chore/fix-anno-workflows-jsonparse-v3 into main
CI / build-and-anchors (push) Successful in 45s
SMOKE / smoke (push) Successful in 15s
Deploy staging+live (annotations) / deploy (push) Successful in 55s
Reviewed-on: #164
2026-03-02 18:53:19 +01:00
Archicratia 8132e315f4 ci: hard-gate anno apply/reject + fix JSON parsing
CI / build-and-anchors (push) Successful in 49s
CI / build-and-anchors (pull_request) Successful in 43s
SMOKE / smoke (push) Successful in 10s
2026-03-02 18:48:39 +01:00
Archicratia 8d993915d7 Merge pull request 'ci: stabilize anno apply/reject (event parsing + strict gating)' (#161) from chore/fix-anno-workflows-jsonparse-v2 into main
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 1m10s
SMOKE / smoke (push) Successful in 8s
Reviewed-on: #161
2026-03-02 12:49:18 +01:00
Archicratia 497bddd05d ci: fix anno apply/reject JSON parsing + hard gates
CI / build-and-anchors (push) Successful in 48s
CI / build-and-anchors (pull_request) Successful in 40s
SMOKE / smoke (push) Successful in 8s
2026-03-02 12:47:37 +01:00
Archicratia 7c8e49c1a9 ci: stabilize anno apply/reject (event parsing + strict gating)
CI / build-and-anchors (push) Successful in 52s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Failing after 12m55s
2026-03-02 11:10:53 +01:00
Archicratia 901d28b89b Merge pull request 'deploy: pin nas-deploy image by digest' (#159) from chore/pin-nas-deploy-image-digest into main
CI / build-and-anchors (push) Successful in 36s
Deploy staging+live (annotations) / deploy (push) Successful in 45s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #159
2026-02-28 20:24:46 +01:00
Archicratia 43e2862c89 deploy: pin nas-deploy image by digest
CI / build-and-anchors (push) Successful in 39s
CI / build-and-anchors (pull_request) Successful in 37s
SMOKE / smoke (push) Successful in 6s
2026-02-28 20:22:17 +01:00
Archicratia 73fb38c4d1 Merge pull request 'deploy: use prebaked nas-deploy image; remove apt-get step' (#158) from chore/deploy-use-prebaked-image into main
CI / build-and-anchors (push) Successful in 36s
Deploy staging+live (annotations) / deploy (push) Successful in 39s
SMOKE / smoke (push) Successful in 9s
Reviewed-on: #158
2026-02-28 19:51:27 +01:00
Archicratia a81d206aba deploy: use prebaked nas-deploy image; remove apt-get step
CI / build-and-anchors (push) Successful in 39s
CI / build-and-anchors (pull_request) Successful in 36s
SMOKE / smoke (push) Successful in 3s
2026-02-28 19:49:25 +01:00
Archicratia 9801ea3cea Merge pull request 'ci: lock deploy workflow to nas-deploy runner' (#157) from chore/lock-nas-deploy into main
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 3m40s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #157
2026-02-28 17:45:35 +01:00
Archicratia c11189fe11 ci: lock deploy workflow to nas-deploy runner
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 40s
SMOKE / smoke (push) Successful in 7s
2026-02-28 17:43:19 +01:00
Archicratia b47edb24cf Merge pull request 'ci: fix YAML newlines after runs-on (mac-ci)' (#156) from chore/fix-yaml-runs-on-newlines into main
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Successful in 3s
Reviewed-on: #156
2026-02-28 15:54:48 +01:00
Archicratia be191b09a0 ci: fix YAML newlines after runs-on (mac-ci)
CI / build-and-anchors (push) Successful in 1m6s
CI / build-and-anchors (pull_request) Successful in 37s
SMOKE / smoke (push) Successful in 25s
2026-02-28 15:50:48 +01:00
Archicratia e06587478d Merge pull request 'ci: route CI/bots to mac runner; keep deploy on NAS' (#155) from chore/route-ci-to-mac-runner into main
Deploy staging+live (annotations) / deploy (push) Successful in 1m58s
Reviewed-on: #155
2026-02-28 15:29:35 +01:00
Archicratia 402ffb04cd ci: route CI/bots to mac runner; keep deploy on NAS 2026-02-28 15:28:49 +01:00
Archicratia 1cbfc02670 Merge pull request 'ci: harden anno-reject (dispatch + conflict guard) and keep deploy concurrency safe' (#153) from chore/fix-anno-reject-close-guard into main
CI / build-and-anchors (push) Successful in 2m49s
Deploy staging+live (annotations) / deploy (push) Successful in 3m7s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #153
2026-02-28 10:05:26 +01:00
Archicratia 28d2fbbd2f ci: harden anno-reject (dispatch + conflict guard) and keep deploy concurrency safe
CI / build-and-anchors (push) Successful in 2m21s
SMOKE / smoke (push) Successful in 19s
2026-02-28 09:55:37 +01:00
Archicratia 225368a952 Merge pull request 'ci: anno-apply gate on supported types (skip proposer)' (#149) from chore/fix-anno-apply-gate-types into main
CI / build-and-anchors (push) Successful in 1m45s
Deploy staging+live (annotations) / deploy (push) Successful in 2m12s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #149
2026-02-27 20:06:57 +01:00
Archicratia 3574695041 ci: anno-apply gate on supported types (skip proposer)
CI / build-and-anchors (push) Successful in 1m52s
SMOKE / smoke (push) Successful in 15s
2026-02-27 20:03:16 +01:00
Archicratia ea68025a1d Merge pull request 'anno: apply ticket #144' (#148) from bot/anno-144-20260227-124313 into main
CI / build-and-anchors (push) Successful in 2m5s
Deploy staging+live (annotations) / deploy (push) Successful in 2m8s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #148
2026-02-27 15:49:11 +01:00
Archicratia 3a08698003 Merge pull request 'anno: apply ticket #143' (#147) from bot/anno-143-20260227-124037 into main
CI / build-and-anchors (push) Has been cancelled
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Has been cancelled
Reviewed-on: #147
2026-02-27 15:48:28 +01:00
Archicratia 3d583608c2 Merge pull request 'anno: apply ticket #142' (#146) from bot/anno-142-20260227-123430 into main
CI / build-and-anchors (push) Has been cancelled
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Successful in 21s
Reviewed-on: #146
2026-02-27 15:47:44 +01:00
archicratie-bot 01ae95ab43 anno: apply ticket #144 (archicrat-ia/chapitre-3#p-0-ace27175 type/media)
CI / build-and-anchors (push) Successful in 2m0s
SMOKE / smoke (push) Successful in 18s
2026-02-27 12:43:16 +00:00
archicratie-bot 0d5821c640 anno: apply ticket #143 (archicrat-ia/chapitre-1#p-1-8a6c18bf type/comment)
CI / build-and-anchors (push) Successful in 1m54s
SMOKE / smoke (push) Successful in 16s
2026-02-27 12:40:39 +00:00
archicratie-bot 2bcea39558 anno: apply ticket #142 (archicrat-ia/chapitre-1#p-0-8d27a7f5 type/reference)
CI / build-and-anchors (push) Successful in 1m53s
SMOKE / smoke (push) Successful in 14s
2026-02-27 12:34:32 +00:00
Archicratia af85970d4a Merge pull request 'chore/fix-build-annotations-index-shards' (#141) from chore/fix-build-annotations-index-shards into main
CI / build-and-anchors (push) Successful in 1m45s
Deploy staging+live (annotations) / deploy (push) Successful in 1m53s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #141
2026-02-27 13:21:43 +01:00
Archicratia 210f621487 ci: support shard annotations in checks + endpoint (pageKey inference)
CI / build-and-anchors (push) Successful in 1m58s
SMOKE / smoke (push) Successful in 13s
2026-02-27 13:13:31 +01:00
Archicratia 8ad960dc69 anno: build-annotations-index supports shard annotations
CI / build-and-anchors (push) Failing after 1m48s
SMOKE / smoke (push) Successful in 16s
2026-02-27 12:27:35 +01:00
Archicratia d45a8b285f anno: support shard annotations in annotations-index endpoint
CI / build-and-anchors (push) Failing after 1m45s
SMOKE / smoke (push) Successful in 15s
2026-02-27 12:09:40 +01:00
Archicratia b6e04a9138 anno: robust verify for para-index (normalize page keys)
CI / build-and-anchors (push) Failing after 1m53s
SMOKE / smoke (push) Successful in 33s
2026-02-27 10:20:49 +01:00
Archicratia dcf1fc2d0b anno: apply ticket #127 (archicrat-ia/chapitre-4#p-11-67c14c09 type/media) 2026-02-27 10:17:06 +01:00
Archicratia 41b0517c6c Merge pull request 'ci: deploy hotpatch-only + full rebuild warmup' (#139) from chore/fix-deploy-hotpatch-stable into main
CI / build-and-anchors (push) Successful in 1m50s
Deploy staging+live (annotations) / deploy (push) Successful in 2m12s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #139
2026-02-26 22:00:20 +01:00
Archicratia 6b43eb199d ci: deploy hotpatch-only + full rebuild warmup
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 20s
2026-02-26 21:56:42 +01:00
Archicratia d40f24e92d Merge pull request 'ci: fix hotpatch (yaml datetime -> json safe)' (#138) from chore/fix-hotpatch-json into main
CI / build-and-anchors (push) Successful in 1m54s
Deploy staging+live (annotations) / deploy (push) Failing after 5m35s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #138
2026-02-26 21:32:56 +01:00
Archicratia 480a61b071 ci: fix hotpatch (yaml datetime -> json safe)
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 22s
2026-02-26 21:29:52 +01:00
Archicratia a5d68d6a7e Merge pull request 'ci: fix deploy workflow (warmup + hotpatch)' (#137) from chore/fix-deploy-warmup into main
CI / build-and-anchors (push) Successful in 1m38s
Deploy staging+live (annotations) / deploy (push) Failing after 8m16s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #137
2026-02-26 21:09:21 +01:00
Archicratia 390f2c33e5 ci: fix deploy workflow (warmup + hotpatch)
CI / build-and-anchors (push) Successful in 2m2s
SMOKE / smoke (push) Successful in 17s
2026-02-26 21:06:01 +01:00
Archicratia 16485dc4a9 Merge pull request 'ci: shard annotations by para + deploy deep-merge hotpatch' (#135) from chore/anno-shard-deepmerge into main
CI / build-and-anchors (push) Successful in 1m54s
Deploy staging+live (annotations) / deploy (push) Failing after 7m20s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #135
2026-02-26 19:58:22 +01:00
Archicratia a43ce5f188 ci: shard annotations by para + deploy deep-merge hotpatch
CI / build-and-anchors (push) Successful in 1m51s
SMOKE / smoke (push) Successful in 29s
2026-02-26 19:54:46 +01:00
Archicratia 0519ae2dd0 Merge pull request 'ci: fix deploy checkout (no eval) + workflow_dispatch fallback' (#134) from chore/fix-deploy-container-conflict into main
CI / build-and-anchors (push) Successful in 1m52s
Deploy staging+live (annotations) / deploy (push) Successful in 8m48s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #134
2026-02-26 18:51:53 +01:00
Archicratia 0d5b790e52 ci: fix deploy checkout (no eval) + workflow_dispatch fallback
CI / build-and-anchors (push) Successful in 1m50s
SMOKE / smoke (push) Successful in 17s
2026-02-26 18:48:12 +01:00
Archicratia 342e21b9ea Merge pull request 'ci: fix deploy checkout (no eval) + workflow_dispatch fallback' (#133) from chore/fix-deploy-checkout-quote into main
CI / build-and-anchors (push) Successful in 1m40s
Deploy staging+live (annotations) / deploy (push) Failing after 10m11s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #133
2026-02-26 18:01:24 +01:00
Archicratia 4dec9e182b ci: fix deploy checkout (no eval) + workflow_dispatch fallback
CI / build-and-anchors (push) Successful in 1m38s
SMOKE / smoke (push) Successful in 18s
2026-02-26 17:58:58 +01:00
Archicratia c7ae883c6a Merge pull request 'ci: fix deploy workflow (workflow_dispatch checkout + gate)' (#132) from chore/fix-deploy-workflow into main
CI / build-and-anchors (push) Successful in 1m37s
Deploy staging+live (annotations) / deploy (push) Failing after 19s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #132
2026-02-26 17:44:28 +01:00
Archicratia 9b4584f70a ci: fix deploy workflow (workflow_dispatch checkout + gate)
CI / build-and-anchors (push) Successful in 1m53s
SMOKE / smoke (push) Successful in 19s
2026-02-26 17:39:51 +01:00
Archicratia 7b64fb7401 Merge pull request 'anno: apply ticket #129' (#131) from bot/anno-129-20260226-131740 into main
CI / build-and-anchors (push) Successful in 1m46s
Deploy staging+live (annotations) / deploy (push) Successful in 22s
SMOKE / smoke (push) Successful in 18s
Reviewed-on: #131
2026-02-26 14:23:46 +01:00
archicratie-bot 57cb23ce8b anno: apply ticket #129 (archicrat-ia/chapitre-4#p-11-67c14c09 type/media)
CI / build-and-anchors (push) Successful in 1m39s
SMOKE / smoke (push) Successful in 18s
2026-02-26 13:17:43 +00:00
Archicratia 708b87ff35 Merge pull request 'ci: deploy staging+live (annotations) with manual force' (#126) from chore/deploy-staging-live into main
CI / build-and-anchors (push) Successful in 1m47s
Deploy staging+live (annotations) / deploy (push) Successful in 20s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #126
2026-02-26 12:41:21 +01:00
Archicratia 577cfd08e8 ci: deploy staging+live (annotations) with manual force
CI / build-and-anchors (push) Successful in 1m51s
SMOKE / smoke (push) Successful in 15s
2026-02-26 12:40:47 +01:00
Archicratia de9edbe532 Merge pull request 'ci: fix deploy (install docker compose plugin)' (#125) from chore/fix-deploy-compose into main
CI / build-and-anchors (push) Successful in 1m39s
Deploy staging+live (annotations) / deploy (push) Successful in 31s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #125
2026-02-26 11:16:19 +01:00
Archicratia 5e95dc9898 ci: fix deploy (install docker compose plugin)
CI / build-and-anchors (push) Successful in 1m42s
SMOKE / smoke (push) Successful in 23s
2026-02-26 11:15:53 +01:00
Archicratia 006fec7efd Merge pull request 'ci: auto deploy staging+live (annotations-only)' (#124) from chore/deploy-auto into main
CI / build-and-anchors (push) Successful in 1m48s
SMOKE / smoke (push) Successful in 24s
Deploy (staging + live) — annotations / deploy (push) Failing after 20s
Reviewed-on: #124
2026-02-26 10:29:50 +01:00
Archicratia 2b612214bb ci: auto deploy staging+live (annotations-only)
CI / build-and-anchors (push) Successful in 1m55s
SMOKE / smoke (push) Successful in 18s
2026-02-26 10:29:17 +01:00
Archicratia 29a6c349aa Merge pull request 'anno: apply ticket #121' (#122) from bot/anno-121-20260225-191130 into main
CI / build-and-anchors (push) Successful in 2m18s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #122
2026-02-26 09:01:52 +01:00
archicratie-bot 33a227c401 anno: apply ticket #121 (archicrat-ia/chapitre-4#p-7-1da4a458 type/media)
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 21s
2026-02-25 19:11:36 +00:00
Archicratia 396ad4df7c Merge pull request 'anno: apply ticket #115' (#120) from bot/anno-115-20260225-185830 into main
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #120
2026-02-25 20:00:04 +01:00
archicratie-bot 0b39427090 anno: apply ticket #115 (archicrat-ia/chapitre-4#p-2-31b12529 type/media)
CI / build-and-anchors (push) Successful in 1m42s
SMOKE / smoke (push) Successful in 15s
2026-02-25 18:58:34 +00:00
Archicratia 8fcb18cb46 Merge pull request 'ci: anno apply workflow builds dist for strict verify' (#119) from chore/fix-anno-verify-build2 into main
CI / build-and-anchors (push) Successful in 1m49s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #119
2026-02-25 19:51:24 +01:00
Archicratia d03fc519de ci: anno apply workflow builds dist for strict verify
CI / build-and-anchors (push) Successful in 1m50s
SMOKE / smoke (push) Successful in 19s
2026-02-25 19:50:47 +01:00
Archicratia 97dd3797d6 Merge pull request 'ci: anno apply workflow builds dist for strict verify' (#118) from chore/fix-anno-verify-build into main
CI / build-and-anchors (push) Successful in 1m35s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #118
2026-02-25 19:31:45 +01:00
Archicratia 6c7b7ab6a0 ci: anno apply workflow builds dist for strict verify
CI / build-and-anchors (push) Successful in 1m59s
SMOKE / smoke (push) Successful in 23s
2026-02-25 19:29:36 +01:00
Archicratia 105dfe1b5b Merge pull request 'ci: add apply-annotation-ticket script for anno bot' (#117) from chore/add-apply-annotation-ticket into main
CI / build-and-anchors (push) Successful in 1m47s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #117
2026-02-25 19:02:33 +01:00
Archicratia 82f6453538 ci: add apply-annotation-ticket script for anno bot
CI / build-and-anchors (push) Successful in 1m43s
SMOKE / smoke (push) Successful in 17s
2026-02-25 19:02:03 +01:00
Archicratia fe862102d3 Merge pull request 'ci: fix anno apply/reject workflows (yaml valid)' (#116) from chore/fix-anno-workflows into main
CI / build-and-anchors (push) Successful in 1m49s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #116
2026-02-25 18:28:36 +01:00
Archicratia 6ef538a0c4 ci: fix anno apply/reject workflows (yaml valid)
CI / build-and-anchors (push) Successful in 1m36s
SMOKE / smoke (push) Successful in 20s
2026-02-25 18:26:03 +01:00
Archicratia 689612ff7f Merge pull request 'anno-workflows' (#114) from chore/anno-workflows into main
CI / build-and-anchors (push) Successful in 1m52s
SMOKE / smoke (push) Successful in 18s
Reviewed-on: #114
2026-02-25 17:51:16 +01:00
Archicratia 7b135a4707 ci: add anno apply bot workflow
CI / build-and-anchors (push) Successful in 2m0s
SMOKE / smoke (push) Successful in 16s
2026-02-25 17:50:45 +01:00
Archicratia 0cb8a54195 Merge pull request 'fix: remove specimen media from prologue annotations' (#108) from chore/Fix_whoami into main
CI / build-and-anchors (push) Has started running
SMOKE / smoke (push) Has been cancelled
Reviewed-on: #108
2026-02-23 12:37:39 +01:00
Archicratia a7a333397d fix: remove specimen media from prologue annotations
CI / build-and-anchors (push) Successful in 1m36s
SMOKE / smoke (push) Successful in 15s
2026-02-23 12:33:55 +01:00
Archicratia eb1d444776 Merge pull request 'fix: …' (#107) from chore/Fix_whoami into main
CI / build-and-anchors (push) Failing after 1m45s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #107
2026-02-23 12:17:21 +01:00
Archicratia 68c3416594 fix: …
CI / build-and-anchors (push) Failing after 2m6s
SMOKE / smoke (push) Successful in 13s
2026-02-23 12:07:01 +01:00
Archicratia ae809e0152 Merge pull request 'docs: add pro runbooks (deploy/edge/public_site) + annotations spec + start-here v2' (#106) from fix/canonical-public-site into main
CI / build-and-anchors (push) Successful in 1m25s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #106
2026-02-21 15:36:07 +01:00
Archicratia 7444eeb532 docs: add pro runbooks (deploy/edge/public_site) + annotations spec + start-here v2
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 11s
2026-02-21 15:34:47 +01:00
Archicratia 9bbebf5886 Merge pull request 'fix(seo): enforce PUBLIC_SITE at docker build (canonical/sitemap) + set per blue/green' (#105) from fix/canonical-public-site into main
CI / build-and-anchors (push) Successful in 1m55s
SMOKE / smoke (push) Successful in 23s
Reviewed-on: #105
2026-02-21 12:32:08 +01:00
Archicratia fe7810671d fix(seo): enforce PUBLIC_SITE at docker build (canonical/sitemap) + set per blue/green
CI / build-and-anchors (push) Successful in 2m26s
SMOKE / smoke (push) Successful in 20s
2026-02-21 12:31:23 +01:00
Archicratia 53562025ac Merge pull request 'fix/anchors-baseline-archicrat-ia-20260220' (#104) from fix/anchors-baseline-archicrat-ia-20260220 into main
CI / build-and-anchors (push) Successful in 1m32s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #104
2026-02-20 22:50:22 +01:00
Archicratia 2b35315466 Merge branch 'main' into fix/anchors-baseline-archicrat-ia-20260220
CI / build-and-anchors (push) Successful in 1m51s
SMOKE / smoke (push) Successful in 15s
2026-02-20 22:50:06 +01:00
Archicratia 1b7f23d0a6 fix(home): Essai-thèse -> /archicrat-ia/ + rename ArchiCraT-IA
CI / build-and-anchors (push) Successful in 1m49s
SMOKE / smoke (push) Successful in 17s
2026-02-20 22:42:49 +01:00
Archicratia 3d1d4d7952 fix(annotations): update archicrat-ia prologue specimen paths 2026-02-20 22:29:55 +01:00
Archicratia 3320563e1b chore: add ops scripts + diagrams PNG renders 2026-02-20 22:29:47 +01:00
Archicratia 798b2ddd0b chore: ignore .DS_Store 2026-02-20 22:22:35 +01:00
Archicratia 31d4896f5d Merge pull request 'test(anchors): update baseline after URL migration to /archicrat-ia' (#103) from fix/anchors-baseline-archicrat-ia-20260220 into main
CI / build-and-anchors (push) Successful in 1m24s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #103
2026-02-20 21:29:19 +01:00
Archicratia 3fda37491d test(anchors): update baseline after URL migration to /archicrat-ia
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 13s
2026-02-20 21:28:45 +01:00
Archicratia 488c02b8b5 Merge pull request 'chore/url-migration-archicrat-ia-20260220' (#102) from chore/url-migration-archicrat-ia-20260220 into main
CI / build-and-anchors (push) Failing after 1m31s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #102
2026-02-20 21:15:55 +01:00
Archicratia 672e6d03d0 fix(url): migrate Essai-thèse to /archicrat-ia (routes + index + annotations)
CI / build-and-anchors (push) Failing after 1m37s
SMOKE / smoke (push) Successful in 17s
2026-02-20 21:14:45 +01:00
Archicratia 2881fdaf01 fix(toc): Essai-thèse links -> /archicrat-ia/* (no /archicratie prefix)
CI / build-and-anchors (push) Successful in 1m42s
SMOKE / smoke (push) Successful in 13s
2026-02-20 20:48:31 +01:00
Archicratia db98a3787b fix(nav): Essai-thèse -> /archicrat-ia/
CI / build-and-anchors (push) Successful in 1m38s
SMOKE / smoke (push) Successful in 15s
2026-02-20 20:35:56 +01:00
Archicratia f9ea3760e2 Merge pull request 'chore: add diagrams + scripts + archicrat-ia route' (#101) from chore/url-migration-archicrat-ia-20260220 into main
CI / build-and-anchors (push) Successful in 1m51s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #101
2026-02-20 18:31:40 +01:00
Archicratia 78eb9cbb58 chore: add diagrams + scripts + archicrat-ia route
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 14s
2026-02-20 18:27:25 +01:00
Archicratia 00e1a1d4b0 Merge pull request 'chore: add missing diagrams/scripts + archicrat-ia routes' (#100) from chore/url-migration-archicrat-ia-20260220 into main
CI / build-and-anchors (push) Successful in 1m31s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #100
2026-02-20 18:17:31 +01:00
Archicratia ab3758bbc2 chore: add missing diagrams/scripts + archicrat-ia routes
CI / build-and-anchors (push) Successful in 2m13s
SMOKE / smoke (push) Successful in 17s
2026-02-20 18:15:27 +01:00
Archicratia 12d3d81518 Merge pull request 'fix(etape8): resync hotfix edition depuis NAS (2026-02-19)' (#99) from sync/etape8-hotfix-20260219 into main
CI / build-and-anchors (push) Successful in 1m34s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #99
2026-02-19 23:01:47 +01:00
Archicratia e2468be522 fix(etape8): resync hotfix edition depuis NAS (2026-02-19)
CI / build-and-anchors (push) Successful in 1m50s
SMOKE / smoke (push) Successful in 18s
2026-02-19 23:00:58 +01:00
Archicratia dc2826df08 Merge pull request 'build: fix astro mdx/rehype config + dedupe duplicate ids in dist' (#82) from chore/fix-astro-ts-mdx-types into main
CI / build-and-anchors (push) Successful in 2m6s
SMOKE / smoke (push) Successful in 23s
Reviewed-on: #82
2026-02-15 15:19:57 +01:00
199 changed files with 27072 additions and 2203 deletions

No files matched your search

@@ -3,7 +3,7 @@ name: "Correction paragraphe"
about: "Proposer une correction ciblée (un paragraphe) avec justification."
---
## Chemin (ex: /archicratie/prologue/)
## Chemin (ex: /archicrat-ia/prologue/)
<!-- obligatoire -->
/...
@@ -3,7 +3,7 @@ name: "Vérification factuelle / sources"
about: "Signaler une assertion à sourcer ou à corriger (preuves, références)."
---
## Chemin (ex: /archicratie/prologue/)
## Chemin (ex: /archicrat-ia/prologue/)
<!-- obligatoire -->
/...
+450
View File
@@ -0,0 +1,450 @@
name: Anno Apply (PR)
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number to apply"
required: true
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
concurrency:
group: anno-apply-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
apply-approved:
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
- name: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || { echo "Missing $EVENT_JSON"; exit 1; }
node --input-type=module - <<'NODE' > /tmp/anno.env
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
let owner =
repoObj?.owner?.login ||
repoObj?.owner?.username ||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
let repo =
repoObj?.name ||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
if (!owner || !repo) {
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
if (m?.groups) {
owner = owner || m.groups.o;
repo = repo || m.groups.r;
}
}
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
const defaultBranch = repoObj?.default_branch || "main";
const issueNumber =
ev?.issue?.number ||
ev?.issue?.index ||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
throw new Error("No issue number in event.json or workflow_dispatch input");
}
let labelName = "workflow_dispatch";
const lab = ev?.label;
if (typeof lab === "string") labelName = lab;
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
else if (ev?.label?.name) labelName = ev.label.name;
const u = new URL(cloneUrl);
const origin = u.origin;
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
: origin;
function sh(s) { return JSON.stringify(String(s)); }
process.stdout.write([
`CLONE_URL=${sh(cloneUrl)}`,
`OWNER=${sh(owner)}`,
`REPO=${sh(repo)}`,
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
`ISSUE_NUMBER=${sh(issueNumber)}`,
`LABEL_NAME=${sh(labelName)}`,
`API_BASE=${sh(apiBase)}`
].join("\n") + "\n");
NODE
echo "context:"
sed -n '1,120p' /tmp/anno.env
- name: Early gate (label event fast-skip, but tolerant)
run: |
set -euo pipefail
source /tmp/anno.env
echo "event label = $LABEL_NAME"
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
echo "label=$LABEL_NAME => skip early"
echo "SKIP=1" >> /tmp/anno.env
echo "SKIP_REASON=\"label_not_approved_event\"" >> /tmp/anno.env
exit 0
fi
echo "continue to API gating (issue=$ISSUE_NUMBER)"
- name: Fetch issue + hard gate on labels + Type
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo "skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "Missing secret FORGE_TOKEN"; exit 1; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/issue.json
node --input-type=module - <<'NODE' >> /tmp/anno.env
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json", "utf8"));
const body = String(issue.body || "").replace(/\r\n/g, "\n");
const labels = Array.isArray(issue.labels)
? issue.labels.map(l => String(l.name || "")).filter(Boolean)
: [];
const hasApproved = labels.includes("state/approved");
function pickLine(key) {
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
const m = body.match(re);
return m ? m[1].trim() : "";
}
const typeRaw = pickLine("Type");
const type = String(typeRaw || "").trim().toLowerCase();
const allowedAnno = new Set(["type/media", "type/reference", "type/comment"]);
const proposerTypes = new Set(["type/correction", "type/fact-check"]);
const out = [];
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
if (!hasApproved) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("not_approved_label_present")}`);
process.stdout.write(out.join("\n") + "\n");
process.exit(0);
}
if (!type) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
} else if (allowedAnno.has(type)) {
// proceed
} else if (proposerTypes.has(type)) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("proposer_type:" + type)}`);
} else {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("unsupported_type:" + type)}`);
}
process.stdout.write(out.join("\n") + "\n");
NODE
echo "gating result:"
grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true
- name: Comment issue if skipped (unsupported / missing Type only)
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" == "1" ]] || exit 0
if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
echo "skip reason=${SKIP_REASON} -> no comment"
exit 0
fi
if [[ "${SKIP_REASON:-}" == proposer_type:* ]]; then
echo "proposer ticket detected -> anno stays silent"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
REASON="${SKIP_REASON:-}"
TYPE="${ISSUE_TYPE:-}"
if [[ "$REASON" == unsupported_type:* ]]; then
MSG="Ticket #${ISSUE_NUMBER} ignored: unsupported Type (${TYPE}). Supported types: type/media, type/reference, type/comment."
else
MSG="Ticket #${ISSUE_NUMBER} ignored: missing or unreadable 'Type:'. Expected: type/media|type/reference|type/comment"
fi
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1] || ""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
- name: Checkout default branch
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo "skipped"; exit 0; }
rm -rf .git
git init -q
git remote add origin "$CLONE_URL"
git fetch --depth 1 origin "$DEFAULT_BRANCH"
git -c advice.detachedHead=false checkout -q FETCH_HEAD
git log -1 --oneline
- name: Install deps
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo "skipped"; exit 0; }
npm ci --no-audit --no-fund
- name: Check apply script exists
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo "skipped"; exit 0; }
test -f scripts/apply-annotation-ticket.mjs || {
echo "missing scripts/apply-annotation-ticket.mjs on $DEFAULT_BRANCH"
ls -la scripts | sed -n '1,200p' || true
exit 1
}
- name: Build dist (needed for --verify)
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo "skipped"; exit 0; }
npm run build
test -f dist/para-index.json || {
echo "missing dist/para-index.json after build"
ls -la dist | sed -n '1,200p' || true
exit 1
}
echo "dist/para-index.json present"
- name: Apply ticket on bot branch (strict+verify, commit)
continue-on-error: true
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo "skipped"; exit 0; }
test -d .git || { echo "not a git repo (checkout failed)"; echo "APPLY_RC=90" >> /tmp/anno.env; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "Missing secret FORGE_TOKEN"; exit 1; }
git config user.name "${BOT_GIT_NAME:-archicratie-bot}"
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
START_SHA="$(git rev-parse HEAD)"
TS="$(date -u +%Y%m%d-%H%M%S)"
BR="bot/anno-${ISSUE_NUMBER}-${TS}"
echo "BRANCH=$BR" >> /tmp/anno.env
git checkout -b "$BR"
export FORGE_API="$API_BASE"
export GITEA_OWNER="$OWNER"
export GITEA_REPO="$REPO"
LOG="/tmp/apply.log"
set +e
node scripts/apply-annotation-ticket.mjs "$ISSUE_NUMBER" --strict --verify --commit >"$LOG" 2>&1
RC=$?
set -e
echo "APPLY_RC=$RC" >> /tmp/anno.env
echo "== apply log (tail) =="
tail -n 180 "$LOG" || true
END_SHA="$(git rev-parse HEAD)"
if [[ "$RC" -ne 0 ]]; then
echo "NOOP=0" >> /tmp/anno.env
exit 0
fi
if [[ "$START_SHA" == "$END_SHA" ]]; then
echo "NOOP=1" >> /tmp/anno.env
else
echo "NOOP=0" >> /tmp/anno.env
echo "END_SHA=$END_SHA" >> /tmp/anno.env
fi
- name: Comment issue on failure (strict/verify/etc)
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || { echo "skipped"; exit 0; }
RC="${APPLY_RC:-0}"
if [[ "$RC" == "0" ]]; then
echo "no failure detected"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
if [[ -f /tmp/apply.log ]]; then
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
else
BODY="(no apply log found)"
fi
MSG="apply-annotation-ticket failed (rc=${RC}).\n\n\`\`\`\n${BODY}\n\`\`\`\n"
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1] || ""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
- name: Push bot branch
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "apply failed -> skip push"; exit 0; }
[[ "${NOOP:-0}" == "0" ]] || { echo "no-op -> skip push"; exit 0; }
test -d .git || { echo "no git repo -> skip push"; exit 0; }
AUTH_URL="$(node --input-type=module -e '
const [clone, tok] = process.argv.slice(1);
const u = new URL(clone);
u.username = "oauth2";
u.password = tok;
console.log(u.toString());
' "$CLONE_URL" "$FORGE_TOKEN")"
git remote set-url origin "$AUTH_URL"
git push -u origin "$BRANCH"
- name: Create PR + comment issue
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "apply failed -> skip PR"; exit 0; }
[[ "${NOOP:-0}" == "0" ]] || { echo "no-op -> skip PR"; exit 0; }
PR_TITLE="anno: apply ticket #${ISSUE_NUMBER}"
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA}\n\nMerge si CI OK."
PR_PAYLOAD="$(node --input-type=module -e '
const [title, body, base, head] = process.argv.slice(1);
console.log(JSON.stringify({ title, body, base, head, allow_maintainer_edit: true }));
' "$PR_TITLE" "$PR_BODY" "$DEFAULT_BRANCH" "${OWNER}:${BRANCH}")"
PR_JSON="$(curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
--data-binary "$PR_PAYLOAD")"
PR_URL="$(node --input-type=module -e '
const pr = JSON.parse(process.argv[1] || "{}");
console.log(pr.html_url || pr.url || "");
' "$PR_JSON")"
test -n "$PR_URL" || { echo "PR URL missing. Raw: $PR_JSON"; exit 1; }
MSG="PR created for ticket #${ISSUE_NUMBER}: ${PR_URL}"
C_PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1] || ""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$C_PAYLOAD"
echo "PR: $PR_URL"
- name: Finalize (fail job if apply failed)
if: ${{ always() }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || { echo "skipped"; exit 0; }
RC="${APPLY_RC:-0}"
if [[ "$RC" != "0" ]]; then
echo "apply failed (rc=$RC)"
exit "$RC"
fi
echo "apply ok"
+181
View File
@@ -0,0 +1,181 @@
name: Anno Reject (close issue)
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number to reject/close"
required: true
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
concurrency:
group: anno-reject-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
reject:
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
node --version
- name: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
node --input-type=module - <<'NODE' > /tmp/reject.env
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
let owner =
repoObj?.owner?.login ||
repoObj?.owner?.username ||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
let repo =
repoObj?.name ||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
if ((!owner || !repo) && cloneUrl) {
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
}
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
const issueNumber =
ev?.issue?.number ||
ev?.issue?.index ||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
throw new Error("No issue number in event.json or workflow_dispatch input");
}
// label name: best-effort (non-bloquant)
let labelName = "workflow_dispatch";
const lab = ev?.label;
if (typeof lab === "string") labelName = lab;
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
let apiBase = "";
if (process.env.FORGE_API && String(process.env.FORGE_API).trim()) {
apiBase = String(process.env.FORGE_API).trim().replace(/\/+$/,"");
} else if (cloneUrl) {
apiBase = new URL(cloneUrl).origin;
} else {
apiBase = "";
}
function sh(s){ return JSON.stringify(String(s)); }
process.stdout.write([
`OWNER=${sh(owner)}`,
`REPO=${sh(repo)}`,
`ISSUE_NUMBER=${sh(issueNumber)}`,
`LABEL_NAME=${sh(labelName)}`,
`API_BASE=${sh(apiBase)}`
].join("\n") + "\n");
NODE
echo "✅ context:"
sed -n '1,120p' /tmp/reject.env
- name: Early gate (fast-skip, tolerant)
run: |
set -euo pipefail
source /tmp/reject.env
echo "ℹ️ event label = $LABEL_NAME"
if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
echo "ℹ️ label=$LABEL_NAME => skip early"
echo "SKIP=1" >> /tmp/reject.env
echo "SKIP_REASON=\"label_not_rejected_event\"" >> /tmp/reject.env
exit 0
fi
- name: Comment + close (only if label state/rejected is PRESENT now, and no conflict)
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/reject.env
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
test -n "${API_BASE:-}" || { echo "❌ Missing API_BASE"; exit 1; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/reject.issue.json
node --input-type=module - <<'NODE' > /tmp/reject.flags
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync("/tmp/reject.issue.json","utf8"));
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
const hasApproved = labels.includes("state/approved");
const hasRejected = labels.includes("state/rejected");
process.stdout.write(`HAS_APPROVED=${hasApproved ? "1":"0"}\nHAS_REJECTED=${hasRejected ? "1":"0"}\n`);
NODE
source /tmp/reject.flags
# Do nothing unless state/rejected is truly present now (anti payload weird)
if [[ "${HAS_REJECTED:-0}" != "1" ]]; then
echo "ℹ️ state/rejected not present -> skip"
exit 0
fi
if [[ "${HAS_APPROVED:-0}" == "1" && "${HAS_REJECTED:-0}" == "1" ]]; then
MSG="⚠️ Conflit d'état sur le ticket #${ISSUE_NUMBER} : labels **state/approved** et **state/rejected** présents.\n\n➡️ Action manuelle requise : retirer l'un des deux labels avant relance."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
echo "ℹ️ conflict => stop"
exit 0
fi
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
curl -fsS -X PATCH \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
--data-binary '{"state":"closed"}'
echo "✅ rejected+closed"
+56 -30
View File
@@ -4,22 +4,37 @@ on:
issues:
types: [opened, edited]
concurrency:
group: auto-label-${{ github.event.issue.number || github.event.issue.index || 'manual' }}
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Apply labels from Type/State/Category
env:
FORGE_BASE: ${{ vars.FORGE_API || vars.FORGE_BASE }}
# IMPORTANT: préfère FORGE_BASE (LAN) si défini, sinon FORGE_API
FORGE_BASE: ${{ vars.FORGE_BASE || vars.FORGE_API || vars.FORGE_API_BASE }}
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
REPO_FULL: ${{ gitea.repository }}
EVENT_PATH: ${{ github.event_path }}
NODE_OPTIONS: --dns-result-order=ipv4first
run: |
python3 - <<'PY'
import json, os, re, urllib.request, urllib.error
import json, os, re, time, urllib.request, urllib.error, socket
forge = (os.environ.get("FORGE_BASE") or "").rstrip("/")
if not forge:
raise SystemExit("Missing FORGE_BASE/FORGE_API repo variable (e.g. http://192.168.1.20:3000)")
token = os.environ.get("FORGE_TOKEN") or ""
if not token:
raise SystemExit("Missing secret FORGE_TOKEN")
forge = os.environ["FORGE_BASE"].rstrip("/")
token = os.environ["FORGE_TOKEN"]
owner, repo = os.environ["REPO_FULL"].split("/", 1)
event_path = os.environ["EVENT_PATH"]
@@ -46,12 +61,9 @@ jobs:
print("PARSED:", {"Type": t, "State": s, "Category": c})
# 1) explicite depuis le body
if t:
desired.add(t)
if s:
desired.add(s)
if c:
desired.add(c)
if t: desired.add(t)
if s: desired.add(s)
if c: desired.add(c)
# 2) fallback depuis le titre si Type absent
if not t:
@@ -76,42 +88,56 @@ jobs:
"Authorization": f"token {token}",
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-auto-label/1.0",
"User-Agent": "archicratie-auto-label/1.1",
}
def jreq(method, url, payload=None):
def jreq(method, url, payload=None, timeout=60, retries=4, backoff=2.0):
data = None if payload is None else json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=20) as r:
b = r.read()
return json.loads(b.decode("utf-8")) if b else None
except urllib.error.HTTPError as e:
b = e.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
last_err = None
for i in range(retries):
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
b = r.read()
return json.loads(b.decode("utf-8")) if b else None
except urllib.error.HTTPError as e:
b = e.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
last_err = e
# retry only on network/timeout
time.sleep(backoff * (i + 1))
raise RuntimeError(f"Network/timeout after retries: {method} {url}\n{last_err}")
# labels repo
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000") or []
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000", timeout=60) or []
name_to_id = {x.get("name"): x.get("id") for x in labels}
missing = [x for x in desired if x not in name_to_id]
if missing:
raise SystemExit("Missing labels in repo: " + ", ".join(sorted(missing)))
wanted_ids = [name_to_id[x] for x in desired]
wanted_ids = sorted({int(name_to_id[x]) for x in desired})
# labels actuels de l'issue
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels") or []
current_ids = {x.get("id") for x in current if x.get("id") is not None}
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
current_ids = {int(x.get("id")) for x in current if x.get("id") is not None}
final_ids = sorted(current_ids.union(wanted_ids))
# set labels = union (n'enlève rien)
# Replace labels = union (n'enlève rien)
url = f"{api}/repos/{owner}/{repo}/issues/{number}/labels"
try:
jreq("PUT", url, {"labels": final_ids})
except Exception:
jreq("PUT", url, final_ids)
# IMPORTANT: on n'envoie JAMAIS une liste brute ici (ça a causé le 422)
jreq("PUT", url, {"labels": final_ids}, timeout=90, retries=4)
# vérif post-apply (anti "timeout mais appliqué")
post = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
post_ids = {int(x.get("id")) for x in post if x.get("id") is not None}
missing_ids = [i for i in wanted_ids if i not in post_ids]
if missing_ids:
raise RuntimeError(f"Labels not applied after PUT (missing ids): {missing_ids}")
print(f"OK labels #{number}: {sorted(desired)}")
PY
+4 -19
View File
@@ -3,7 +3,7 @@ name: CI
on:
push:
pull_request:
branches: [master]
branches: [main]
workflow_dispatch:
env:
@@ -15,7 +15,7 @@ defaults:
jobs:
build-and-anchors:
runs-on: ubuntu-latest
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
@@ -79,22 +79,7 @@ jobs:
set -euo pipefail
npm ci
- name: Inline scripts syntax check
- name: Full test suite (CI=1)
run: |
set -euo pipefail
node scripts/check-inline-js.mjs
- name: Build (includes postbuild injection + pagefind)
run: |
set -euo pipefail
npm run build
- name: Anchors contract
run: |
set -euo pipefail
npm run test:anchors
- name: Verify anchor aliases injected in dist
run: |
set -euo pipefail
node scripts/verify-anchor-aliases-in-dist.mjs
npm run ci
-103
View File
@@ -1,103 +0,0 @@
name: CI
on:
push: {}
pull_request:
branches: ["master"]
workflow_dispatch: {}
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
jobs:
build-and-anchors:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
npm ping --registry=https://registry.npmjs.org
- name: Checkout (from event.json, no external actions)
run: |
set -euo pipefail
EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || (echo "❌ Missing $EVENT_JSON" && exit 1)
eval "$(node - <<'NODE'
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync("/var/run/act/workflow/event.json","utf8"));
const repo =
ev?.repository?.clone_url ||
(ev?.repository?.html_url ? (ev.repository.html_url.replace(/\/$/,'') + ".git") : "");
const sha =
ev?.after ||
ev?.pull_request?.head?.sha ||
ev?.head_commit?.id ||
ev?.sha ||
"";
if (!repo) { console.error("No repository.clone_url/html_url in event.json"); process.exit(1); }
if (!sha) { console.error("No sha/after/pull_request.head.sha in event.json"); process.exit(1); }
console.log(`REPO_URL=${JSON.stringify(repo)}`);
console.log(`SHA=${JSON.stringify(sha)}`);
NODE
)"
echo "Repo URL: $REPO_URL"
echo "SHA: $SHA"
rm -rf .git
git init
git remote add origin "$REPO_URL"
git fetch --depth 1 origin "$SHA"
git checkout -q FETCH_HEAD
git log -1 --oneline
- name: Anchor aliases schema
run: |
set -euo pipefail
node scripts/check-anchor-aliases.mjs
- name: NPM harden
run: |
set -euo pipefail
npm config set fetch-retries 5
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set registry https://registry.npmjs.org
npm config get registry
- name: Install deps
run: |
set -euo pipefail
npm ci
- name: Inline scripts syntax check
run: |
set -euo pipefail
node scripts/check-inline-js.mjs
- name: Build (includes postbuild injection + pagefind)
run: |
set -euo pipefail
npm run build
- name: Anchors contract
run: |
set -euo pipefail
npm run test:anchors
- name: Verify anchor aliases injected in dist
run: |
set -euo pipefail
node scripts/verify-anchor-aliases-in-dist.mjs
+577
View File
@@ -0,0 +1,577 @@
name: Deploy staging+live (annotations)
on:
push:
branches: [main]
workflow_dispatch:
inputs:
force:
description: "Force FULL deploy (rebuild+restart) even if gate would hotpatch-only (1=yes, 0=no)"
required: false
default: "0"
env:
NODE_OPTIONS: --dns-result-order=ipv4first
DOCKER_API_VERSION: "1.43"
COMPOSE_VERSION: "2.29.7"
ASTRO_TELEMETRY_DISABLED: "1"
defaults:
run:
shell: bash
concurrency:
group: deploy-staging-live-main
cancel-in-progress: false
jobs:
deploy:
runs-on: nas-deploy
container:
image: localhost:5000/archicratie/nas-deploy-node22@sha256:fefa8bb307005cebec07796661ab25528dc319c33a8f1e480e1d66f90cd5cff6
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
- name: Checkout (push or workflow_dispatch, no external actions)
env:
EVENT_JSON: /var/run/act/workflow/event.json
run: |
set -euo pipefail
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
node --input-type=module <<'NODE'
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
const defaultBranch = repoObj?.default_branch || "main";
// Push-range (most reliable for change detection)
const before = String(ev?.before || "").trim();
const after =
(process.env.GITHUB_SHA && String(process.env.GITHUB_SHA).trim()) ||
String(ev?.after || ev?.sha || ev?.head_commit?.id || ev?.pull_request?.head?.sha || "").trim();
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
fs.writeFileSync("/tmp/deploy.env", [
`REPO_URL=${shq(cloneUrl)}`,
`DEFAULT_BRANCH=${shq(defaultBranch)}`,
`BEFORE=${shq(before)}`,
`AFTER=${shq(after)}`
].join("\n") + "\n");
NODE
source /tmp/deploy.env
echo "Repo URL: $REPO_URL"
echo "Default branch: $DEFAULT_BRANCH"
echo "BEFORE: ${BEFORE:-<empty>}"
echo "AFTER: ${AFTER:-<empty>}"
rm -rf .git
git init -q
git remote add origin "$REPO_URL"
# Checkout AFTER (or default branch if missing)
if [[ -n "${AFTER:-}" ]]; then
git fetch --depth 50 origin "$AFTER"
git -c advice.detachedHead=false checkout -q FETCH_HEAD
else
git fetch --depth 50 origin "$DEFAULT_BRANCH"
git -c advice.detachedHead=false checkout -q "origin/$DEFAULT_BRANCH"
AFTER="$(git rev-parse HEAD)"
echo "AFTER='$AFTER'" >> /tmp/deploy.env
echo "Resolved AFTER: $AFTER"
fi
git log -1 --oneline
- name: Gate — decide SKIP vs HOTPATCH vs FULL rebuild
env:
INPUT_FORCE: ${{ inputs.force }}
EVENT_JSON: /var/run/act/workflow/event.json
run: |
set -euo pipefail
source /tmp/deploy.env
FORCE="${INPUT_FORCE:-0}"
# Lire before/after du push depuis event.json (merge-proof)
node --input-type=module <<'NODE'
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const before = ev?.before || "";
const after = ev?.after || ev?.sha || "";
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
fs.writeFileSync("/tmp/gate.env", [
`EV_BEFORE=${shq(before)}`,
`EV_AFTER=${shq(after)}`
].join("\n") + "\n");
NODE
source /tmp/gate.env
BEFORE="${EV_BEFORE:-}"
AFTER="${EV_AFTER:-}"
if [[ -z "${AFTER:-}" ]]; then
AFTER="${SHA:-}"
fi
echo "Gate ctx: BEFORE=${BEFORE:-<empty>} AFTER=${AFTER:-<empty>} FORCE=${FORCE}"
# Produire une liste CHANGED fiable :
# - si BEFORE/AFTER valides -> git diff before..after
# - sinon fallback -> diff parent1..after ou show after
CHANGED=""
Z40="0000000000000000000000000000000000000000"
if [[ -n "${BEFORE:-}" && "${BEFORE}" != "${Z40}" ]] \
&& git cat-file -e "${BEFORE}^{commit}" 2>/dev/null \
&& git cat-file -e "${AFTER}^{commit}" 2>/dev/null; then
CHANGED="$(git diff --name-only "${BEFORE}" "${AFTER}" || true)"
else
P1="$(git rev-parse "${AFTER}^" 2>/dev/null || true)"
if [[ -n "${P1:-}" ]] && git cat-file -e "${P1}^{commit}" 2>/dev/null; then
CHANGED="$(git diff --name-only "${P1}" "${AFTER}" || true)"
else
CHANGED="$(git show --name-only --pretty="" "${AFTER}" | sed '/^$/d' || true)"
fi
fi
printf "%s\n" "${CHANGED}" > /tmp/changed.txt
echo "== changed files (first 200) =="
sed -n '1,200p' /tmp/changed.txt || true
# Flags
HAS_FULL=0
HAS_HOTPATCH=0
# HOTPATCH si annotations/media touchés
if grep -qE '^(src/annotations/|public/media/)' /tmp/changed.txt; then
HAS_HOTPATCH=1
fi
# FULL si build-impacting (robuste)
# 1) Tout src/ SAUF src/annotations/
if grep -qE '^src/' /tmp/changed.txt && grep -qEv '^src/annotations/' /tmp/changed.txt; then
HAS_FULL=1
fi
# 2) scripts/
if grep -qE '^scripts/' /tmp/changed.txt; then
HAS_FULL=1
fi
# 3) Tout public/ SAUF public/media/
if grep -qE '^public/' /tmp/changed.txt && grep -qEv '^public/media/' /tmp/changed.txt; then
HAS_FULL=1
fi
# 4) fichiers racine qui changent le build / l’image
if grep -qE '^(package\.json|package-lock\.json|astro\.config\.mjs|tsconfig\.json|\.npmrc|\.nvmrc|Dockerfile|docker-compose\.yml|nginx\.conf)$' /tmp/changed.txt; then
HAS_FULL=1
fi
echo "Gate flags: HAS_FULL=${HAS_FULL} HAS_HOTPATCH=${HAS_HOTPATCH}"
# Décision
if [[ "${FORCE}" == "1" ]]; then
GO=1
MODE="full"
echo "✅ force=1 -> MODE=full (rebuild+restart)"
elif [[ "${HAS_FULL}" == "1" ]]; then
GO=1
MODE="full"
echo "✅ build-impacting change -> MODE=full (rebuild+restart)"
elif [[ "${HAS_HOTPATCH}" == "1" ]]; then
GO=1
MODE="hotpatch"
echo "✅ annotations/media change -> MODE=hotpatch"
else
GO=0
MODE="skip"
echo "ℹ️ no relevant change -> skip deploy"
fi
echo "GO=${GO}" >> /tmp/deploy.env
echo "MODE='${MODE}'" >> /tmp/deploy.env
- name: Toolchain sanity + resolve COMPOSE_PROJECT_NAME
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
# tools are prebaked in the image
git --version
docker version
docker compose version
python3 -c 'import yaml; print("PyYAML OK")'
# Reuse existing compose project name if containers already exist
PROJ="$(docker inspect archicratie-web-blue --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
if [[ -z "${PROJ:-}" ]]; then
PROJ="$(docker inspect archicratie-web-green --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
fi
if [[ -z "${PROJ:-}" ]]; then PROJ="archicratie-web"; fi
echo "COMPOSE_PROJECT_NAME='$PROJ'" >> /tmp/deploy.env
echo "✅ Using COMPOSE_PROJECT_NAME=$PROJ"
# Assert target containers exist (hotpatch needs them)
for c in archicratie-web-blue archicratie-web-green; do
docker inspect "$c" >/dev/null 2>&1 || { echo "❌ missing container $c"; exit 5; }
done
- name: Assert required vars (PUBLIC_GITEA_*) — only needed for MODE=full
env:
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ hotpatch mode -> vars not required"; exit 0; }
test -n "${PUBLIC_GITEA_BASE:-}" || { echo "❌ missing repo var PUBLIC_GITEA_BASE"; exit 2; }
test -n "${PUBLIC_GITEA_OWNER:-}" || { echo "❌ missing repo var PUBLIC_GITEA_OWNER"; exit 2; }
test -n "${PUBLIC_GITEA_REPO:-}" || { echo "❌ missing repo var PUBLIC_GITEA_REPO"; exit 2; }
echo "✅ vars OK"
- name: Assert deploy files exist — only needed for MODE=full
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ hotpatch mode -> files not required"; exit 0; }
test -f docker-compose.yml
test -f Dockerfile
test -f nginx.conf
echo "✅ deploy files OK"
- name: FULL — Build + deploy staging (blue) then warmup+smoke
env:
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ MODE=$MODE -> skip full rebuild"; exit 0; }
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
wait_url() {
local url="$1"
local label="$2"
local tries="${3:-60}"
for i in $(seq 1 "$tries"); do
if curl -fsS --max-time 4 "$url" >/dev/null; then
echo "✅ $label OK ($url)"
return 0
fi
echo "… warmup $label ($i/$tries)"
sleep 1
done
echo "❌ timeout $label ($url)"
return 1
}
TS="$(date -u +%Y%m%d-%H%M%S)"
echo "TS='$TS'" >> /tmp/deploy.env
docker image tag archicratie-web:blue "archicratie-web:blue.BAK.${TS}" || true
docker image tag archicratie-web:green "archicratie-web:green.BAK.${TS}" || true
docker compose -p "$PROJ" -f docker-compose.yml build web_blue
docker rm -f archicratie-web-blue || true
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_blue
# warmup endpoints
wait_url "http://127.0.0.1:8081/para-index.json" "blue para-index"
wait_url "http://127.0.0.1:8081/annotations-index.json" "blue annotations-index"
wait_url "http://127.0.0.1:8081/pagefind/pagefind.js" "blue pagefind.js"
CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
echo "canonical(blue)=$CANON"
echo "$CANON" | grep -q 'https://staging\.archicratie\.trans-hands\.synology\.me/' || {
echo "❌ staging canonical mismatch"
docker logs --tail 120 archicratie-web-blue || true
exit 3
}
echo "✅ staging OK"
- name: FULL — Build + deploy live (green) then warmup+smoke + rollback if needed
env:
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ MODE=$MODE -> skip full rebuild"; exit 0; }
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
TS="${TS:-$(date -u +%Y%m%d-%H%M%S)}"
wait_url() {
local url="$1"
local label="$2"
local tries="${3:-60}"
for i in $(seq 1 "$tries"); do
if curl -fsS --max-time 4 "$url" >/dev/null; then
echo "✅ $label OK ($url)"
return 0
fi
echo "… warmup $label ($i/$tries)"
sleep 1
done
echo "❌ timeout $label ($url)"
return 1
}
rollback() {
echo "⚠️ rollback green -> previous image tag (best effort)"
docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green || true
docker rm -f archicratie-web-green || true
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green || true
}
# build/restart green
if ! docker compose -p "$PROJ" -f docker-compose.yml build web_green; then
echo "❌ build green failed"; rollback; exit 4
fi
docker rm -f archicratie-web-green || true
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green
# warmup endpoints
if ! wait_url "http://127.0.0.1:8082/para-index.json" "green para-index"; then rollback; exit 4; fi
if ! wait_url "http://127.0.0.1:8082/annotations-index.json" "green annotations-index"; then rollback; exit 4; fi
if ! wait_url "http://127.0.0.1:8082/pagefind/pagefind.js" "green pagefind.js"; then rollback; exit 4; fi
CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
echo "canonical(green)=$CANON"
echo "$CANON" | grep -q 'https://archicratie\.trans-hands\.synology\.me/' || {
echo "❌ live canonical mismatch"
docker logs --tail 120 archicratie-web-green || true
rollback
exit 4
}
echo "✅ live OK"
- name: HOTPATCH — deep merge shards -> annotations-index + copy changed media into blue+green
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
python3 - <<'PY'
import os, re, json, glob
import yaml
import datetime as dt
ROOT = os.getcwd()
ANNO_ROOT = os.path.join(ROOT, "src", "annotations")
def is_obj(x): return isinstance(x, dict)
def is_arr(x): return isinstance(x, list)
def iso_dt(x):
if isinstance(x, dt.datetime):
if x.tzinfo is None:
return x.isoformat()
return x.astimezone(dt.timezone.utc).isoformat().replace("+00:00","Z")
if isinstance(x, dt.date):
return x.isoformat()
return None
def normalize(x):
s = iso_dt(x)
if s is not None: return s
if isinstance(x, dict):
return {str(k): normalize(v) for k, v in x.items()}
if isinstance(x, list):
return [normalize(v) for v in x]
return x
def key_media(it): return str((it or {}).get("src",""))
def key_ref(it):
it = it or {}
return "||".join([str(it.get("url","")), str(it.get("label","")), str(it.get("kind","")), str(it.get("citation",""))])
def key_comment(it): return str((it or {}).get("text","")).strip()
def dedup_extend(dst_list, src_list, key_fn):
seen = set(); out = []
for x in (dst_list or []):
x = normalize(x); k = key_fn(x)
if k and k not in seen: seen.add(k); out.append(x)
for x in (src_list or []):
x = normalize(x); k = key_fn(x)
if k and k not in seen: seen.add(k); out.append(x)
return out
def deep_merge(dst, src):
src = normalize(src)
for k, v in (src or {}).items():
if k in ("media","refs","comments_editorial") and is_arr(v):
if k == "media": dst[k] = dedup_extend(dst.get(k, []), v, key_media)
elif k == "refs": dst[k] = dedup_extend(dst.get(k, []), v, key_ref)
else: dst[k] = dedup_extend(dst.get(k, []), v, key_comment)
continue
if is_obj(v):
if not is_obj(dst.get(k)): dst[k] = {}
deep_merge(dst[k], v)
continue
if is_arr(v):
cur = dst.get(k, [])
if not is_arr(cur): cur = []
seen = set(); out = []
for x in cur:
x = normalize(x)
s = json.dumps(x, sort_keys=True, ensure_ascii=False)
if s not in seen: seen.add(s); out.append(x)
for x in v:
x = normalize(x)
s = json.dumps(x, sort_keys=True, ensure_ascii=False)
if s not in seen: seen.add(s); out.append(x)
dst[k] = out
continue
v = normalize(v)
if k not in dst or dst.get(k) in (None, ""):
dst[k] = v
def para_num(pid):
m = re.match(r"^p-(\d+)-", str(pid))
return int(m.group(1)) if m else 10**9
def sort_lists(entry):
for k in ("media","refs","comments_editorial"):
arr = entry.get(k)
if not is_arr(arr): continue
def ts(x):
x = normalize(x)
try:
s = str((x or {}).get("ts",""))
return dt.datetime.fromisoformat(s.replace("Z","+00:00")).timestamp() if s else 0
except Exception:
return 0
arr = [normalize(x) for x in arr]
arr.sort(key=lambda x: (ts(x), json.dumps(x, sort_keys=True, ensure_ascii=False)))
entry[k] = arr
if not os.path.isdir(ANNO_ROOT):
raise SystemExit(f"Missing annotations root: {ANNO_ROOT}")
pages = {}
errors = []
files = sorted(glob.glob(os.path.join(ANNO_ROOT, "**", "*.yml"), recursive=True))
for fp in files:
try:
with open(fp, "r", encoding="utf-8") as f:
doc = yaml.safe_load(f) or {}
doc = normalize(doc)
if not isinstance(doc, dict) or doc.get("schema") != 1:
continue
page = str(doc.get("page","")).strip().strip("/")
paras = doc.get("paras") or {}
if not page or not isinstance(paras, dict):
continue
pg = pages.setdefault(page, {"paras": {}})
for pid, entry in paras.items():
pid = str(pid)
if pid not in pg["paras"] or not isinstance(pg["paras"].get(pid), dict):
pg["paras"][pid] = {}
if isinstance(entry, dict):
deep_merge(pg["paras"][pid], entry)
sort_lists(pg["paras"][pid])
except Exception as e:
errors.append({"file": os.path.relpath(fp, ROOT), "error": str(e)})
for page, obj in pages.items():
keys = list((obj.get("paras") or {}).keys())
keys.sort(key=lambda k: (para_num(k), k))
obj["paras"] = {k: obj["paras"][k] for k in keys}
out = {
"schema": 1,
"generatedAt": dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc).isoformat().replace("+00:00","Z"),
"pages": pages,
"stats": {
"pages": len(pages),
"paras": sum(len(v.get("paras") or {}) for v in pages.values()),
"errors": len(errors),
},
"errors": errors,
}
with open("/tmp/annotations-index.json", "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False)
print("OK: wrote /tmp/annotations-index.json pages=", out["stats"]["pages"], "paras=", out["stats"]["paras"], "errors=", out["stats"]["errors"])
PY
# patch JSON into running containers
for c in archicratie-web-blue archicratie-web-green; do
echo "== patch annotations-index.json into $c =="
docker cp /tmp/annotations-index.json "${c}:/usr/share/nginx/html/annotations-index.json"
done
# copy changed media files into containers (so new media appears without rebuild)
if [[ -s /tmp/changed.txt ]]; then
while IFS= read -r f; do
[[ -n "$f" ]] || continue
if [[ "$f" == public/media/* ]]; then
dest="/usr/share/nginx/html/${f#public/}" # => /usr/share/nginx/html/media/...
for c in archicratie-web-blue archicratie-web-green; do
echo "== copy media into $c: $f -> $dest =="
docker exec "$c" sh -lc "mkdir -p \"$(dirname "$dest")\""
docker cp "$f" "$c:$dest"
done
fi
done < /tmp/changed.txt
fi
# smoke after patch
for p in 8081 8082; do
echo "== smoke annotations-index on $p =="
curl -fsS --max-time 6 "http://127.0.0.1:${p}/annotations-index.json" \
| python3 -c 'import sys,json; j=json.load(sys.stdin); print("generatedAt:", j.get("generatedAt")); print("pages:", len(j.get("pages") or {})); print("paras:", j.get("stats",{}).get("paras"))'
done
echo "✅ hotpatch done"
- name: Debug on failure (containers status/logs)
if: ${{ failure() }}
run: |
set -euo pipefail
echo "== docker ps =="
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | sed -n '1,80p' || true
for c in archicratie-web-blue archicratie-web-green; do
echo "== logs $c (tail 200) =="
docker logs --tail 200 "$c" || true
done
+650
View File
@@ -0,0 +1,650 @@
name: Proposer Apply (Queue)
on:
issues:
types: [labeled]
push:
branches: [main]
workflow_dispatch:
inputs:
issue:
description: "Issue number to prioritize (optional)"
required: false
default: ""
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
concurrency:
group: proposer-queue-main
cancel-in-progress: false
jobs:
apply-proposer:
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
- name: Derive context (event.json / workflow_dispatch / push)
env:
INPUT_ISSUE: ${{ inputs.issue }}
EVENT_NAME_IN: ${{ github.event_name }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || { echo "Missing $EVENT_JSON"; exit 1; }
node --input-type=module - <<'NODE' > /tmp/proposer.env
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/, "") + ".git") : "");
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
let owner =
repoObj?.owner?.login ||
repoObj?.owner?.username ||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
let repo =
repoObj?.name ||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
if (!owner || !repo) {
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
if (m?.groups) {
owner = owner || m.groups.o;
repo = repo || m.groups.r;
}
}
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
const defaultBranch = repoObj?.default_branch || "main";
const issueNumber =
ev?.issue?.number ||
ev?.issue?.index ||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0) ||
0;
const labelName =
ev?.label?.name ||
(typeof ev?.label === "string" ? ev.label : "") ||
"";
const eventName =
String(process.env.EVENT_NAME_IN || "").trim() ||
(ev?.issue ? "issues" : (ev?.before || ev?.after ? "push" : "workflow_dispatch"));
const u = new URL(cloneUrl);
const origin = u.origin;
const apiBase =
(process.env.FORGE_API && String(process.env.FORGE_API).trim())
? String(process.env.FORGE_API).trim().replace(/\/+$/, "")
: origin;
function sh(s) {
return JSON.stringify(String(s));
}
process.stdout.write([
`CLONE_URL=${sh(cloneUrl)}`,
`OWNER=${sh(owner)}`,
`REPO=${sh(repo)}`,
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
`ISSUE_NUMBER=${sh(issueNumber)}`,
`LABEL_NAME=${sh(labelName)}`,
`EVENT_NAME=${sh(eventName)}`,
`API_BASE=${sh(apiBase)}`
].join("\n") + "\n");
NODE
echo "Context:"
sed -n '1,200p' /tmp/proposer.env
- name: Early gate
run: |
set -euo pipefail
source /tmp/proposer.env
if [[ "$EVENT_NAME" == "issues" ]]; then
if [[ "$LABEL_NAME" != "state/approved" ]]; then
echo "issues/labeled but label=$LABEL_NAME -> skip"
echo 'SKIP=1' >> /tmp/proposer.env
echo 'SKIP_REASON="label_not_state_approved"' >> /tmp/proposer.env
exit 0
fi
fi
echo "Proceed"
- name: Checkout default branch
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
rm -rf .git
git init -q
git remote add origin "$CLONE_URL"
git fetch --depth 1 origin "$DEFAULT_BRANCH"
git -c advice.detachedHead=false checkout -q FETCH_HEAD
git log -1 --oneline
- name: Detect app dir (repo-root vs ./site)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
APP_DIR="."
if [[ -d "site" && -f "site/package.json" ]]; then
APP_DIR="site"
fi
echo "APP_DIR=$APP_DIR" >> /tmp/proposer.env
echo "APP_DIR=$APP_DIR"
test -f "$APP_DIR/package.json" || {
echo "package.json missing in APP_DIR=$APP_DIR"
exit 1
}
test -d "$APP_DIR/scripts" || {
echo "scripts/ missing in APP_DIR=$APP_DIR"
exit 1
}
- name: Select next proposer batch (by path)
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || {
echo "Missing secret FORGE_TOKEN"
exit 1
}
export GITEA_OWNER="$OWNER"
export GITEA_REPO="$REPO"
export FORGE_API="$API_BASE"
cd "$APP_DIR"
test -f scripts/pick-proposer-issue.mjs || {
echo "missing scripts/pick-proposer-issue.mjs in APP_DIR=$APP_DIR"
ls -la scripts | sed -n '1,200p' || true
exit 1
}
node scripts/pick-proposer-issue.mjs "${ISSUE_NUMBER:-0}" > /tmp/proposer.pick.env
cat /tmp/proposer.pick.env >> /tmp/proposer.env
source /tmp/proposer.pick.env
if [[ "${TARGET_FOUND:-0}" != "1" ]]; then
echo 'SKIP=1' >> /tmp/proposer.env
echo "SKIP_REASON=${TARGET_REASON:-no_target}" >> /tmp/proposer.env
echo "No target batch"
exit 0
fi
echo "Target batch:"
grep -E '^(TARGET_PRIMARY_ISSUE|TARGET_ISSUES|TARGET_COUNT|TARGET_CHEMIN)=' /tmp/proposer.env
- name: Inspect open proposer PRs
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls?state=open&limit=100" \
-o /tmp/open_pulls.json
export TARGET_ISSUES="${TARGET_ISSUES:-}"
node --input-type=module - <<'NODE' >> /tmp/proposer.env
import fs from "node:fs";
const pulls = JSON.parse(fs.readFileSync("/tmp/open_pulls.json", "utf8"));
const issues = String(process.env.TARGET_ISSUES || "")
.trim()
.split(/\s+/)
.filter(Boolean);
const proposerOpen = Array.isArray(pulls)
? pulls.filter((pr) => String(pr?.head?.ref || "").startsWith("bot/proposer-"))
: [];
const current = proposerOpen.find((pr) => {
const ref = String(pr?.head?.ref || "");
const title = String(pr?.title || "");
const body = String(pr?.body || "");
return issues.some((n) =>
ref.startsWith(`bot/proposer-${n}-`) ||
title.includes(`#${n}`) ||
body.includes(`#${n}`) ||
body.includes(`ticket #${n}`)
);
});
const out = [];
if (current) {
out.push("SKIP=1");
out.push(`SKIP_REASON=${JSON.stringify("issue_already_has_open_pr")}`);
out.push(`OPEN_PR_URL=${JSON.stringify(String(current.html_url || current.url || ""))}`);
} else if (proposerOpen.length > 0) {
const first = proposerOpen[0];
out.push("SKIP=1");
out.push(`SKIP_REASON=${JSON.stringify("queue_busy_open_proposer_pr")}`);
out.push(`OPEN_PR_URL=${JSON.stringify(String(first.html_url || first.url || ""))}`);
out.push(`OPEN_PR_BRANCH=${JSON.stringify(String(first?.head?.ref || ""))}`);
}
process.stdout.write(out.join("\n") + (out.length ? "\n" : ""));
NODE
- name: Comment issue if queued / skipped
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" == "1" ]] || exit 0
[[ "${EVENT_NAME:-}" != "push" ]] || exit 0
test -n "${FORGE_TOKEN:-}" || exit 0
ISSUE_TO_COMMENT="${ISSUE_NUMBER:-0}"
if [[ "$ISSUE_TO_COMMENT" == "0" || -z "$ISSUE_TO_COMMENT" ]]; then
ISSUE_TO_COMMENT="${TARGET_PRIMARY_ISSUE:-0}"
fi
[[ "$ISSUE_TO_COMMENT" != "0" ]] || exit 0
case "${SKIP_REASON:-}" in
queue_busy_open_proposer_pr)
MSG="Ticket queued in proposer queue. An open proposer PR already exists: ${OPEN_PR_URL:-"(URL unavailable)"}. The workflow will resume after merge on main."
;;
issue_already_has_open_pr)
MSG="This ticket already has an open proposer PR: ${OPEN_PR_URL:-"(URL unavailable)"}"
;;
explicit_issue_missing_chemin)
MSG="Proposer Apply: cannot process this ticket automatically because field Chemin is missing or unreadable."
;;
explicit_issue_missing_type)
MSG="Proposer Apply: cannot process this ticket automatically because field Type is missing or unreadable."
;;
explicit_issue_not_approved)
MSG="Proposer Apply: this ticket is not currently labeled state/approved."
;;
explicit_issue_rejected)
MSG="Proposer Apply: this ticket has state/rejected and is not eligible for the proposer queue."
;;
no_open_approved_proposer_issue)
MSG="No approved proposer ticket is currently waiting."
;;
*)
MSG="Proposer Apply: skip - ${SKIP_REASON:-unspecified reason}."
;;
esac
node --input-type=module - <<'NODE' > /tmp/proposer.skip.comment.json
const msg = process.env.MSG || "";
process.stdout.write(JSON.stringify({ body: msg }));
NODE
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_TO_COMMENT/comments" \
--data-binary @/tmp/proposer.skip.comment.json || true
- name: NPM harden
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
cd "$APP_DIR"
npm config set fetch-retries 5
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set registry https://registry.npmjs.org
- name: Install deps
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
cd "$APP_DIR"
npm ci --no-audit --no-fund
- name: Build dist baseline
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
cd "$APP_DIR"
npm run build
- name: Apply proposer batch on bot branch
continue-on-error: true
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
git config user.name "${BOT_GIT_NAME:-archicratie-bot}"
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
START_SHA="$(git rev-parse HEAD)"
TS="$(date -u +%Y%m%d-%H%M%S)"
BR="bot/proposer-${TARGET_PRIMARY_ISSUE}-${TS}"
echo "BRANCH=$BR" >> /tmp/proposer.env
git checkout -b "$BR"
export GITEA_OWNER="$OWNER"
export GITEA_REPO="$REPO"
export FORGE_API="$API_BASE"
LOG="/tmp/proposer-apply.log"
: > "$LOG"
RC=0
FAILED_ISSUE=""
for ISSUE in $TARGET_ISSUES; do
echo "" >> "$LOG"
echo "== ticket #$ISSUE ==" >> "$LOG"
set +e
(cd "$APP_DIR" && node scripts/apply-ticket.mjs "$ISSUE" --alias --commit) >> "$LOG" 2>&1
STEP_RC=$?
set -e
if [[ "$STEP_RC" -ne 0 ]]; then
RC="$STEP_RC"
FAILED_ISSUE="$ISSUE"
break
fi
done
echo "APPLY_RC=$RC" >> /tmp/proposer.env
echo "FAILED_ISSUE=${FAILED_ISSUE}" >> /tmp/proposer.env
echo "Apply log (tail):"
tail -n 220 "$LOG" || true
END_SHA="$(git rev-parse HEAD)"
if [[ "$RC" -ne 0 ]]; then
echo "NOOP=0" >> /tmp/proposer.env
exit 0
fi
if [[ "$START_SHA" == "$END_SHA" ]]; then
echo "NOOP=1" >> /tmp/proposer.env
else
echo "NOOP=0" >> /tmp/proposer.env
echo "END_SHA=$END_SHA" >> /tmp/proposer.env
fi
- name: Rebase bot branch on latest main
continue-on-error: true
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "0" ]] || exit 0
LOG="/tmp/proposer-apply.log"
git fetch origin "$DEFAULT_BRANCH"
set +e
git rebase "origin/$DEFAULT_BRANCH" >> "$LOG" 2>&1
RC=$?
set -e
if [[ "$RC" -ne 0 ]]; then
git rebase --abort || true
fi
echo "REBASE_RC=$RC" >> /tmp/proposer.env
echo "Rebase log (tail):"
tail -n 220 "$LOG" || true
- name: Comment issues on failure
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
APPLY_RC="${APPLY_RC:-0}"
REBASE_RC="${REBASE_RC:-0}"
if [[ "$APPLY_RC" == "0" && "$REBASE_RC" == "0" ]]; then
echo "No failure detected"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
if [[ -f /tmp/proposer-apply.log ]]; then
BODY="$(tail -n 160 /tmp/proposer-apply.log | sed 's/\r$//')"
else
BODY="(no proposer log found)"
fi
export BODY APPLY_RC REBASE_RC FAILED_ISSUE
if [[ "$APPLY_RC" != "0" ]]; then
export FAILURE_KIND="apply"
else
export FAILURE_KIND="rebase"
fi
node --input-type=module - <<'NODE' > /tmp/proposer.failure.comment.json
const body = process.env.BODY || "";
const applyRc = process.env.APPLY_RC || "0";
const rebaseRc = process.env.REBASE_RC || "0";
const failedIssue = process.env.FAILED_ISSUE || "unknown";
const kind = process.env.FAILURE_KIND || "apply";
const msg =
kind === "apply"
? `Batch proposer failed on ticket #${failedIssue} (rc=${applyRc}).\n\n\`\`\`\n${body}\n\`\`\`\n`
: `Rebase proposer failed on main (rc=${rebaseRc}).\n\n\`\`\`\n${body}\n\`\`\`\n`;
process.stdout.write(JSON.stringify({ body: msg }));
NODE
for ISSUE in ${TARGET_ISSUES:-}; do
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE/comments" \
--data-binary @/tmp/proposer.failure.comment.json || true
done
- name: Push bot branch
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "Apply failed -> skip push"; exit 0; }
[[ "${REBASE_RC:-0}" == "0" ]] || { echo "Rebase failed -> skip push"; exit 0; }
[[ "${NOOP:-0}" == "0" ]] || { echo "No-op -> skip push"; exit 0; }
[[ -n "${BRANCH:-}" ]] || { echo "BRANCH unset -> skip push"; exit 0; }
AUTH_URL="$(node --input-type=module -e '
const [clone, tok] = process.argv.slice(1);
const u = new URL(clone);
u.username = "oauth2";
u.password = tok;
console.log(u.toString());
' "$CLONE_URL" "$FORGE_TOKEN")"
git remote set-url origin "$AUTH_URL"
git push -u origin "$BRANCH"
- name: Create PR + comment issues + close issues
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
[[ "${REBASE_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "0" ]] || exit 0
[[ -n "${BRANCH:-}" ]] || { echo "BRANCH unset -> skip PR"; exit 0; }
if [[ "${TARGET_COUNT:-0}" == "1" ]]; then
PR_TITLE="proposer: apply ticket #${TARGET_PRIMARY_ISSUE}"
else
PR_TITLE="proposer: apply ${TARGET_COUNT} tickets on ${TARGET_CHEMIN}"
fi
export TITLE="$PR_TITLE"
export CHEMIN="$TARGET_CHEMIN"
export ISSUES="$TARGET_ISSUES"
export BRANCH="$BRANCH"
export END_SHA="${END_SHA:-unknown}"
export DEFAULT_BRANCH="$DEFAULT_BRANCH"
export OWNER="$OWNER"
node --input-type=module - <<'NODE' > /tmp/proposer.pr.json
const issues = String(process.env.ISSUES || "")
.trim()
.split(/\s+/)
.filter(Boolean);
const body = [
`PR auto depuis ticket${issues.length > 1 ? "s" : ""} ${issues.map((n) => `#${n}`).join(", ")} (state/approved).`,
"",
`- Chemin: ${process.env.CHEMIN || "(inconnu)"}`,
"- Tickets:",
...issues.map((n) => ` - #${n}`),
`- Branche: ${process.env.BRANCH || ""}`,
`- Commit: ${process.env.END_SHA || "unknown"}`,
"",
"Merge si CI OK."
].join("\n");
process.stdout.write(JSON.stringify({
title: process.env.TITLE || "proposer: apply tickets",
body,
base: process.env.DEFAULT_BRANCH || "main",
head: `${process.env.OWNER}:${process.env.BRANCH}`,
allow_maintainer_edit: true
}));
NODE
PR_JSON="$(curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
--data-binary @/tmp/proposer.pr.json)"
PR_URL="$(node --input-type=module -e '
const pr = JSON.parse(process.argv[1] || "{}");
console.log(pr.html_url || pr.url || "");
' "$PR_JSON")"
test -n "$PR_URL" || {
echo "PR URL missing. Raw: $PR_JSON"
exit 1
}
for ISSUE in $TARGET_ISSUES; do
export ISSUE PR_URL
node --input-type=module - <<'NODE' > /tmp/proposer.issue.close.comment.json
const issue = process.env.ISSUE || "";
const url = process.env.PR_URL || "";
const msg =
`PR proposer created for ticket #${issue}: ${url}\n\n` +
`The ticket is closed automatically. Discussion can continue in the PR.`;
process.stdout.write(JSON.stringify({ body: msg }));
NODE
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE/comments" \
--data-binary @/tmp/proposer.issue.close.comment.json
curl -fsS -X PATCH \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" \
--data-binary '{"state":"closed"}'
done
echo "PR: $PR_URL"
- name: Finalize
if: ${{ always() }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
if [[ "${APPLY_RC:-0}" != "0" ]]; then
echo "Apply failed (rc=${APPLY_RC})"
exit "${APPLY_RC}"
fi
if [[ "${REBASE_RC:-0}" != "0" ]]; then
echo "Rebase failed (rc=${REBASE_RC})"
exit "${REBASE_RC}"
fi
echo "Proposer queue OK"
+1 -1
View File
@@ -3,7 +3,7 @@ on: [push, workflow_dispatch]
jobs:
smoke:
runs-on: ubuntu-latest
runs-on: mac-ci
steps:
- run: node -v && npm -v
- run: echo "runner OK"
+10
View File
@@ -3,6 +3,10 @@
.env.*
!.env.example
# dev-only
public/_auth/whoami
public/_auth/whoami/*
# --- local backups ---
*.bak
*.bak.*
@@ -21,3 +25,9 @@ dist/
# local backups
Dockerfile.bak.*
public/favicon_io.zip
# macOS
.DS_Store
# local temp workspace
.tmp/
+15 -3
View File
@@ -12,7 +12,7 @@ ENV npm_config_update_notifier=false \
# (Optionnel mais propre) git + certificats
RUN apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update \
&& apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/*
# Déps d’abord (cache Docker)
COPY package.json package-lock.json ./
@@ -25,9 +25,21 @@ COPY . .
ARG PUBLIC_GITEA_BASE
ARG PUBLIC_GITEA_OWNER
ARG PUBLIC_GITEA_REPO
# ✅ Canonical + sitemap base (astro.config.mjs lit process.env.PUBLIC_SITE)
ARG PUBLIC_SITE
# ✅ Garde-fou : si 1 → build fail si PUBLIC_SITE absent
ARG REQUIRE_PUBLIC_SITE=0
ENV PUBLIC_GITEA_BASE=$PUBLIC_GITEA_BASE \
PUBLIC_GITEA_OWNER=$PUBLIC_GITEA_OWNER \
PUBLIC_GITEA_REPO=$PUBLIC_GITEA_REPO
PUBLIC_GITEA_REPO=$PUBLIC_GITEA_REPO \
PUBLIC_SITE=$PUBLIC_SITE \
REQUIRE_PUBLIC_SITE=$REQUIRE_PUBLIC_SITE
# ✅ antifragile : refuse de builder sans PUBLIC_SITE quand on l’exige
RUN node -e "if (process.env.REQUIRE_PUBLIC_SITE==='1' && !process.env.PUBLIC_SITE) { console.error('FATAL: PUBLIC_SITE is required (canonical/sitemap).'); process.exit(1) }"
# Build Astro (postbuild tourne via npm scripts)
RUN npm run build
@@ -38,4 +50,4 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/ /usr/share/nginx/html/
RUN find /usr/share/nginx/html -type d -exec chmod 755 {} \; \
&& find /usr/share/nginx/html -type f -exec chmod 644 {} \;
EXPOSE 80
EXPOSE 80
+4
View File
@@ -86,6 +86,10 @@ function rehypeDedupeIds() {
}
export default defineConfig({
legacy: {
collectionsBackwardsCompat: true,
},
output: "static",
trailingSlash: "always",
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
+7
View File
@@ -0,0 +1,7 @@
FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --omit=dev
COPY server.mjs ./
EXPOSE 8787
CMD ["node","server.mjs"]
+15
View File
@@ -0,0 +1,15 @@
services:
issue_bridge:
build: ./bridge
environment:
GITEA_API_BASE: "http://gitea:3000"
GITEA_TOKEN: "${GITEA_TOKEN}"
GITEA_OWNER: "Archicratia"
GITEA_REPO: "archicratie-edition"
restart: unless-stopped
networks:
- internal
networks:
internal:
external: true
+10
View File
@@ -0,0 +1,10 @@
{
"name": "issue-bridge",
"private": true,
"type": "module",
"dependencies": {
"express": "^4.19.2",
"multer": "^1.4.5-lts.1"
}
}
+89
View File
@@ -0,0 +1,89 @@
import express from "express";
import multer from "multer";
const app = express();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } }); // 25 MB
const {
GITEA_API_BASE, // ex: http://gitea:3000 (ou https://forge.tld)
GITEA_TOKEN, // PAT du bot
GITEA_OWNER, // owner/org
GITEA_REPO // repo
} = process.env;
function mustEnv(name) {
if (!process.env[name]) throw new Error(`Missing env ${name}`);
}
["GITEA_API_BASE","GITEA_TOKEN","GITEA_OWNER","GITEA_REPO"].forEach(mustEnv);
function isEditor(req) {
// Adapte selon tes headers Authelia. Souvent Remote-Groups / Remote-User.
const groups = String(req.header("Remote-Groups") || req.header("X-Remote-Groups") || "");
return groups.split(/[,\s]+/).includes("editors");
}
async function giteaFetch(path, init = {}) {
const url = String(GITEA_API_BASE).replace(/\/+$/, "") + path;
const headers = new Headers(init.headers || {});
headers.set("Authorization", `token ${GITEA_TOKEN}`);
return fetch(url, { ...init, headers });
}
app.get("/health", (_req, res) => res.json({ ok: true }));
app.post("/media", upload.single("file"), async (req, res) => {
try {
if (!isEditor(req)) return res.status(403).json({ ok: false, error: "forbidden" });
const file = req.file;
const title = String(req.body.title || "").trim();
const body = String(req.body.body || "").trim();
const suggestedName = String(req.body.suggestedName || "").trim();
if (!file) return res.status(400).json({ ok: false, error: "missing_file" });
if (!title) return res.status(400).json({ ok: false, error: "missing_title" });
if (!body) return res.status(400).json({ ok: false, error: "missing_body" });
// 1) Create issue
const r1 = await giteaFetch(`/api/v1/repos/${encodeURIComponent(GITEA_OWNER)}/${encodeURIComponent(GITEA_REPO)}/issues`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body })
});
if (!r1.ok) {
const t = await r1.text().catch(() => "");
return res.status(502).json({ ok: false, step: "create_issue", status: r1.status, detail: t.slice(0, 2000) });
}
const issue = await r1.json();
const index = issue?.number ?? issue?.index;
const issueUrl = issue?.html_url;
if (!index) return res.status(502).json({ ok: false, step: "create_issue", error: "missing_issue_index" });
// 2) Upload attachment (multipart field name = "attachment") :contentReference[oaicite:1]{index=1}
const fd = new FormData();
fd.append("attachment", new Blob([file.buffer], { type: file.mimetype || "application/octet-stream" }), file.originalname);
const q = suggestedName ? `?name=${encodeURIComponent(suggestedName)}` : "";
const r2 = await giteaFetch(`/api/v1/repos/${encodeURIComponent(GITEA_OWNER)}/${encodeURIComponent(GITEA_REPO)}/issues/${encodeURIComponent(String(index))}/assets${q}`, {
method: "POST",
body: fd
});
if (!r2.ok) {
const t = await r2.text().catch(() => "");
return res.status(502).json({ ok: false, step: "upload_asset", status: r2.status, detail: t.slice(0, 2000), issueUrl });
}
const asset = await r2.json().catch(() => ({}));
return res.json({ ok: true, issueUrl, issueIndex: index, asset });
} catch (e) {
return res.status(500).json({ ok: false, error: String(e?.message || e) });
}
});
app.listen(8787, "0.0.0.0", () => {
console.log("issue-bridge listening on :8787");
});
+5
View File
@@ -0,0 +1,5 @@
{
"accepted_resets": {
"archicrat-ia/chapitre-1/index.html": "Reset intentionnel des ancres après révision doctrinale substantielle du chapitre 1. Site neuf, sans annotations ni compatibilité descendante à préserver."
}
}
+5 -1
View File
@@ -5,6 +5,8 @@ services:
dockerfile: Dockerfile
network: host
args:
REQUIRE_PUBLIC_SITE: "1"
PUBLIC_SITE: "https://staging.archicratie.trans-hands.synology.me"
PUBLIC_GITEA_BASE: ${PUBLIC_GITEA_BASE}
PUBLIC_GITEA_OWNER: ${PUBLIC_GITEA_OWNER}
PUBLIC_GITEA_REPO: ${PUBLIC_GITEA_REPO}
@@ -20,6 +22,8 @@ services:
dockerfile: Dockerfile
network: host
args:
REQUIRE_PUBLIC_SITE: "1"
PUBLIC_SITE: "https://archicratie.trans-hands.synology.me"
PUBLIC_GITEA_BASE: ${PUBLIC_GITEA_BASE}
PUBLIC_GITEA_OWNER: ${PUBLIC_GITEA_OWNER}
PUBLIC_GITEA_REPO: ${PUBLIC_GITEA_REPO}
@@ -27,4 +31,4 @@ services:
container_name: archicratie-web-green
ports:
- "127.0.0.1:8082:80"
restart: unless-stopped
restart: unless-stopped
+327
View File
@@ -0,0 +1,327 @@
# SPEC — Annotations éditoriales (YAML v1) + merge + anti-doublon
> Objectif : permettre aux tickets (Gitea) de déposer “Références / Médias / Commentaires” dans `src/annotations/**`,
> de façon univoque, stable, et sans régression.
## 0) Contexte et intention
Le site est statique. L’édition collaborative se fait via :
- un mode “proposition” (UI / modal)
- un ticket Gitea (issue) standardisé
- un script d’application côté éditeur (`apply-ticket.mjs` ou équivalent)
- génération d’un YAML d’annotations versionné dans Git
La donnée d’annotation doit être :
- **audit-able** (Git)
- **merge-able** (sans tout casser)
- **stable** (IDs paragraphes / liens / médias)
- **scalable** (éviter YAML monstrueux à long terme)
## 1) Arborescence canonique
### 1.1 Un workKey par “ouvrage / section du site”
On veut une univocité entre :
- SiteNav (Méthode, Essai-thèse, Traité, Cas IA, Glossaire, Atlas)
et
- l’arborescence annotations
Proposition canonique (workKey = route racine) :
- `methode`
- `archicrat-ia` (Essai-thèse ArchiCraT-IA)
- `traite`
- `ia`
- `glossaire`
- `atlas`
### 1.2 Règle de stockage “v1”
**Par page**, un YAML unique :
src/annotations/<workKey>/<slugSansWorkKey>.yml
Exemples :
- Page : `/archicrat-ia/prologue/`
- slug content = `archicrat-ia/prologue`
- fichier : `src/annotations/archicrat-ia/prologue.yml`
- Page : `/traite/00-demarrage/`
- fichier : `src/annotations/traite/00-demarrage.yml`
> Note : “slugSansWorkKey” = la partie après `<workKey>/`.
> S’il y a des sous-dossiers (chapitres), le chemin reflète la structure : `chapitre-1/section-a.yml` si on choisit du sharding.
## 2) Question “gros YAML” : page unique vs sharding par paragraphe
### 2.1 Option A (v1 recommandée) : 1 YAML par page
Avantages :
- simple
- peu de fichiers
- diff lisible si volume modéré
- cohérent avec un modèle “annotations par page”
Inconvénients :
- YAML peut grossir si milliers d’annotations
### 2.2 Option B (v2 future) : sharding par paragraphe
src/annotations/<workKey>/<slugSansWorkKey>/<paraId>.yml
Avantages :
- fichiers petits
- merges moins conflictuels
Inconvénients :
- plus de fichiers
- tooling plus complexe (indexation + merge multi-fichiers)
### 2.3 Recommandation de mission (sans casser l’existant)
- On démarre en **Option A**.
- On se garde une migration future (v2) quand le volume réel le justifie.
- On impose dès v1 : **clé unique + merge déterministe + anti-doublon**, ce qui rend la migration future possible.
## 3) Format YAML v1 (schéma complet)
### 3.1 Top-level
en yaml :
schema: 1
# Optionnel mais recommandé (doit matcher la page)
page: "<workKey>/<slugSansWorkKey>"
meta:
title: "Titre de la page (optionnel)"
updatedAt: "2026-02-21T12:34:56Z" # ISO8601
updatedBy: "username" # compte editor
source:
kind: "ticket"
id: 123
url: "https://gitea.../issues/123"
paras:
"<paraId>":
references: []
media: []
comments: []
### 3.2 paras : clé = paraId (ex: p-0-d7974f88)
Chaque paragraphe peut porter 3 types d’éléments :
references
media
comments
Règle : si une section est vide, elle peut être [] ou absente.
Mais pour simplifier les merges, on recommande de garder la forme canonique avec [].
## 4) Formats des items + clés uniques
### 4.1 References
#### 4.1.1 Format
references:
- id: "ref:doi:10.1234/abcd.efgh" # clé stable (voir 4.1.2)
kind: "doi" # doi | url | isbn | arxiv | hal | other
label: "Titre court"
target: "https://doi.org/10.1234/abcd.efgh"
note: "Pourquoi c’est pertinent (optionnel)"
addedAt: "2026-02-21T12:34:56Z"
addedBy: "username"
#### 4.1.2 Règle de clé unique (anti-doublon)
id doit être stable et déterministe :
doi → ref:doi:<doi>
isbn → ref:isbn:<isbn>
url → ref:url:<normalizedUrl>
Normalisation URL (v1) : au minimum
trim
lowercase scheme/host
retirer trailing slash si non significatif
conserver query si importante
#### 4.1.3 Merge / précédence
Quand on merge deux listes references :
union par id (clé unique)
si même id existe des deux côtés :
conserver kind/target de l’item le plus “riche” (target non vide gagne)
concat/merge note :
si notes différentes : garder les deux en les séparant (ex: noteA + "\n---\n" + noteB)
addedAt : conserver le plus ancien
addedBy : conserver le premier (ou liste si on veut, mais v1 simple : first)
### 4.2 Media
#### 4.2.1 Format
media:
- id: "media:image:sha256:abcd..." # clé stable (voir 4.2.2)
type: "image" # image | video | audio | file
src: "/public/media/<workKey>/<slugSansWorkKey>/<paraId>/<filename>"
caption: "Légende (optionnel)"
credit: "Auteur/source (optionnel)"
license: "CC-BY (optionnel)"
addedAt: "2026-02-21T12:34:56Z"
addedBy: "username"
#### 4.2.2 Règle de clé unique
id déterministe :
idéal : hash du fichier (sha256)
sinon : hash de type + src
v1 (si on ne calcule pas de hash fichier) :
media:<type>:<src>
#### 4.2.3 Merge / précédence
union par id
si collision :
garder src identique (sinon c’est un bug)
fusionner caption/credit/license selon “non vide gagne”
addedAt : plus ancien
### 4.3 Comments
#### 4.3.1 Format
comments:
- id: "cmt:20260221T123456Z:username:0001"
kind: "comment" # comment | question | objection | todo | validation
text: "Texte du commentaire"
status: "open" # open | resolved
addedAt: "2026-02-21T12:34:56Z"
addedBy: "username"
source:
kind: "ticket"
id: 123
#### 4.3.2 Clé unique
Les commentaires sont “append-only” → id peut être générée (timestamp + user + compteur)
Anti-doublon : si on ré-applique un ticket, on refuse de dupliquer un id existant.
#### 4.3.3 Merge / précédence
union par id
collisions rares, mais si elles arrivent :
si textes différents → garder les deux (on renomme l’id du second)
## 5) Règles globales de merge (résumé)
Quand on applique un ticket sur un YAML existant :
vérifier schema == 1
vérifier page si présent :
doit matcher <workKey>/<slugSansWorkKey>
paras :
créer paras[paraId] si absent
pour chaque liste (references/media/comments) :
merge par id (anti-doublon)
appliquer règles de précédence (non vide gagne / concat note / append-only comments)
## 6) Table de correspondance “UI ticket → YAML”
Cette table permet à un successeur IA d’implémenter apply-ticket.mjs sans ambiguïté.
### 6.1 Champs UI minimaux
workKey (sélection implicite via page)
pagePath (ex: /archicrat-ia/prologue/)
pageSlug (ex: archicrat-ia/prologue)
paraId (ex: p-0-d7974f88)
kind :
reference
media
comment
### 6.2 Mapping exact
| UI kind | UI champs | YAML cible |
| --------- | ----------------------------------------------------------- | ---------------------------- |
| reference | kind(doi/url/isbn), target, label, note | `paras[paraId].references[]` |
| media | type(image/video/audio/file), src, caption, credit, license | `paras[paraId].media[]` |
| comment | kind(comment/question/objection/todo/validation), text | `paras[paraId].comments[]` |
### 6.3 Règles de génération d’ID (implémentation)
reference.id :
doi : ref:doi:${doi}
isbn : ref:isbn:${isbn}
url : ref:url:${normalize(url)}
media.id :
media:${type}:${src}
comment.id :
cmt:${timestamp}:${user}:${counter}
## 7) Validation YAML (sanity)
Avant commit (et en CI) :
YAML parse OK
schema OK
page si présent cohérent
paras est un mapping
paraId match pattern : ^p-\d+-[a-f0-9]{8}$ (existant)
src media pointe dans /public/media/... (ou /media/... si on choisit un alias, mais v1 canon : /public/media/...)
## 8) Notes de compatibilité
Les routes “Essai-thèse” ont été migrées vers /archicrat-ia/*.
Les anciennes routes /archicratie/archicrat-ia/* peuvent exister en legacy, mais la donnée canonique d’annotation doit suivre le workKey final (archicrat-ia).
## 9) Ce que l’étape 9 devra implémenter
pipeline : ticket → YAML (apply-ticket)
index : build-annotations-index + check-annotations
tooling : détection médias orphelins / liens cassés
éventuellement : migration vers sharding par paragraphe (v2) si volume réel le justifie
+13
View File
@@ -25,6 +25,19 @@ Objectif : déployer une nouvelle version du site sur le NAS (DS220+) sans jamai
➡️ Déploiement = `docs/DEPLOY_PROD_SYNOLOGY_DS220.md` (procédure détaillée, à jour).
## Mise à jour (2026-03-03) — Gate CI de déploiement (SKIP / HOTPATCH / FULL) + preuves A/B
La procédure de déploiement “vivante” est désormais pilotée par **Gitea Actions** via le workflow :
- `.gitea/workflows/deploy-staging-live.yml`
Ce workflow décide automatiquement :
- **FULL** (rebuild + restart blue + green) dès qu’un changement impacte le build (ex: `src/content/`, `src/pages/`, `scripts/`, `src/anchors/`, etc.)
- **HOTPATCH** (patch JSON + copie media) quand le changement ne concerne que `src/annotations/` et/ou `public/media/`
- **SKIP** sinon
Les preuves et la procédure de test reproductible A/B sont documentées dans :
➡️ `docs/runbooks/DEPLOY-BLUE-GREEN.md` → section “CI Deploy gate (merge-proof) + Tests A/B + preuve alias injection”.
## Schéma (résumé, sans commandes)
- Ne jamais toucher au slot live.
+30 -1
View File
@@ -202,4 +202,33 @@ docker compose logs --tail=200 web_blue
docker compose logs --tail=200 web_green
# Si tu veux suivre en live :
docker compose logs -f web_green
docker compose logs -f web_green
## Historique synthétique (2026-03-03) — Stabilisation CI/CD “zéro surprise”
### Problème initial observé
- Déploiement parfois lancé en “hotpatch” alors qu’un rebuild était nécessaire.
- Sur merge commits, la détection de fichiers modifiés pouvait être ambiguë.
- Résultat : besoin de `force=1` manuel pour éviter des incohérences.
### Correctif appliqué
- Gate CI rendu **merge-proof** :
- lecture de `BEFORE` et `AFTER` depuis `event.json`
- calcul des fichiers modifiés via `git diff --name-only BEFORE AFTER`
- Politique de décision stabilisée :
- FULL auto dès qu’un changement impacte build/runtime (content/pages/scripts/anchors/etc.)
- HOTPATCH auto uniquement pour annotations/media
### Preuves
- Test A (touch src/content) :
- Gate flags: HAS_FULL=1 HAS_HOTPATCH=0 → MODE=full
- Test B (touch src/annotations) :
- Gate flags: HAS_FULL=0 HAS_HOTPATCH=1 → MODE=hotpatch
### Audit post-déploiement (preuves côté NAS)
- 8081 + 8082 répondent HTTP 200
- `/para-index.json` + `/annotations-index.json` OK
- Aliases injectés visibles dans HTML via `.para-alias` quand alias présent
+176
View File
@@ -0,0 +1,176 @@
# START-HERE — Archicratie / Édition Web (v2)
> Onboarding + exploitation “nickel chrome” (DEV → Gitea → CI → Release → Blue/Green → Edge/SSO)
## 0) TL;DR (la règle d’or)
- **Gitea = source canonique**.
- **main est protégé** : toute modification passe par **branche → PR → CI → merge**.
- **Le NAS n’est pas la source** : si un hotfix est fait sur NAS, on **backporte** via PR immédiatement.
- **Le site est statique Astro** : la prod sert du HTML (nginx), l’accès est contrôlé au niveau reverse-proxy (Traefik + Authelia).
## 1) Architecture mentale (ultra simple)
- **DEV (Mac Studio)** : édition + tests + commit + push
- **Gitea** : dépôt canon + PR + CI (CI.yaml)
- **NAS (DS220+)** : déploiement “blue/green”
- `web_blue` (staging upstream) → `127.0.0.1:8081`
- `web_green` (live upstream) → `127.0.0.1:8082`
- **Edge (Traefik)** : route les hosts
- `staging.archicratie...` → 8081
- `archicratie...` → 8082
- **Authelia** devant, via middleware `chain-auth@file`
## 2) Répertoires & conventions (repo)
### 2.1 Contenu canon (édition)
- `src/content/**` : contenu MD / MDX canon (Astro content collections)
- `src/pages/**` : routes Astro (index, [...slug], etc.)
- `src/components/**` : composants UI (SiteNav, TOC, SidePanel, etc.)
- `src/layouts/**` : layouts (EditionLayout, SiteLayout)
- `src/styles/**` : CSS global
### 2.2 Annotations (pré-Édition “tickets”)
- `src/annotations/<workKey>/<slug>.yml`
- Exemple : `src/annotations/archicrat-ia/prologue.yml`
- Objectif : stocker “Références / Médias / Commentaires” par page et par paragraphe (`p-...`).
### 2.3 Scripts (tooling / build)
- `scripts/inject-anchor-aliases.mjs` : injection aliases dans dist
- `scripts/dedupe-ids-dist.mjs` : retire IDs dupliqués dans dist
- `scripts/build-para-index.mjs` : index paragraphes (postbuild / predev)
- `scripts/build-annotations-index.mjs` : index annotations (postbuild / predev)
- `scripts/check-anchors.mjs` : contrat stabilité d’ancres (CI)
- `scripts/check-annotations*.mjs` : sanity YAML + médias
> Important : les scripts sont **partie intégrante** de la stabilité (IDs/ancres/indexation).
> On évite “la magie” : tout est scripté + vérifié.
## 3) Workflow Git “pro” (main protégé)
### 3.1 Cycle standard (toute modif)
en bash :
git checkout main
git pull --ff-only
BR="chore/xxx-$(date +%Y%m%d)"
git checkout -b "$BR"
# dev…
npm i
npm run build
npm run test:anchors
git add -A
git commit -m "xxx: description claire"
git push -u origin "$BR"
### 3.2 PR vers main
Ouvrir PR dans Gitea
CI doit être verte
Merge PR → main
### 3.3 Cas spécial : hotfix prod (NAS)
On peut faire un hotfix “urgence” en prod/staging si nécessaire…
MAIS : l’état final doit revenir dans Gitea : branche → PR → CI → merge.
## 4) Déploiement (NAS) — principe
### 4.1 Release pack
On génère un pack “reproductible” (source + config + scripts) puis on déploie.
### 4.2 Blue/Green
web_blue = staging upstream (8081)
web_green = live upstream (8082)
Edge Traefik sélectionne quel host pointe vers quel upstream.
## 5) Check-list “≤ 10 commandes” (happy path complet)
### 5.1 DEV (Mac)
git checkout main && git pull --ff-only
git checkout -b chore/my-change-$(date +%Y%m%d)
npm i
rm -rf .astro node_modules/.vite dist
npm run build
npm run test:anchors
npm run dev
### 5.2 Push + PR
git add -A
git commit -m "chore: my change"
git push -u origin chore/my-change-YYYYMMDD
# ouvrir PR dans Gitea
### 5.3 Déploiement NAS (résumé)
Voir docs/runbooks/DEPLOY-BLUE-GREEN.md.
## 6) Problèmes “classiques” + diagnostic rapide
### 6.1 “Le staging ne ressemble pas au local”
# Comparer upstream direct 8081 vs 8082 :
curl -sS http://127.0.0.1:8081/ | head -n 2
curl -sS http://127.0.0.1:8082/ | head -n 2
# Vérifier quel routeur edge répond (header diag) :
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router'
# Lire docs/runbooks/EDGE-TRAEFIK.md.
### 6.2 Canonical incorrect (localhost en prod)
Cause racine : site dans Astro = PUBLIC_SITE non injecté au build.
Fix canonique : voir docs/runbooks/ENV-PUBLIC_SITE.md.
Test :
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -1
### 6.3 Contrat “anchors” en échec après migration d’URL
Quand on déplace des routes (ex: /archicratie/archicrat-ia/* → /archicrat-ia/*), le test d’ancres peut échouer même si les IDs n’ont pas changé, car les pages ont changé de chemin.
# Procédure safe :
Backup baseline :
cp -a tests/anchors-baseline.json /tmp/anchors-baseline.json.bak.$(date +%F-%H%M%S)
Mettre à jour les clés (chemins) sans toucher aux IDs :
node - <<'NODE'
import fs from 'fs';
const p='tests/anchors-baseline.json';
const j=JSON.parse(fs.readFileSync(p,'utf8'));
const out={};
for (const [k,v] of Object.entries(j)) {
const nk = k.replace(/^archicratie\/archicrat-ia\//, 'archicrat-ia/');
out[nk]=v;
}
fs.writeFileSync(p, JSON.stringify(out,null,2)+'\n');
console.log('updated keys:', Object.keys(j).length, '->', Object.keys(out).length);
NODE
Re-run :
npm run test:anchors
## 7) Ce que l’étape 9 doit faire (orientation)
Stabiliser le pipeline “tickets → YAML annotations”
Formaliser la spec YAML + merge + anti-doublon (voir docs/EDITORIAL-ANNOTATIONS-SPEC.md)
Durcir l’onboarding (ce START-HERE + runbooks)
Éviter les régressions par tests (anchors / annotations / smoke)
@@ -0,0 +1,427 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg49"
sodipodi:docname="archicratie-web-edition-blue-green-runbook-verbatim-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview49"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="726.17247"
inkscape:cy="401.21029"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg49" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Runbook Blue/Green (v2, verbatim)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">Mise à jour 2026-02-20 — release-pack → releases/&lt;ts&gt;/app → current → docker compose web_blue/web_green</text>
<rect
x="40"
y="130"
width="500"
height="240"
class="box"
id="rect5" />
<text
x="58"
y="160"
class="h"
id="text6">0) Pré-requis</text>
<text
x="58"
y="184"
class="t"
id="text7">main protégé → travail via branches + PR</text>
<text
x="58"
y="202"
class="t"
id="text8">CI doit rester source de vérité</text>
<text
x="58"
y="220"
class="t"
id="text9">Éviter d'éditer une release en prod (hotfix = exception)</text>
<text
x="58"
y="238"
class="s"
id="text10">Si hotfix: on le re-synchronise ensuite dans Git (cf. étape 5)</text>
<rect
x="40"
y="400"
width="500"
height="260"
class="box2"
id="rect10" />
<text
x="58"
y="430"
class="h"
id="text11">1) Préparer une release (atelier DEV)</text>
<text
x="58"
y="454"
class="mono"
id="text12">npm ci &amp;&amp; npm run build</text>
<text
x="58"
y="472"
class="mono"
id="text13">release-pack.sh → tarball/artefact</text>
<text
x="58"
y="490"
class="mono"
id="text14">inclut dist/ + pagefind + indexes + build stamp</text>
<text
x="58"
y="508"
class="t"
id="text15">ouvrir PR → merge → CI</text>
<rect
x="40"
y="690"
width="500"
height="170"
class="box"
id="rect15" />
<text
x="58"
y="720"
class="h"
id="text16">2) Déposer sur NAS</text>
<text
x="58"
y="744"
class="mono"
id="text17">/volume2/docker/archicratie-web/releases/&lt;ts&gt;/app</text>
<text
x="58"
y="762"
class="mono"
id="text18">current → pointe vers la release active</text>
<text
x="58"
y="780"
class="t"
id="text19">build context docker = current OU release/app (selon compose)</text>
<rect
x="600"
y="130"
width="520"
height="210"
class="box"
id="rect19" />
<text
x="618"
y="160"
class="h"
id="text20">3) Build images</text>
<text
x="618"
y="184"
class="mono"
id="text21">sudo env DOCKER_API_VERSION=1.43 \</text>
<text
x="618"
y="202"
class="mono"
id="text22">docker compose -f docker-compose.yml build --no-cache \</text>
<text
x="618"
y="220"
class="mono"
id="text23"> web_blue web_green</text>
<text
x="618"
y="238"
class="s"
id="text24">les 2 images doivent builder OK</text>
<rect
x="639.93951"
y="378.7897"
width="480.06052"
height="241.21028"
class="box2"
id="rect24" />
<text
x="658"
y="410"
class="h"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">4) Switch trafic (blue ↔ green)</text>
<text
x="658"
y="434"
class="t"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">déterminer couleur active (proxy / conf)</text>
<text
x="658"
y="452"
class="t"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">mettre à jour routing vers l'autre couleur</text>
<text
x="658"
y="470"
class="t"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">reload proxy, vérifier 200/302</text>
<text
x="658"
y="488"
class="mono"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">curl -sSI -H 'Host: staging.*' http://127.0.0.1:18080/</text>
<text
x="658"
y="506"
class="s"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">rollback = revenir à l'ancienne couleur</text>
<rect
x="600"
y="660"
width="520"
height="210"
class="box"
id="rect30" />
<text
x="618"
y="690"
class="h"
id="text31">5) Hotfix de release → re-synchroniser Git (step8)</text>
<text
x="618"
y="714"
class="t"
id="text32">A) NAS: find src -mtime -3 → liste fichiers</text>
<text
x="618"
y="732"
class="t"
id="text33">B) NAS: tar -czf /tmp/hotfix.tgz -T liste</text>
<text
x="618"
y="750"
class="t"
id="text34">C) sha256 + manifest, puis scp vers Mac</text>
<text
x="618"
y="768"
class="t"
id="text35">D) Mac: tar -xzf → rsync --checksum vers repo</text>
<text
x="618"
y="786"
class="t"
id="text36">E) commit sur branche dédiée → push → PR vers main</text>
<rect
x="1180"
y="160"
width="380"
height="520"
class="box2"
id="rect36" />
<text
x="1198"
y="190"
class="h"
id="text37">Arborescence NAS (rappel)</text>
<text
x="1198"
y="214"
class="mono"
id="text38">/volume2/docker/archicratie-web/</text>
<text
x="1198"
y="232"
class="mono"
id="text39"> releases/</text>
<text
x="1198"
y="250"
class="mono"
id="text40"> 20260219-103222/</text>
<text
x="1198"
y="268"
class="mono"
id="text41"> app/ (ctx build)</text>
<text
x="1198"
y="286"
class="mono"
id="text42"> current -&gt; releases/…/app</text>
<text
x="1198"
y="304"
class="mono"
id="text43"> compose/ docker-compose.yml</text>
<text
x="1198"
y="322"
class="s"
id="text44">compose.expanded.yml t'indique le build.context effectif</text>
<path
d="M 540,520 H 642.36006"
class="arrowA"
id="path44"
sodipodi:nodetypes="cc" />
<text
x="545"
y="500"
class="s"
id="text45">→ NAS + build</text>
<path
d="M860 340 C860 360 860 360 860 380"
class="arrowA"
id="path45" />
<text
x="875"
y="365"
class="s"
id="text46">images OK</text>
<path
d="M860 620 C860 640 860 640 860 660"
class="arrowG"
id="path46" />
<text
x="875"
y="645"
class="s"
id="text47">si hotfix</text>
<rect
x="40"
y="20"
width="1520"
height="80"
class="box2"
id="rect47" />
<text
x="58"
y="50"
class="h"
id="text48">Règle d'or</text>
<text
x="58"
y="74"
class="t"
id="text49">La release doit être reproductible depuis Git. Toute modif manuelle en prod doit finir: (a) re-sync dans une branche, (b) PR, (c) merge, (d) prochaine release propre.</text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,551 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="1020"
viewBox="0 0 1600 1020"
version="1.1"
id="svg1"
sodipodi:docname="archicratie-web-edition-blue-green-runbook-verbatim.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-blue-green-runbook-verbatim.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="1.0606602"
inkscape:cx="675.05126"
inkscape:cy="300.28467"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="222"
inkscape:window-y="74"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<style
id="style1"><![CDATA[
.bg { fill: #fff; }
.outer { fill: #fff; stroke: #111; stroke-width: 3; rx: 18; }
.box { fill: #fff; stroke: #111; stroke-width: 2; rx: 14; }
.ok { fill: #eafff1; stroke: #1a7f37; stroke-width: 2; rx: 14; }
.warn { fill: #fff0f0; stroke: #b42318; stroke-width: 2; rx: 14; }
.title { font: 800 40px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
.subtitle { font: 500 16px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #333; }
.h2 { font: 800 24px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
.h3 { font: 800 20px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
.txt { font: 500 16px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
.small { font: 500 12px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #333; }
.mono { font: 500 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill: #111; }
.arrow { stroke: #111; stroke-width: 2; fill: none; marker-end: url(#arrow); }
.arrowThin { stroke: #111; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }
.cap { stroke-linecap: round; stroke-linejoin: round; }
]]></style>
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#111"
id="path1" />
</marker>
</defs>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1">
<!-- Title -->
<text
x="40"
y="55"
class="title"
id="text1">Archicratie – Web Edition : Blue/Green Runbook visuel (VERBATIM)</text>
<text
x="40"
y="88"
class="subtitle"
id="text2">Cible : déployer une nouvelle version sur le slot inactif (8081/8082), basculer via Traefik (dynamic/20-archicratie-backend.yml), vérifier (smoke tests), rollback en 30s si besoin.</text>
<!-- Outer frame -->
<rect
x="30"
y="110"
width="1540"
height="870"
class="outer"
id="rect2" />
<!-- Invariants box -->
<rect
x="60"
y="140"
width="1480"
height="130"
class="box"
id="rect3" />
<text
x="90"
y="175"
class="h2"
id="text3">Invariants (ce qui évite de casser la prod)</text>
<text
x="90"
y="198"
class="txt"
id="text4">• Les 2 slots existent en parallèle : archicratie-web-blue = 127.0.0.1:8081 et archicratie-web-green = 127.0.0.1:8082.</text>
<text
x="90"
y="220"
class="txt"
id="text5">• Traefik edge écoute :18080 et choisit le slot LIVE via /volume2/docker/edge/config/dynamic/20-archicratie-backend.yml.</text>
<text
x="90"
y="242"
class="txt"
id="text7">• Une seule cible active dans Traefik (pas de load-balance non déterministe). Rollback = remettre l’URL précédente dans le même fichier.</text>
<!-- RIGUEUR ABSOLUE (ajout non destructif) -->
<rect
x="64"
y="269"
width="1480"
height="30"
rx="12"
class="warn"
id="rect-rigueur" />
<text
x="84"
y="289"
class="txt"
id="text-rigueur"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">• RIGUEUR ABSOLUE : STAGING = slot INACTIF (opposé au LIVE). LIVE = 20-archicratie-backend.yml ; STAGING = 21-archicratie-staging.yml.</text>
<!-- Step columns -->
<!-- Column 1 -->
<rect
x="60"
y="300"
width="470"
height="610"
class="box"
id="rect4" />
<text
x="90"
y="335"
class="h2"
id="text8">Étape 1 — Build &amp; déployer</text>
<text
x="90"
y="365"
class="h3"
id="text9">But</text>
<text
x="90"
y="387"
class="txt"
id="text10">Mettre la nouvelle version sur le slot inactif</text>
<text
x="90"
y="409"
class="txt"
id="text11">(sans toucher au slot LIVE actuel).</text>
<!-- Où box -->
<rect
x="90"
y="435"
width="410"
height="210"
class="box"
id="rect5" />
<text
x="110"
y="465"
class="h3"
id="text12">Où</text>
<text
x="110"
y="490"
class="mono"
id="text13">/volume2/docker/archicratie-web/current/</text>
<text
x="110"
y="515"
class="txt"
id="text14">Compose : docker-compose.yml</text>
<text
x="110"
y="545"
class="txt"
id="text15">Slots :</text>
<text
x="140"
y="568"
class="mono"
id="text16">web_blue → 127.0.0.1:8081</text>
<text
x="140"
y="590"
class="mono"
id="text17">web_green → 127.0.0.1:8082</text>
<text
x="110"
y="622"
class="small"
id="text18">Le build injecte aussi PUBLIC_GITEA_* via build args</text>
<text
x="110"
y="640"
class="small"
id="text19">(déjà dans ton compose).</text>
<!-- Commands box -->
<rect
x="90"
y="665"
width="410"
height="225"
class="box"
id="rect6" />
<text
x="110"
y="695"
class="h3"
id="text20">Commandes (safe)</text>
<text
x="110"
y="720"
class="txt"
id="text21">1) Choisir le slot cible (inactif)</text>
<text
x="100"
y="742"
class="small"
id="text22"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Astuce : le LIVE = ce que pointe Traefik dans 20-archicratie-backend.yml</text>
<text
x="110"
y="770"
class="txt"
id="text23">2) Build + redémarrer uniquement ce slot</text>
<text
x="130"
y="794"
class="mono"
id="text24">cd /volume2/docker/archicratie-web/current</text>
<text
x="130"
y="814"
class="mono"
id="text25">sudo docker compose build web_green</text>
<text
x="130"
y="834"
class="mono"
id="text26">sudo docker compose up -d --no-deps web_green</text>
<text
x="110"
y="862"
class="small"
id="text27">Remplace web_green par web_blue selon la cible.</text>
<text
x="110"
y="880"
class="small"
id="text28">Ne pas toucher l’autre service.</text>
<!-- Arrow to column 2 -->
<path
d="M 530 605 L 560 605"
class="arrow cap"
id="path6" />
<!-- Column 2 -->
<rect
x="560"
y="300"
width="470"
height="610"
class="box"
id="rect7" />
<text
x="590"
y="335"
class="h2"
id="text29">Étape 2 — Switch Traefik (LIVE)</text>
<text
x="590"
y="365"
class="h3"
id="text30">But</text>
<text
x="590"
y="387"
class="txt"
id="text31">Basculer le LIVE en modifiant 1 fichier</text>
<text
x="590"
y="409"
class="txt"
id="text32">et laisser Traefik recharger automatiquement.</text>
<!-- Canon file box -->
<rect
x="565.48694"
y="433.11438"
width="458.08331"
height="176.31372"
class="box"
id="rect8"
ry="0" />
<text
x="610"
y="465"
class="h3"
id="text33">Fichier canonique (LIVE switch)</text>
<text
x="572"
y="490"
class="mono"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/volume2/docker/edge/config/dynamic/20-archicratie-backend.yml</text>
<text
x="610"
y="520"
class="txt"
id="text35">Contient :</text>
<text
x="588"
y="545"
class="mono"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http.services.archicratie_web.loadBalancer.servers[0].url</text>
<text
x="610"
y="575"
class="txt"
id="text37">Ex : http://127.0.0.1:8082 (green)</text>
<text
x="610"
y="597"
class="txt"
id="text38">ou http://127.0.0.1:8081 (blue)</text>
<!-- Procedure warn box -->
<rect
x="567.37256"
y="621.88562"
width="454.31201"
height="279.4281"
class="warn"
id="rect9" />
<text
x="610"
y="644"
class="h3"
id="text39"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:20px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Procédure (anti-casse)</text>
<text
x="576"
y="668"
class="txt"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">1) Backup horodaté du fichier</text>
<text
x="591"
y="690"
class="mono"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">cd /volume2/docker/edge/config/dynamic</text>
<text
x="577"
y="712"
class="mono"
id="text42"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
sodipodi:role="line"
id="tspan2"
x="577"
y="712">sudo cp 20-archicratie-backend.yml</tspan><tspan
sodipodi:role="line"
id="tspan3"
x="577"
y="727">20-archicratie-backend.yml.bak.$(date +%F-%H%M%S)</tspan></text>
<text
x="576"
y="752"
class="txt"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">2) Éditer l’URL (un seul backend)</text>
<text
x="591"
y="774"
class="mono"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">sudo vi 20-archicratie-backend.yml</text>
<text
x="591"
y="788"
class="small"
id="text47"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Changer uniquement la valeur url : 8081 ↔ 8082</text>
<text
x="571"
y="810"
class="txt"
id="text45bis"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
sodipodi:role="line"
id="tspan4"
x="571"
y="810">2bis) Mettre à jour 21-archicratie-staging.yml sur l’autre port</tspan><tspan
sodipodi:role="line"
id="tspan5"
x="571"
y="830">(opposé au LIVE)</tspan></text>
<text
x="571"
y="856"
class="txt"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">3) Traefik recharge (watch=true)</text>
<text
x="591"
y="878"
class="small"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan71"
x="591"
y="878">Pas de restart requis si provider file watch=true</tspan><tspan
sodipodi:role="line"
id="tspan72"
x="591"
y="893">(ton traefik.yml).</tspan></text>
<!-- Arrow to column 3 -->
<path
d="M 1030 605 L 1060 605"
class="arrow cap"
id="path7" />
<!-- Column 3 -->
<rect
x="1060"
y="300"
width="480"
height="610"
class="box"
id="rect10" />
<text
x="1090"
y="335"
class="h2"
id="text48">Étape 3 — Smoke tests</text>
<text
x="1090"
y="365"
class="h3"
id="text49">But</text>
<text
x="1090"
y="387"
class="txt"
id="text50">Prouver que le nouveau LIVE répond,</text>
<text
x="1090"
y="409"
class="txt"
id="text51">et que l’auth (Authelia/whoami) est OK.</text>
<!-- Slot direct ok box -->
<rect
x="1090"
y="435"
width="420"
height="185"
class="ok"
id="rect11" />
<text
x="1110"
y="465"
class="h3"
id="text52">Tests “slot direct” (preuve build)</text>
<text
x="1110"
y="490"
class="txt"
id="text53">Le slot construit doit répondre en 200 :</text>
<text
x="1130"
y="515"
class="mono"
id="text54">curl -sS -I http://127.0.0.1:8081/ | head -n 12</text>
<text
x="1130"
y="540"
class="mono"
id="text55">curl -sS -I http://127.0.0.1:8082/ | head -n 12</text>
<text
x="1110"
y="570"
class="small"
id="text56">L’un des deux peut rester l’ancien LIVE.</text>
<text
x="1110"
y="590"
class="small"
id="text57">L’objectif est que le slot cible soit OK.</text>
<!-- Edge tests box -->
<rect
x="1090"
y="638.4342"
width="420"
height="251.56581"
class="box"
id="rect54" />
<text
x="1110"
y="675"
class="h3"
id="text57b"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:20px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Tests “edge” (preuve routage + auth)</text>
<text
x="1110"
y="700"
class="txt"
id="text57c"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Host rules : tester AVEC Host header :</text>
<text
x="1130"
y="724"
class="mono"
id="text57d"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
sodipodi:role="line"
id="tspan57d1"
x="1130"
y="724">curl -sS -I -H 'Host:</tspan><tspan
sodipodi:role="line"
id="tspan57d2"
x="1130"
y="739">archicratie.trans-hands.synology.me'</tspan><tspan
sodipodi:role="line"
x="1130"
y="754"
id="tspan57d3">http://127.0.0.1:18080/ | head -n 20</tspan></text>
<text
x="1130"
y="780"
class="mono"
id="text58"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
id="tspan1" /></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,437 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg43"
sodipodi:docname="archicratie-web-edition-edge-routing-verbatim-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview43"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="424.81089"
inkscape:cy="464.14523"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg43" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Edge routing (v2, verbatim)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">Mise à jour 2026-02-20 — Host routing + Authelia + Blue/Green web_*</text>
<rect
x="40"
y="140"
width="420"
height="180"
class="box"
id="rect5" />
<text
x="58"
y="170"
class="h"
id="text6">Client (navigateur)</text>
<text
x="58"
y="194"
class="t"
id="text7">https://archicratie.* / https://staging.archicratie.*</text>
<text
x="58"
y="212"
class="t"
id="text8">cookies authelia_session</text>
<text
x="58"
y="230"
class="s"
id="text9">HEAD/GET → 302 si non auth</text>
<rect
x="60"
y="320"
width="110"
height="28"
class="chip2"
id="rect9" />
<text
x="74"
y="339"
class="mono"
id="text10">HTTPS 443</text>
<rect
x="520"
y="120"
width="520"
height="260"
class="box"
id="rect10" />
<text
x="538"
y="150"
class="h"
id="text11">Reverse-proxy (Nginx / DSM)</text>
<text
x="538"
y="174"
class="t"
id="text12">Routage par Host</text>
<text
x="538"
y="192"
class="t"
id="text13">auth_request → Authelia</text>
<text
x="538"
y="210"
class="t"
id="text14">proxy_pass → service web_blue ou web_green</text>
<text
x="538"
y="228"
class="t"
id="text15">headers: X-Forwarded-* + Host</text>
<text
x="538"
y="246"
class="s"
id="text16">en local: curl -H 'Host: ...' http://127.0.0.1:18080/</text>
<rect
x="540"
y="320"
width="142"
height="28"
class="chip"
id="rect16" />
<text
x="554"
y="339"
class="mono"
id="text17">auth_request</text>
<rect
x="520"
y="420"
width="520"
height="210"
class="box2"
id="rect17" />
<text
x="538"
y="450"
class="h"
id="text18">Auth stack</text>
<text
x="538"
y="474"
class="t"
id="text19">Authelia (portal login)</text>
<text
x="538"
y="492"
class="t"
id="text20">LLDAP (backend LDAP)</text>
<text
x="538"
y="510"
class="t"
id="text21">Redis (sessions / storage)</text>
<text
x="538"
y="528"
class="s"
id="text22">auth.* domain</text>
<rect
x="540"
y="600"
width="250"
height="28"
class="chipW"
id="rect22" />
<text
x="554"
y="619"
class="mono"
id="text23">302 → auth.*?rd=...</text>
<rect
x="1175.0378"
y="237.57942"
width="384.96219"
height="162.42058"
class="box"
id="rect23" />
<text
x="1198"
y="270"
class="h"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Service web_blue (container)</text>
<text
x="1198"
y="294"
class="mono"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">nginx static (dist/)</text>
<text
x="1198"
y="312"
class="mono"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">/pagefind/*, /assets/*</text>
<rect
x="1172.6172"
y="417.57944"
width="387.38275"
height="162.42058"
class="box"
id="rect26" />
<text
x="1198"
y="450"
class="h"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Service web_green (container)</text>
<text
x="1198"
y="474"
class="mono"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">nginx static (dist/)</text>
<text
x="1198"
y="492"
class="mono"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">build identique, couleur swap</text>
<rect
x="1120"
y="600"
width="230"
height="28"
class="chip2"
id="rect29" />
<text
x="1134"
y="619"
class="mono"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">une seule couleur active</text>
<rect
x="40"
y="410"
width="420"
height="220"
class="box2"
id="rect30" />
<text
x="58"
y="440"
class="h"
id="text31">Atelier DEV (local)</text>
<text
x="58"
y="464"
class="mono"
id="text32">astro dev : http://localhost:4321</text>
<text
x="58"
y="482"
class="mono"
id="text33">pas d'authelia</text>
<text
x="58"
y="500"
class="mono"
id="text34">predev génère:</text>
<text
x="58"
y="518"
class="mono"
id="text35"> /annotations-index.json</text>
<text
x="58"
y="536"
class="mono"
id="text36"> /para-index.json</text>
<text
x="58"
y="554"
class="s"
id="text37">404 = index manquant (relancer predev/dev)</text>
<path
d="M460 230 C500 230 500 230 520 230"
class="arrow"
id="path37" />
<text
x="465"
y="210"
class="s"
id="text38"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan45">requête</tspan></text>
<path
d="M780 380 C780 410 780 410 780 420"
class="arrow"
id="path38" />
<text
x="795"
y="410"
class="s"
id="text39">auth_request</text>
<path
d="m 1040,332 h 132.6172"
class="arrowA"
id="path39"
sodipodi:nodetypes="cc" />
<path
d="m 1040,464 131.407,2.42057"
class="arrowA"
id="path40"
sodipodi:nodetypes="cc" />
<text
x="1045"
y="317"
class="s"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan44">proxy_pass (active)</tspan></text>
<path
d="M780 630 C780 700 340 700 250 630"
class="arrow"
id="path41" />
<text
x="462.36005"
y="707.89716"
class="s"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
id="tspan43"
style="font-size:14.6667px">callback + cookie</tspan></text>
<rect
x="40"
y="820"
width="1520"
height="60"
class="box2"
id="rect41" />
<text
x="58"
y="850"
class="h"
id="text42">Note importante (debug)</text>
<text
x="58"
y="874"
class="s"
id="text43">Si tu testes via loopback (127.0.0.1:18080), la directive Host détermine la vhost. Sans Host correct, tu peux tomber sur une autre conf.</text>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,324 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="820"
viewBox="0 0 1600 820"
version="1.1"
id="svg36"
sodipodi:docname="archicratie-web-edition-edge-routing-verbatim.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-edge-routing-verbatim.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview36"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="665.65809"
inkscape:cy="354.00908"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg36" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<text
x="40"
y="45"
class="title"
id="text1">Edge Traefik (verbatim) — routers Host(...) + middlewares + services</text>
<text
x="40"
y="75"
class="small"
id="text2">Source : /volume2/docker/edge/config/dynamic/10-core.yml + 20-archicratie-backend.yml + 21-archicratie-staging.yml + 30-lldap-ui.yml</text>
<rect
x="35"
y="110"
width="1530"
height="670"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">Traefik : entryPoint web = :18080 — provider file (dynamic/) watch=true</text>
<!-- Middlewares -->
<rect
x="60"
y="185"
width="601.60364"
height="196.36914"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="215"
class="h2"
id="text4">Middlewares (10-core.yml)</text>
<text
x="80"
y="242"
class="txt"
id="text5">sanitize-remote : purge Remote-* + force X-Forwarded-Proto/Port</text>
<text
x="80"
y="264"
class="txt"
id="text6">authelia : forwardAuth → <tspan
class="mono"
id="tspan5">http://127.0.0.1:9091/api/authz/forward-auth</tspan></text>
<text
x="80"
y="286"
class="txt"
id="text7">chain-auth : [sanitize-remote, authelia]</text>
<!-- Routers -->
<rect
x="60"
y="415"
width="823.08624"
height="205.02269"
rx="12"
class="box"
id="rect7" />
<text
x="80"
y="445"
class="h2"
id="text8">Routers</text>
<text
x="80"
y="472"
class="mono"
id="text9">archicratie</text>
<text
x="200"
y="472"
class="txt"
id="text10">Host(archicratie.trans-hands.synology.me) + chain-auth → service archicratie_web</text>
<text
x="80"
y="498"
class="mono"
id="text11">archicratie-authinfo</text>
<text
x="290"
y="498"
class="txt"
id="text12">Host(archicratie…) PathPrefix(/_auth/whoami) + chain-auth → whoami</text>
<text
x="80"
y="524"
class="mono"
id="text13">gitea</text>
<text
x="200"
y="524"
class="txt"
id="text14">Host(gitea.archicratie.trans-hands.synology.me) + sanitize-remote → gitea_web</text>
<text
x="80"
y="550"
class="mono"
id="text15">archicratie-staging</text>
<text
x="290"
y="550"
class="txt"
id="text16">Host(staging.archicratie.trans-hands.synology.me) + chain-auth → archicratie_blue</text>
<text
x="80"
y="576"
class="mono"
id="text17">lldap-ui</text>
<text
x="200"
y="576"
class="txt"
id="text18">Host(lldap.archicratie.trans-hands.synology.me) + chain-auth → lldap_ui</text>
<rect
x="925.50684"
y="181.36914"
width="614.49316"
height="553.63086"
rx="12"
class="box"
id="rect18" />
<text
x="985"
y="215"
class="h2"
id="text19"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Services (loadBalancer → url)</text>
<text
x="985"
y="250"
class="mono"
id="text20"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">whoami</text>
<text
x="1120"
y="250"
class="txt"
id="text21"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ <tspan
class="mono"
id="tspan20"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:18081</tspan> (edge-whoami)</text>
<text
x="985"
y="285"
class="mono"
id="text22"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea_web</text>
<text
x="1120"
y="285"
class="txt"
id="text23"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ <tspan
class="mono"
id="tspan22"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:3000</tspan> (Gitea)</text>
<text
x="985"
y="320"
class="mono"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">archicratie_web</text>
<text
x="1120"
y="320"
class="txt"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ défini par <tspan
class="mono"
id="tspan24"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">20-archicratie-backend.yml</tspan></text>
<text
x="1140"
y="345"
class="txt"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• actuel : <tspan
class="mono"
id="tspan25"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:8082</tspan> (green)</text>
<text
x="985"
y="390"
class="mono"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">archicratie_blue</text>
<text
x="1170"
y="390"
class="txt"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ <tspan
class="mono"
id="tspan27"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:8081</tspan> (staging)</text>
<text
x="985"
y="435"
class="mono"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">lldap_ui</text>
<text
x="1120"
y="435"
class="txt"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ <tspan
class="mono"
id="tspan29"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:17170</tspan> (LLDAP UI)</text>
<rect
x="954.1377"
y="493.94855"
width="560.8623"
height="216.05144"
rx="12"
class="note"
id="rect30" />
<text
x="975"
y="530"
class="h2"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Interprétation debug (safe)</text>
<text
x="975"
y="555"
class="txt"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si tu testes sans Host header sur :18080 → 404 (normal)</text>
<text
x="975"
y="577"
class="txt"
id="text33"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si archicratie → 302 auth.* : Authelia forward-auth OK</text>
<text
x="975"
y="599"
class="txt"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si /_auth/whoami → 302 auth.* : gate OK (non-auth)</text>
<text
x="975"
y="621"
class="txt"
id="text35"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Pour basculer blue/green : modifier 20-archicratie-backend.yml (8081 ↔ 8082)</text>
<text
x="975"
y="643"
class="small"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">But : une seule cible active (évite load-balance non déterministe).</text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,870 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1500"
height="940"
viewBox="0 0 1500 940"
role="img"
aria-label="Workflow Git CI - main protégé, PR, CI, release-pack, déploiement blue/green"
version="1.1"
id="svg93"
sodipodi:docname="archicratie-web-edition-git-ci-workflow-v1.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview93"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.88133333"
inkscape:cx="253.02572"
inkscape:cy="536.11952"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg93" />
<defs
id="defs2">
<style
id="style1">
/* ✅ Version “Inkscape-safe” : pas de var(), pas de rgba() */
.canvasBg { fill:#f8fafc; stroke:#e2e8f0; stroke-width:1; }
.title { font:800 26px/1.2 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; }
.subtitle { font:600 14px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
.laneTitle { font:800 14px/1 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; letter-spacing:.2px; }
.laneNote { font:600 12px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
.boxTitle { font:800 14px/1.2 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; }
.boxText { font:600 12px/1.35 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
.mono { font:700 11px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Liberation Mono&quot;,&quot;Courier New&quot;,monospace; fill:#334155; }
.lane { fill:#f1f5f9; fill-opacity:.70; stroke:#cbd5e1; stroke-width:1; }
.laneAlt { fill:#e2e8f0; fill-opacity:.55; stroke:#cbd5e1; stroke-width:1; }
.box { fill:#ffffff; fill-opacity:.92; stroke:#94a3b8; stroke-width:1.4; }
.boxAlt { fill:#f8fafc; fill-opacity:.92; stroke:#94a3b8; stroke-width:1.4; }
.tag { font:800 10px/1 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; }
.tagOk { fill:#16a34a; }
.tagWarn { fill:#f59e0b; }
.tagInfo { fill:#2563eb; }
.tagDanger { fill:#dc2626; }
.arrow { stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#arrowHead); }
.arrowSoft { stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#arrowHeadSoft); }
.dashed { stroke-dasharray:7 6; }
.callout { fill:#ffffff; fill-opacity:.70; stroke:#cbd5e1; stroke-width:1.1; }
.small { font:600 11px/1.35 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
</style>
<marker
id="arrowHead"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#64748b"
id="path1" />
</marker>
<marker
id="arrowHeadSoft"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#94a3b8"
id="path2" />
</marker>
</defs>
<!-- ✅ Fond explicite + bordure douce -->
<rect
x="0.5"
y="0.5"
width="1499"
height="939"
class="canvasBg"
rx="26"
id="rect2" />
<!-- Header -->
<text
x="44"
y="54"
class="title"
id="text2">Archicratie — Workflow Git “pro” (main protégé) + CI + Release + Blue/Green</text>
<text
x="44"
y="82"
class="subtitle"
id="text3">
Objectif : partir d’un hotfix appliqué (si besoin), le remettre proprement sous Git (branche → PR → CI → merge),
puis produire une release packagée et déployer sans régression.
</text>
<!-- Lanes -->
<rect
x="40"
y="115"
width="440"
height="770"
class="lane"
rx="18"
id="rect3" />
<rect
x="520"
y="115"
width="430"
height="770"
class="laneAlt"
rx="18"
id="rect4" />
<rect
x="980"
y="115"
width="250"
height="770"
class="lane"
rx="18"
id="rect5" />
<rect
x="1250"
y="115"
width="210"
height="770"
class="laneAlt"
rx="18"
id="rect6" />
<text
x="60"
y="142"
class="laneTitle"
id="text6"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Atelier DEV (Mac Studio)</text>
<text
x="60"
y="164"
class="laneNote"
id="text7"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.4;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Travail local, build, commit, push de branche</text>
<text
x="540"
y="148"
class="laneTitle"
id="text8">Gitea (remote)</text>
<text
x="540"
y="170"
class="laneNote"
id="text9">main verrouillé · PR obligatoire · historique canon</text>
<text
x="1000"
y="148"
class="laneTitle"
id="text10">CI (CI.yaml)</text>
<text
x="1000"
y="170"
class="laneNote"
id="text11">build checks · gate de merge</text>
<text
x="1270"
y="148"
class="laneTitle"
id="text12">NAS (Prod)</text>
<text
x="1270"
y="170"
class="laneNote"
id="text13">release-pack + blue/green</text>
<!-- Boxes: Mac -->
<rect
x="70"
y="170"
width="380"
height="105"
class="box"
rx="16"
id="rect13" />
<text
x="92"
y="198"
class="boxTitle"
id="text14"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">1) Se baser sur main (canon)</text>
<text
x="92"
y="220"
class="boxText"
id="text15"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Synchroniser le dépôt local sur le dernier état validé.</text>
<text
x="92"
y="242"
class="mono"
id="text16"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git checkout main git pull --ff-only</text>
<text
x="92"
y="264"
class="small"
id="text17"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagInfo"
id="tspan16"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">INFO</tspan> main est protégé : pas de commit direct.</text>
<rect
x="70"
y="300"
width="380"
height="125"
class="boxAlt"
rx="16"
id="rect17" />
<text
x="92"
y="328"
class="boxTitle"
id="text18"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">2) Créer une branche dédiée “hotfix sync”</text>
<text
x="92"
y="350"
class="boxText"
id="text19"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Nom explicite + date. Toute la synchro se fait ici.</text>
<text
x="92"
y="372"
class="mono"
id="text20"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git checkout -b chore/step8-sync-hotfix-YYYYMMDD</text>
<text
x="92"
y="394"
class="small"
id="text21"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan94"
x="92"
y="394"><tspan
class="tag tagWarn"
id="tspan20"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Optionnel : appliquer un pack hotfix (tar/sha/rsync)</tspan><tspan
sodipodi:role="line"
id="tspan95"
x="92"
y="408.85001">si prod a bougé.</tspan></text>
<rect
x="70"
y="455"
width="380"
height="160"
class="box"
rx="16"
id="rect21" />
<text
x="92"
y="483"
class="boxTitle"
id="text22"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">3) Appliquer les changements vérifier</text>
<text
x="72"
y="505"
class="boxText"
id="text23"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Copier/merge les fichiers (rsync/checksum), puis tester build/dev.</text>
<text
x="92"
y="527"
class="mono"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">rm -rf .astro node_modules/.vite</text>
<text
x="92"
y="548"
class="mono"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm i npm run build</text>
<text
x="92"
y="569"
class="mono"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm run dev</text>
<text
x="92"
y="593"
class="small"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagOk"
id="tspan26"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">OK</tspan> On ne push que si build + postbuild passent.</text>
<rect
x="70"
y="640"
width="380"
height="145"
class="boxAlt"
rx="16"
id="rect27" />
<text
x="92"
y="668"
class="boxTitle"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">4) Commit propre + diff lisible</text>
<text
x="92"
y="690"
class="boxText"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Inspecter, puis commiter en message clair (hotfix étape X).</text>
<text
x="92"
y="712"
class="mono"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git status git diff</text>
<text
x="92"
y="733"
class="mono"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"><tspan
sodipodi:role="line"
id="tspan96"
x="92"
y="733">git add -A git commit -m &quot;step8: sync hotfix</tspan><tspan
sodipodi:role="line"
id="tspan97"
x="92"
y="747.84998">(SidePanel/reading)&quot;</tspan></text>
<text
x="92"
y="770"
class="small"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagInfo"
id="tspan31"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">TIP</tspan> Garder le commit “gros” mais unique si c’est un backport prod.</text>
<rect
x="70"
y="805"
width="380"
height="65"
class="box"
rx="16"
id="rect32" />
<text
x="92"
y="833"
class="boxTitle"
id="text33"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">5) Push de branche vers Gitea</text>
<text
x="92"
y="855"
class="mono"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git push -u origin chore/step8-sync-hotfix-YYYYMMDD</text>
<!-- Boxes: Gitea -->
<rect
x="536.38428"
y="241.13464"
width="396.0968"
height="123.86536"
class="box"
rx="16"
id="rect34" />
<text
x="572"
y="268"
class="boxTitle"
id="text35">6) Ouvrir une PR vers main</text>
<text
x="554"
y="290"
class="boxText"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">main protégé → PR obligatoire. Décrire : “backport hotfix prod”.</text>
<text
x="554"
y="312"
class="small"
id="text37"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagWarn"
id="tspan36"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Ajouter contexte : fichiers touchés, risque, checks attendus.</text>
<text
x="572"
y="334"
class="small"
id="text38"><tspan
class="tag tagInfo"
id="tspan37">INFO</tspan> La PR déclenche CI.yaml (pipeline de validation).</text>
<rect
x="538.65356"
y="396.13464"
width="389.28894"
height="135"
class="boxAlt"
rx="16"
id="rect38" />
<text
x="572"
y="423"
class="boxTitle"
id="text39">7) Review + décisions</text>
<text
x="552"
y="445"
class="boxText"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Lecture diff, vérif logique, pas de secrets, pas de régressions UI.</text>
<text
x="572"
y="468"
class="small"
id="text41"><tspan
class="tag tagDanger"
id="tspan40">STOP</tspan> Si CI rouge : corriger sur la branche, push → CI relancé.</text>
<text
x="572"
y="490"
class="small"
id="text42"><tspan
class="tag tagOk"
id="tspan41">OK</tspan> Si CI vert + review OK : merge autorisé.</text>
<rect
x="537.51892"
y="548.65356"
width="390.42358"
height="138.82753"
class="box"
rx="16"
id="rect42" />
<text
x="572"
y="588"
class="boxTitle"
id="text43">8) Merge PR → main (canon)</text>
<text
x="572"
y="610"
class="boxText"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan107"
x="572"
y="610">main devient l’unique source officielle.</tspan><tspan
sodipodi:role="line"
id="tspan108"
x="572"
y="626.20001">La prod se recale dessus.</tspan></text>
<text
x="572"
y="646"
class="mono"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">Merge (UI Gitea) → origin/main updated</text>
<text
x="572"
y="668"
class="small"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagInfo"
id="tspan45"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">INFO</tspan> Optionnel : tagger une release (vX.Y / date).</text>
<rect
x="550"
y="705"
width="370"
height="145"
class="boxAlt"
rx="16"
id="rect46" />
<text
x="572"
y="733"
class="boxTitle"
id="text47">9) Préparer une release packagée</text>
<text
x="572"
y="755"
class="boxText"
id="text48"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan105"
x="572"
y="755">Générer un paquet de release reproductible</tspan><tspan
sodipodi:role="line"
id="tspan106"
x="572"
y="771.20001">(sources + scripts + config).</tspan></text>
<text
x="572"
y="791"
class="mono"
id="text49"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">./release-pack.sh</text>
<text
x="572"
y="813"
class="small"
id="text50"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagWarn"
id="tspan49"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Le pack sert au déploiement sur NAS (blue/green).</text>
<text
x="572"
y="835"
class="small"
id="text51"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagInfo"
id="tspan50"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">TIP</tspan> Conserver checksum + manifest (traçabilité).</text>
<!-- Boxes: CI -->
<rect
x="1000"
y="260"
width="210"
height="160"
class="box"
rx="16"
id="rect51" />
<text
x="1018"
y="288"
class="boxTitle"
id="text52">CI : checks</text>
<text
x="1018"
y="310"
class="boxText"
id="text53">npm ci</text>
<text
x="1018"
y="330"
class="boxText"
id="text54">astro build</text>
<text
x="1018"
y="350"
class="boxText"
id="text55">postbuild scripts</text>
<text
x="1018"
y="370"
class="boxText"
id="text56">pagefind</text>
<text
x="1018"
y="394"
class="small"
id="text57"><tspan
class="tag tagOk"
id="tspan56">PASS</tspan> → merge autorisé</text>
<text
x="1018"
y="414"
class="small"
id="text58"><tspan
class="tag tagDanger"
id="tspan57">FAIL</tspan> → corriger branche</text>
<rect
x="1000"
y="450"
width="210"
height="105"
class="boxAlt"
rx="16"
id="rect58" />
<text
x="1018"
y="478"
class="boxTitle"
id="text59">Artefacts</text>
<text
x="1018"
y="500"
class="boxText"
id="text60">Logs + traces</text>
<text
x="1018"
y="520"
class="boxText"
id="text61">Optionnel : build artefact</text>
<text
x="1018"
y="540"
class="small"
id="text62"><tspan
class="tag tagInfo"
id="tspan61">INFO</tspan> Sert au diagnostic rapide.</text>
<!-- Boxes: NAS -->
<rect
x="1270"
y="260"
width="170"
height="155"
class="box"
rx="16"
id="rect62" />
<text
x="1288"
y="288"
class="boxTitle"
id="text63">Déploiement</text>
<text
x="1288"
y="312"
class="boxText"
id="text64">Importer release</text>
<text
x="1288"
y="332"
class="boxText"
id="text65">docker build</text>
<text
x="1288"
y="352"
class="boxText"
id="text66">web_blue / web_green</text>
<text
x="1288"
y="376"
class="boxText"
id="text67">switch proxy</text>
<text
x="1288"
y="398"
class="small"
id="text68"><tspan
class="tag tagOk"
id="tspan67">OK</tspan> rollback possible</text>
<rect
x="1270"
y="450"
width="170"
height="140"
class="boxAlt"
rx="16"
id="rect68" />
<text
x="1288"
y="478"
class="boxTitle"
id="text69">Runbook</text>
<text
x="1288"
y="500"
class="boxText"
id="text70">healthchecks</text>
<text
x="1288"
y="520"
class="boxText"
id="text71">logs</text>
<text
x="1288"
y="540"
class="boxText"
id="text72">validation UI</text>
<text
x="1288"
y="566"
class="small"
id="text73"><tspan
class="tag tagWarn"
id="tspan72">MANUEL</tspan> staging d’abord</text>
<rect
x="1270"
y="640"
width="170"
height="210"
class="box"
rx="16"
id="rect73" />
<text
x="1288"
y="668"
class="boxTitle"
id="text74">Hotfix prod</text>
<text
x="1288"
y="690"
class="boxText"
id="text75">À éviter si possible</text>
<text
x="1288"
y="710"
class="boxText"
id="text76">Si nécessaire :</text>
<text
x="1288"
y="732"
class="boxText"
id="text77">pack (tar+sha)</text>
<text
x="1288"
y="754"
class="boxText"
id="text78">→ rapatrier DEV</text>
<text
x="1288"
y="778"
class="small"
id="text79"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan109"
x="1288"
y="778"><tspan
style="fill:#ff0000"
id="tspan111">RISK</tspan> Toujours backporter</tspan><tspan
sodipodi:role="line"
id="tspan110"
x="1288"
y="792.84998">via PR.</tspan></text>
<!-- Callout -->
<rect
x="988.19214"
y="665.28741"
width="231.68678"
height="196.73224"
class="callout"
rx="14"
id="rect79" />
<text
x="999"
y="701"
class="boxTitle"
id="text80"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Règle d’or</text>
<text
x="999"
y="725"
class="boxText"
id="text81"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan98"
x="999"
y="725">Le NAS n’est pas le dépôt source.</tspan><tspan
sodipodi:role="line"
id="tspan99"
x="999"
y="741.20001">Même si un hotfix a été fait en prod,</tspan></text>
<text
x="999"
y="760"
class="boxText"
id="text82"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan100"
x="999"
y="760">l’état final “vrai” doit être : branche</tspan><tspan
sodipodi:role="line"
id="tspan101"
x="999"
y="776.20001">→ PR → CI → merge main → release</tspan><tspan
sodipodi:role="line"
x="999"
y="792.87439"
id="tspan102">→ deploy.</tspan></text>
<text
x="999"
y="812"
class="small"
id="text83"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan103"
x="999"
y="812"><tspan
class="tag tagOk"
id="tspan82"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">BUT</tspan> Passation étape 9 = base Git propre</tspan><tspan
sodipodi:role="line"
id="tspan104"
x="999"
y="826.84998">+ reproductible.</tspan></text>
<!-- Arrows -->
<path
class="arrow"
d="m 450,222.34493 c 50,0 46.38427,58.78971 86.38427,78.78971"
id="path83"
sodipodi:nodetypes="cc" />
<path
class="arrow"
d="m 450,835 c 50,0 60,-15 100,-55"
id="path84" />
<path
class="arrow"
d="M 931.75492,299.19062 C 995.29501,300.15129 924.67474,339.65204 1000,340"
id="path85"
sodipodi:nodetypes="cc" />
<path
class="arrowSoft dashed"
d="M 888.63843,365 C 909.06203,422.26929 925.80938,435.71104 1000,500"
id="path86"
sodipodi:nodetypes="cc" />
<path
class="arrow"
d="M1210 340 C1240 340, 1245 340, 1270 340"
id="path87" />
<path
class="arrow"
d="M920 620 C980 620, 1000 620, 1080 620"
id="path88" />
<path
class="arrow"
d="m 920,620 c 60,0 311.3313,0.2118 347.7307,-236.67171"
id="path89"
sodipodi:nodetypes="cc" />
<path
class="arrowSoft dashed"
d="m 1269.652,672.90469 c -83.9636,0.6354 -155.1134,-27.51891 -348.51736,-27.76853"
id="path90"
sodipodi:nodetypes="cc" />
<!-- Footnote -->
<text
x="44"
y="916"
class="subtitle"
id="text93">
Légende : <tspan
class="tag tagInfo"
id="tspan90">INFO</tspan> invariant / contexte · <tspan
class="tag tagWarn"
id="tspan91">MANUEL</tspan> action humaine ·
<tspan
class="tag tagOk"
id="tspan92">OK</tspan> attendu · <tspan
class="tag tagDanger"
id="tspan93">STOP</tspan> bloquant.
</text>
</svg>

After

Width:  |  Height:  |  Size: 29 KiB

@@ -0,0 +1,537 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg57"
sodipodi:docname="archicratie-web-edition-global-verbatim-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview57"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="540.99849"
inkscape:cy="369.74281"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg57" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Vue globale (v2, verbatim)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">Étape 8 (hotfix UI + sync Git) — mise à jour 2026-02-20 — Astro static + Pagefind + Authelia + Blue/Green</text>
<rect
x="40"
y="120"
width="470"
height="290"
class="box"
id="rect5" />
<text
x="58"
y="150"
class="h"
id="text6">Atelier DEV (Mac Studio)</text>
<text
x="58"
y="174"
class="mono"
id="text7">repo git (branches, PR vers main)</text>
<text
x="58"
y="192"
class="mono"
id="text8">npm run dev → http://localhost:4321</text>
<text
x="58"
y="210"
class="mono"
id="text9">npm run build → dist/ (static)</text>
<text
x="58"
y="228"
class="mono"
id="text10">postbuild:</text>
<text
x="58"
y="246"
class="mono"
id="text11"> inject-anchor-aliases.mjs</text>
<text
x="58"
y="264"
class="mono"
id="text12"> dedupe-ids-dist.mjs</text>
<text
x="58"
y="282"
class="mono"
id="text13"> build-para-index.mjs → dist/para-index.json</text>
<text
x="58"
y="300"
class="mono"
id="text14"> build-annotations-index.mjs → dist/annotations-index.json</text>
<text
x="58"
y="318"
class="mono"
id="text15"> pagefind → dist/pagefind/</text>
<text
x="58"
y="336"
class="s"
id="text16">predev: build public/para-index.json + public/annotations-index.json</text>
<rect
x="60"
y="420"
width="206"
height="28"
class="chip2"
id="rect16" />
<text
x="74"
y="439"
class="mono"
id="text17">dist/ + pagefind + indexes</text>
<rect
x="300"
y="420"
width="198"
height="28"
class="chip"
id="rect17" />
<text
x="314"
y="439"
class="mono"
id="text18"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">public/*-index.json</text>
<rect
x="40"
y="470"
width="470"
height="330"
class="box2"
id="rect18" />
<text
x="58"
y="500"
class="h"
id="text19">UI Lecture / Édition (dans le site)</text>
<text
x="58"
y="524"
class="t"
id="text20">EditionLayout.astro (globals + meta)</text>
<text
x="58"
y="542"
class="t"
id="text21">SidePanel.astro (reading-follow + annotations + propose)</text>
<text
x="58"
y="560"
class="t"
id="text22">LevelToggle.astro (Niveaux)</text>
<text
x="58"
y="578"
class="t"
id="text23">global.css (UX lecture + TOC-local sync)</text>
<text
x="58"
y="596"
class="s"
id="text24">SidePanel consomme para-index + annotations-index</text>
<text
x="58"
y="614"
class="s"
id="text25">ProposeModal ouvre une issue Gitea (direct ou via bridge)</text>
<text
x="58"
y="632"
class="mono"
id="text26">env publics: PUBLIC_GITEA_* + PUBLIC_ISSUE_BRIDGE_PATH</text>
<rect
x="673.92584"
y="119.57942"
width="344.13016"
height="250"
class="box"
id="rect26" />
<text
x="723"
y="180"
class="h"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Gitea (sur NAS) — source of truth</text>
<text
x="723"
y="204"
class="t"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">main protégé (push direct interdit)</text>
<text
x="723"
y="222"
class="t"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">branches de travail → PR → merge</text>
<text
x="723"
y="240"
class="t"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">CI (workflow) : build + checks + artefacts</text>
<text
x="723"
y="258"
class="t"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">issues + labels (tickets)</text>
<rect
x="745"
y="320"
width="126"
height="28"
class="chip2"
id="rect31" />
<text
x="759"
y="339"
class="mono"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PR → CI.yaml</text>
<rect
x="565"
y="430"
width="470"
height="160"
class="box2"
id="rect32" />
<text
x="583"
y="460"
class="h"
id="text33">Hotfix de release → re-sync Git (méthode step8)</text>
<text
x="583"
y="484"
class="t"
id="text34">1) lister fichiers modifiés sur NAS</text>
<text
x="583"
y="502"
class="t"
id="text35">2) tar + sha256 → transfert</text>
<text
x="583"
y="520"
class="t"
id="text36">3) rsync --checksum vers repo local</text>
<text
x="583"
y="538"
class="t"
id="text37">4) commit sur branche dédiée + push + PR</text>
<rect
x="1183.1921"
y="122.42058"
width="376.80786"
height="247.57942"
class="box"
id="rect37" />
<text
x="1228"
y="150"
class="h"
id="text38"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">NAS DS220+ — runtime Blue/Green</text>
<text
x="1228"
y="174"
class="mono"
id="text39"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">/volume2/docker/archicratie-web/</text>
<text
x="1228"
y="192"
class="mono"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">releases/&lt;timestamp&gt;/app (build context)</text>
<text
x="1228"
y="210"
class="mono"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">current → release active</text>
<text
x="1228"
y="228"
class="mono"
id="text42"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">docker compose: web_blue / web_green</text>
<text
x="1228"
y="246"
class="s"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">une seule couleur sert le trafic (reverse-proxy)</text>
<rect
x="1210"
y="310"
width="322"
height="28"
class="chipW"
id="rect43" />
<text
x="1224"
y="329"
class="mono"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">docker compose build --no-cache web_*</text>
<rect
x="1184.4025"
y="393.94858"
width="375.59756"
height="246.05144"
class="box2"
id="rect44" />
<text
x="1208"
y="430"
class="h"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Edge &amp; Auth</text>
<text
x="1208"
y="454"
class="t"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Reverse-proxy (Nginx) protège le site</text>
<text
x="1208"
y="472"
class="t"
id="text47"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Authelia (SSO) + LLDAP (LDAP) + Redis</text>
<text
x="1208"
y="490"
class="t"
id="text48"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">302 vers auth.* si non authentifié</text>
<text
x="1208"
y="508"
class="t"
id="text49"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Host header: staging.* / archicratie.*</text>
<text
x="1208"
y="526"
class="s"
id="text50"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">/_auth/whoami utilisé en check côté client (peut 404 en local)</text>
<path
d="m 510,260 163.92587,-1.21029"
class="arrowA"
id="path50"
sodipodi:nodetypes="cc" />
<text
x="514"
y="245"
class="s"
id="text51"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan57">git push (branche) + PR</tspan></text>
<path
d="m 1016.8457,240 h 166.3464"
class="arrowA"
id="path51"
sodipodi:nodetypes="cc" />
<text
x="1025.3177"
y="206.9062"
class="s"
id="text52"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
sodipodi:role="line"
id="tspan58"
x="1025.3177"
y="206.9062"
style="font-size:14.6667px">CI/artefact</tspan><tspan
sodipodi:role="line"
id="tspan59"
x="1025.3177"
y="226.70625"
style="font-size:14.6667px">→ déploiement release</tspan></text>
<path
d="M1420 650 C1480 650 1520 650 1560 650"
class="arrow"
id="path52" />
<text
x="1420"
y="632"
class="s"
id="text53">HTTPS → navigateur</text>
<path
d="M 510,640.25719 C 540,640.25719 540,520 565,520"
class="arrow"
id="path53"
sodipodi:nodetypes="cc" />
<text
x="534.84113"
y="620.20575"
class="s"
id="text54"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan60">Propose → issue</tspan></text>
<path
d="m 1181.9818,520 -145.7715,-1.21029"
class="arrowG"
id="path54"
sodipodi:nodetypes="cc" />
<path
d="M565 520 C520 520 520 520 510 520"
class="arrowG"
id="path55" />
<text
x="800"
y="505"
class="s"
id="text55">tar+sha256 → scp → rsync --checksum</text>
<rect
x="40"
y="820"
width="1520"
height="60"
class="box2"
id="rect55" />
<text
x="58"
y="850"
class="h"
id="text56">Légende</text>
<text
x="58"
y="874"
class="s"
id="text57"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan61">Bleu = flux Git/CI · Vert = flux de re-sync hotfix · Orange = build runtime · Le reste = navigation/HTTP</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,828 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="1020"
viewBox="0 0 1600 1020"
version="1.1"
id="svg80"
sodipodi:docname="archicratie-web-edition-global-verbatim.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-global-verbatim.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview80"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="594.25113"
inkscape:cy="481.08926"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg80" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.h3 { font: 700 14px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<!-- Header -->
<text
x="40"
y="45"
class="title"
id="text1">Archicratie – Web Edition : schéma global VERBATIM (Mac Studio ↔ NAS Synology DS220+)</text>
<text
x="40"
y="75"
class="small"
id="text2">Factuel (capturé sur ton NAS) : DSM (TLS) → Traefik :18080 (file provider) → routers Host(...) → (Authelia forward-auth) → backends (blue/green). Gitea via Traefik sans chain-auth.</text>
<!-- LOCAL -->
<rect
x="35"
y="110"
width="520"
height="880"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">LOCAL — Mac Studio (atelier)</text>
<rect
x="60"
y="175"
width="470"
height="110"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="205"
class="h2"
id="text4">Repo site (Astro)</text>
<text
x="80"
y="232"
class="txt"
id="text5">• build statique → dist/</text>
<text
x="80"
y="254"
class="txt"
id="text6">• postbuild : inject aliases + dedupe IDs + indexes + pagefind</text>
<rect
x="60"
y="305"
width="470"
height="115"
rx="12"
class="box"
id="rect6" />
<text
x="80"
y="335"
class="h2"
id="text7">Tooling (scripts/)</text>
<text
x="66"
y="360"
class="txt"
id="text8"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/inject-anchor-aliases.mjs</text>
<text
x="66"
y="382"
class="txt"
id="text9"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/apply-ticket.mjs --alias</text>
<text
x="66"
y="404"
class="txt"
id="text10"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/check-anchor-aliases.mjs + verify-anchor-aliases-in-dist.mjs</text>
<rect
x="60"
y="445"
width="470"
height="105"
rx="12"
class="box"
id="rect10" />
<text
x="80"
y="475"
class="h2"
id="text11">Déploiement (release pack)</text>
<text
x="80"
y="500"
class="txt"
id="text12">• build Docker avec ARG/ENV : PUBLIC_GITEA_BASE/OWNER/REPO</text>
<text
x="80"
y="522"
class="txt"
id="text13">• pousse/maj sur NAS (containers web_blue/web_green)</text>
<rect
x="60"
y="569"
width="470"
height="165"
rx="12"
class="note"
id="rect13" />
<text
x="80"
y="605"
class="h2"
id="text14">Repères “vrais” côté site</text>
<text
x="80"
y="632"
class="txt"
id="text15">• whoami runtime : <tspan
class="mono"
id="tspan14">/_auth/whoami</tspan></text>
<text
x="80"
y="654"
class="txt"
id="text16">• variables injectées : <tspan
class="mono"
id="tspan15">PUBLIC_GITEA_BASE/OWNER/REPO</tspan></text>
<text
x="80"
y="676"
class="txt"
id="text17">• anchors canon : <tspan
class="mono"
id="tspan16">src/anchors/anchor-aliases.json</tspan></text>
<text
x="80"
y="698"
class="txt"
id="text18">• injection build-time : <tspan
class="mono"
id="tspan17">scripts/inject-anchor-aliases.mjs</tspan></text>
<!-- NAS -->
<rect
x="590"
y="110"
width="975"
height="880"
rx="18"
class="zone"
id="rect18" />
<text
x="615"
y="145"
class="h2"
id="text19">DISTANT — NAS Synology DS220+ (DSM + Container Manager)</text>
<!-- Users -->
<rect
x="615"
y="175"
width="270"
height="90"
rx="12"
class="box"
id="rect19" />
<text
x="635"
y="205"
class="h2"
id="text20">Utilisateurs</text>
<text
x="635"
y="230"
class="txt"
id="text21">• Web (public)</text>
<text
x="635"
y="252"
class="txt"
id="text22">• Éditeurs (groupe LDAP)</text>
<!-- DSM RP -->
<rect
x="905"
y="175"
width="630"
height="120"
rx="12"
class="box"
id="rect22" />
<text
x="930"
y="205"
class="h2"
id="text23">DSM Reverse Proxy (TLS terminé ici)</text>
<text
x="930"
y="230"
class="txt"
id="text24">• Host archicratie.trans-hands.synology.me → 127.0.0.1:18080</text>
<text
x="930"
y="252"
class="txt"
id="text25">• Host gitea.archicratie.trans-hands.synology.me → 127.0.0.1:18080</text>
<text
x="930"
y="274"
class="txt"
id="text26">• (idem staging.*, lldap.* si routés via Traefik)</text>
<!-- Edge Traefik -->
<rect
x="1012.7156"
y="321.2103"
width="522.28442"
height="148.78972"
rx="12"
class="box"
id="rect26" />
<text
x="1050"
y="350"
class="h2"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">edge-traefik (traefik:v2.11) — network_mode: host</text>
<text
x="1050"
y="375"
class="txt"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• entryPoint web : <tspan
class="mono"
id="tspan27"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">:18080</tspan></text>
<text
x="1050"
y="397"
class="txt"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• provider file : <tspan
class="mono"
id="tspan28"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/etc/traefik/dynamic</tspan> (watch: true)</text>
<text
x="1050"
y="419"
class="txt"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Host rules (routers) + middlewares (chain-auth / sanitize-remote)</text>
<text
x="1050"
y="441"
class="small"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">Tes 404 initiaux venaient d’un test sans Host: les routers utilisent Host(...)</text>
<!-- Dynamic files -->
<rect
x="615"
y="290"
width="270"
height="180"
rx="12"
class="note"
id="rect31" />
<text
x="623"
y="320"
class="h2"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Fichiers dynamiques (edge)</text>
<text
x="623"
y="345"
class="mono"
id="text33"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/volume2/docker/edge/config/dynamic/</text>
<text
x="623"
y="368"
class="txt"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 10-core.yml (routers + chain-auth)</text>
<text
x="623"
y="390"
class="txt"
id="text35"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 20-archicratie-backend.yml (slot actif)</text>
<text
x="623"
y="412"
class="txt"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan88"
x="623"
y="412">• 21-archicratie-staging.yml</tspan><tspan
sodipodi:role="line"
id="tspan89"
x="623"
y="428.25">(staging→8081)</tspan></text>
<text
x="623"
y="448"
class="txt"
id="text37"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 30-lldap-ui.yml (lldap UI)</text>
<!-- Auth stack -->
<rect
x="615"
y="495"
width="376.80786"
height="258.41147"
rx="12"
class="box"
id="rect37" />
<text
x="635"
y="525"
class="h2"
id="text38">Auth stack (auth)</text>
<text
x="635"
y="550"
class="txt"
id="text39">auth-authelia (authelia:4.39.13) — host</text>
<text
x="635"
y="572"
class="txt"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan92"
x="635"
y="572">• forward-auth :</tspan><tspan
sodipodi:role="line"
id="tspan93"
x="635"
y="588.25">http://127.0.0.1:9091/api/authz/forward-auth</tspan></text>
<text
x="635"
y="614"
class="txt"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">auth-lldap (lldap:stable)</text>
<text
x="635"
y="640"
class="txt"
id="text42"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• LDAP : <tspan
class="mono"
id="tspan41"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:3890</tspan> • UI : <tspan
class="mono"
id="tspan42"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:17170</tspan></text>
<text
x="635"
y="662"
class="txt"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">auth-redis (redis:7-alpine)</text>
<text
x="635"
y="684"
class="txt"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• exposé : <tspan
class="mono"
id="tspan43"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:6380</tspan></text>
<text
x="635"
y="708"
class="small"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan94"
x="635"
y="708">Traefik injecte Remote-* via forward-auth,</tspan><tspan
sodipodi:role="line"
id="tspan95"
x="635"
y="723">et purge l’entrée (sanitize-remote).</tspan></text>
<!-- Whoami service -->
<rect
x="1110"
y="495"
width="365.69592"
height="117.57942"
rx="12"
class="box"
id="rect45" />
<text
x="1135"
y="525"
class="h2"
id="text46">edge-whoami (traefik/whoami)</text>
<text
x="1135"
y="550"
class="txt"
id="text47">• exposé : <tspan
class="mono"
id="tspan46">127.0.0.1:18081 → 80</tspan></text>
<text
x="1135"
y="572"
class="txt"
id="text48">• router Traefik : <tspan
class="mono"
id="tspan47">PathPrefix('/_auth/whoami')</tspan></text>
<text
x="1135"
y="594"
class="txt"
id="text49">• protégé par <tspan
class="mono"
id="tspan48">chain-auth</tspan> (302 login si non auth)</text>
<!-- Web blue/green -->
<rect
x="1047.9349"
y="639.94855"
width="224.96217"
height="116.11195"
rx="12"
class="box"
id="rect49" />
<text
x="1070"
y="670"
class="h2"
id="text50"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">archicratie-web-blue</text>
<text
x="1070"
y="695"
class="txt"
id="text51"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 127.0.0.1:8081 → 80</text>
<text
x="1070"
y="717"
class="txt"
id="text52"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Nginx sert dist/</text>
<text
x="1070"
y="739"
class="small"
id="text53"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">slot blue (staging cible 8081)</text>
<rect
x="1295"
y="640"
width="240.69592"
height="116.11195"
rx="12"
class="box"
id="rect53" />
<text
x="1320"
y="670"
class="h2"
id="text54"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">archicratie-web-green</text>
<text
x="1320"
y="695"
class="txt"
id="text55"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 127.0.0.1:8082 → 80</text>
<text
x="1320"
y="717"
class="txt"
id="text56"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Nginx sert dist/</text>
<text
x="1320"
y="739"
class="small"
id="text57"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">slot green (backend actuel)</text>
<rect
x="1156.7399"
y="778.21478"
width="374.62918"
height="105.4161"
rx="12"
class="note"
id="rect57" />
<text
x="1190.2118"
y="797.89716"
class="txt"
id="text58"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
id="tspan96"
x="1190.2118"
y="797.89716"
sodipodi:role="line">Bascule blue/green (Traefik) :</tspan><tspan
x="1190.2118"
y="814.14716"
id="tspan104"
sodipodi:role="line">modifier <tspan
class="mono"
id="tspan57"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">dynamic/20-archicratie-backend.yml</tspan></tspan><tspan
id="tspan97"
x="1190.2118"
y="830.39716"
sodipodi:role="line">→ url 8081/8082 (un seul backend actif)</tspan></text>
<text
x="1202.3751"
y="858.05145"
class="small"
id="text59"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan101"
x="1202.3751"
y="858.05145">Actuellement (d’après ton dump) :<tspan
class="mono"
id="tspan58"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"></tspan></tspan><tspan
sodipodi:role="line"
id="tspan102"
x="1202.3751"
y="873.05145"><tspan
class="mono"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"
id="tspan103">archicratie_web → http://127.0.0.1:8082</tspan></tspan></text>
<!-- Gitea + Runner -->
<rect
x="615"
y="790"
width="440.12103"
height="180.57489"
rx="12"
class="box"
id="rect59" />
<text
x="635"
y="820"
class="h2"
id="text60"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Gitea (actuel)</text>
<text
x="635"
y="845"
class="txt"
id="text61"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• conteneur : <tspan
class="mono"
id="tspan60"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea-old-2026-02-09-105211</tspan></text>
<text
x="635"
y="867"
class="txt"
id="text62"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• port : <tspan
class="mono"
id="tspan61"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">0.0.0.0:3000</tspan> (Traefik route aussi vers 127.0.0.1:3000)</text>
<text
x="635"
y="889"
class="txt"
id="text63"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan90"
x="635"
y="889">• router Traefik : Host(gitea.archicratie...)</tspan><tspan
sodipodi:role="line"
id="tspan91"
x="635"
y="905.25">+ middleware sanitize-remote (pas chain-auth)</tspan></text>
<text
x="635"
y="929"
class="small"
id="text64"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan99"
x="635"
y="929">“Proposer” dépend de PUBLIC_GITEA_* corrects</tspan><tspan
sodipodi:role="line"
id="tspan100"
x="635"
y="944">(owner casse sensible).</tspan></text>
<rect
x="1160"
y="895"
width="375"
height="85"
rx="12"
class="box"
id="rect64" />
<text
x="1185"
y="925"
class="h2"
id="text65"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">gitea-act-runner</text>
<text
x="1185"
y="950"
class="txt"
id="text66"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• image : <tspan
class="mono"
id="tspan65"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea/act_runner:0.2.11</tspan></text>
<text
x="1185"
y="972"
class="small"
id="text67"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">CI : labels / checks (anchors, aliases, etc.).</text>
<!-- Connections -->
<path
d="M 885 220 L 905 220"
class="line"
id="path67" />
<!-- Users -> DSM -->
<path
d="M 1220 295 L 1220 320"
class="line"
id="path68" />
<!-- DSM -> Traefik -->
<!-- Traefik -> auth (forward auth) -->
<path
d="m 1005,420 -45,75"
class="dash"
id="path69" />
<text
x="120.96539"
y="1057.8674"
class="small"
id="text69"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"
transform="rotate(-57.013356)">forward-auth</text>
<!-- Traefik -> whoami -->
<path
d="M 1220 470 L 1220 495"
class="line"
id="path70" />
<!-- Traefik -> web service -->
<path
d="m 1082.9955,470 -0.3782,168.78971"
class="dash"
id="path71"
sodipodi:nodetypes="cc" />
<path
d="m 1500,470 -0.416,170"
class="dash"
id="path72"
sodipodi:nodetypes="cc" />
<!-- Traefik -> gitea -->
<path
d="m 1034.826,471.21029 1.3616,315.67322"
class="dash"
id="path73"
sodipodi:nodetypes="cc" />
<!-- Gitea -> runner -->
<path
d="m 1060,890 98.7897,59.52345"
class="line"
id="path74"
sodipodi:nodetypes="cc" />
<!-- Local -> NAS (release/deploy) -->
<path
d="M 530 505 L 615 505"
class="line"
id="path75" />
<!-- Legend -->
<rect
x="61.210289"
y="750.47656"
width="467.57938"
height="215.31776"
rx="12"
class="note"
id="rect75" />
<text
x="80"
y="777"
class="h2"
id="text75"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Lecture (opérationnelle)</text>
<text
x="80"
y="798"
class="txt"
id="text76"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan86"
x="80"
y="798">1) Web public : DSM → Traefik :18080 → Host(archicratie...)</tspan><tspan
sodipodi:role="line"
id="tspan87"
x="80"
y="814.40051">→ chain-auth → backend (8081/8082)</tspan></text>
<text
x="80"
y="836"
class="txt"
id="text77"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan84"
x="80"
y="836">2) Gate éditeurs : site appelle /_auth/whoami</tspan><tspan
sodipodi:role="line"
id="tspan85"
x="80"
y="852.25">→ Traefik route vers edge-whoami (protégé)</tspan></text>
<text
x="80"
y="874"
class="txt"
id="text78"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan82"
x="80"
y="874">3) Gitea : Host(gitea...) → Traefik → 127.0.0.1:3000</tspan><tspan
sodipodi:role="line"
id="tspan83"
x="80"
y="890.40051">(sanitize-remote)</tspan></text>
<text
x="80"
y="912"
class="txt"
id="text79"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan80"
x="80"
y="912">4) Blue/green : changer <tspan
class="mono"
id="tspan78"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">dynamic/20-archicratie-backend.yml</tspan></tspan><tspan
sodipodi:role="line"
id="tspan81"
x="80"
y="928.25">(un seul backend actif)</tspan></text>
<text
x="80"
y="950"
class="small"
id="text80"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">NB : un test sans Host sur :18080 renvoie 404 (normal, Host rules).</text>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,409 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg43"
sodipodi:docname="archicratie-web-edition-machine-editoriale-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview43"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="1108.6233"
inkscape:cy="435.09834"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg43" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Machine éditoriale (v2)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">De la source au site (lecture + annotations + propositions) — 2026-02-20</text>
<rect
x="40"
y="140"
width="460"
height="256.62631"
class="box"
id="rect5" />
<text
x="118"
y="230"
class="h"
id="text6"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Sources (repo)</text>
<text
x="118"
y="254"
class="t"
id="text7"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Contenu : src/content/** (MD/MDX)</text>
<text
x="118"
y="272"
class="t"
id="text8"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Annotations : src/annotations/** (YAML)</text>
<text
x="118"
y="290"
class="t"
id="text9"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI : src/layouts + src/components + global.css</text>
<text
x="118"
y="308"
class="s"
id="text10"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Plugin paragraph-ids ajoute des ids stables sur paragraphes</text>
<rect
x="560"
y="140"
width="500"
height="260"
class="box"
id="rect10" />
<text
x="698"
y="230"
class="h"
id="text11"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Build (Astro static)</text>
<text
x="698"
y="254"
class="t"
id="text12"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">astro build → dist/**/index.html</text>
<text
x="698"
y="272"
class="t"
id="text13"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">meta Pagefind: edition/level/status/version</text>
<text
x="698"
y="290"
class="t"
id="text14"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Layout : EditionLayout + SiteLayout</text>
<text
x="698"
y="308"
class="s"
id="text15"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">data-pagefind-body = zone indexée</text>
<rect
x="560"
y="430"
width="497.57944"
height="249.74281"
class="box2"
id="rect15" />
<text
x="658"
y="520"
class="h"
id="text16"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Postbuild (qualité + recherche + indexes)</text>
<text
x="658"
y="544"
class="t"
id="text17"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Aliases d'ancres (backward compat)</text>
<text
x="658"
y="562"
class="t"
id="text18"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Dédoublonnage d'IDs (anti-régression)</text>
<text
x="658"
y="580"
class="t"
id="text19"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Index des paragraphes (para-index)</text>
<text
x="658"
y="598"
class="t"
id="text20"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Index des annotations (annotations-index)</text>
<text
x="658"
y="616"
class="t"
id="text21"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Pagefind (recherche full-text)</text>
<rect
x="1120"
y="140"
width="436.36914"
height="259.36459"
class="box"
id="rect21" />
<text
x="1238"
y="210"
class="h"
id="text22"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Artefacts (dist/)</text>
<text
x="1238"
y="234"
class="mono"
id="text23"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">HTML statique + assets</text>
<text
x="1238"
y="252"
class="mono"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/pagefind/**</text>
<text
x="1238"
y="270"
class="mono"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/para-index.json</text>
<text
x="1238"
y="288"
class="mono"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/annotations-index.json</text>
<text
x="1238"
y="306"
class="s"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">(en dev) recopiés dans public/*-index.json</text>
<rect
x="1120"
y="430"
width="436.36917"
height="249.48566"
class="box2"
id="rect27" />
<text
x="1178"
y="520"
class="h"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Runtime navigateur (lecture)</text>
<text
x="1178"
y="544"
class="t"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">LocalToc sync (H2/H3)</text>
<text
x="1178"
y="562"
class="t"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">banner-follow + reading-follow__inner</text>
<text
x="1178"
y="580"
class="t"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">SidePanel: niveaux + annotations + propose</text>
<text
x="1178"
y="598"
class="s"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Comportement lecture: H2/H3 unifiés (plus d’accordéon gênant)</text>
<rect
x="40"
y="430"
width="460"
height="250"
class="box2"
id="rect32" />
<text
x="138"
y="524"
class="h"
id="text33"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Flux “Proposer” (tickets)</text>
<text
x="138"
y="548"
class="t"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI collecte: page + paragraphe + type + message</text>
<text
x="138"
y="566"
class="t"
id="text35"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Création d'issue Gitea (labels)</text>
<text
x="138"
y="584"
class="t"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Lien retour: issue → page + id</text>
<text
x="138"
y="602"
class="s"
id="text37"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Option: bridge same-origin pour éviter CORS/auth</text>
<path
d="M500 250 C530 250 530 250 560 250"
class="arrowA"
id="path37" />
<path
d="M810 400 C810 420 810 420 810 430"
class="arrowA"
id="path38" />
<path
d="M1060 250 C1090 250 1090 250 1120 250"
class="arrowA"
id="path39" />
<path
d="M 1338.7897,398.15432 1340,430"
class="arrow"
id="path40"
sodipodi:nodetypes="cc" />
<path
d="M500 540 C620 540 620 520 560 520"
class="arrow"
id="path41" />
<text
x="520"
y="525"
class="s"
id="text41">issues</text>
<rect
x="40"
y="820"
width="1520"
height="60"
class="box2"
id="rect41" />
<text
x="58"
y="850"
class="h"
id="text42">Conseil de maintenance</text>
<text
x="58"
y="874"
class="s"
id="text43">Toute évolution UI/indices doit rester déterministe : build identique sur Mac, CI, et NAS. En cas de hotfix, re-sync via PR.</text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,596 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1500"
height="940"
viewBox="0 0 1500 940"
role="img"
aria-label="Archicratie — Machine éditoriale (synthèse) : DEV/PROD, indices, postbuild, proposer/bridge"
version="1.1"
id="svg71"
sodipodi:docname="archicratie-web-edition-machine-editoriale-v3.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview71"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.88133333"
inkscape:cx="794.25113"
inkscape:cy="350.03782"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg71" />
<defs
id="defs2">
<style
id="style1">
/* Inkscape-safe: pas de CSS variables, fond explicite */
.title{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:26px; font-weight:800; fill:#0f172a;}
.sub{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:600; fill:#475569;}
.laneT{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:800; fill:#0f172a; letter-spacing:.2px;}
.laneN{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:600; fill:#475569;}
.h{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:800; fill:#0f172a;}
.p{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:600; fill:#475569;}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Liberation Mono&quot;,&quot;Courier New&quot;,monospace; font-size:11px; font-weight:700; fill:#334155;}
.small{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:11px; font-weight:600; fill:#475569;}
.canvas{fill:#f8fafc; stroke:#e2e8f0; stroke-width:1;}
.lane{fill:#f1f5f9; stroke:#cbd5e1; stroke-width:1;}
.laneAlt{fill:#eef2f7; stroke:#cbd5e1; stroke-width:1;}
.box{fill:#ffffff; stroke:#94a3b8; stroke-width:1.4;}
.boxAlt{fill:#f8fafc; stroke:#94a3b8; stroke-width:1.4;}
.call{fill:#ffffff; fill-opacity:.72; stroke:#cbd5e1; stroke-width:1.1;}
.arrow{stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#ah);}
.arrowSoft{stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#ahSoft);}
.dash{stroke-dasharray:7 6;}
</style>
<marker
id="ah"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#64748b"
id="path1" />
</marker>
<marker
id="ahSoft"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#94a3b8"
id="path2" />
</marker>
</defs>
<!-- Fond explicite -->
<rect
x="0.5"
y="0.5"
width="1499"
height="939"
rx="26"
class="canvas"
id="rect2" />
<!-- Titre -->
<text
x="44"
y="54"
class="title"
id="text2">Archicratie — Machine éditoriale (synthèse “exploitation + onboarding”)</text>
<text
x="44"
y="82"
class="sub"
id="text3">Inclut : DEV vs PROD (indices), ordre exact du postbuild, et fork “Proposer” (direct vs bridge same-origin).</text>
<!-- Lanes -->
<rect
x="40"
y="115"
width="390"
height="770"
rx="18"
class="lane"
id="rect3" />
<rect
x="450"
y="115"
width="390"
height="770"
rx="18"
class="laneAlt"
id="rect4" />
<rect
x="860"
y="115"
width="330"
height="770"
rx="18"
class="lane"
id="rect5" />
<rect
x="1210"
y="115"
width="250"
height="770"
rx="18"
class="laneAlt"
id="rect6" />
<text
x="60"
y="148"
class="laneT"
id="text6">1) Sources &amp; vérité éditoriale</text>
<text
x="60"
y="170"
class="laneN"
id="text7">Ce qui est versionné (canon) et transforme l’édition</text>
<text
x="470"
y="148"
class="laneT"
id="text8">2) Build Astro (static)</text>
<text
x="470"
y="170"
class="laneN"
id="text9">Rendu HTML + UI d’édition (EditionLayout)</text>
<text
x="880"
y="148"
class="laneT"
id="text10">3) Postbuild (ordre exact)</text>
<text
x="880"
y="170"
class="laneN"
id="text11">Anti-régressions + index + recherche</text>
<text
x="1230"
y="148"
class="laneT"
id="text12">4) Runtime &amp; feedback</text>
<text
x="1230"
y="170"
class="laneN"
id="text13">DEV (public) / PROD (dist) + Proposer</text>
<!-- Lane 1 -->
<rect
x="70"
y="210"
width="330"
height="135"
rx="16"
class="box"
id="rect13" />
<text
x="92"
y="238"
class="h"
id="text14">Sources amont (traçabilité)</text>
<text
x="92"
y="260"
class="p"
id="text15">Fichiers “sources/” (docx/pdf) + historiques.</text>
<text
x="92"
y="282"
class="mono"
id="text16">sources/** (non servi tel quel)</text>
<text
x="92"
y="304"
class="small"
id="text17">→ import/pipeline vers le contenu canon.</text>
<rect
x="70"
y="365"
width="330"
height="190"
rx="16"
class="boxAlt"
id="rect17" />
<text
x="92"
y="393"
class="h"
id="text18">Contenu canon (site)</text>
<text
x="92"
y="415"
class="p"
id="text19">Pages : MD/MDX (Astro content)</text>
<text
x="92"
y="437"
class="mono"
id="text20">src/content/**</text>
<text
x="92"
y="461"
class="p"
id="text21">Annotations : YAML</text>
<text
x="92"
y="483"
class="mono"
id="text22">src/annotations/**</text>
<text
x="78"
y="507"
class="small"
id="text23">Ces deux entrées alimentent l’UI (SidePanel, highlights, etc.).</text>
<rect
x="70"
y="575"
width="330"
height="140"
rx="16"
class="box"
id="rect23" />
<text
x="92"
y="603"
class="h"
id="text24">Scripts d’import / qualité</text>
<text
x="92"
y="625"
class="p"
id="text25">Import DOCX, contrôle d’IDs, aliases, etc.</text>
<text
x="92"
y="647"
class="mono"
id="text26">scripts/*.mjs</text>
<text
x="86"
y="671"
class="small"
id="text27">Objectif : build reproductible + pas de régression d’ancres.</text>
<!-- Lane 2 -->
<rect
x="480"
y="210"
width="330"
height="170"
rx="16"
class="box"
id="rect27" />
<text
x="502"
y="238"
class="h"
id="text28">EditionLayout (UI d’édition)</text>
<text
x="502"
y="260"
class="p"
id="text29"><tspan
sodipodi:role="line"
id="tspan71"
x="502"
y="260">SiteNav + TOC global + TOC local + reading-follow</tspan><tspan
sodipodi:role="line"
id="tspan72"
x="502"
y="275">+ SidePanel.</tspan></text>
<text
x="508"
y="300"
class="mono"
id="text30">src/layouts/EditionLayout.astro</text>
<text
x="508"
y="322"
class="mono"
id="text31">src/components/SidePanel.astro</text>
<text
x="508"
y="344"
class="small"
id="text32"><tspan
sodipodi:role="line"
id="tspan73"
x="508"
y="344">Globals boot (flags/env)</tspan><tspan
sodipodi:role="line"
id="tspan74"
x="508"
y="357.75">+ interactions “Propos / Réfs / Illus / Com”.</tspan></text>
<rect
x="480"
y="400"
width="330"
height="160"
rx="16"
class="boxAlt"
id="rect32" />
<text
x="502"
y="428"
class="h"
id="text33">Build statique</text>
<text
x="502"
y="450"
class="p"
id="text34">Astro génère HTML &amp; assets.</text>
<text
x="502"
y="472"
class="mono"
id="text35">npm run build → astro build</text>
<text
x="502"
y="496"
class="p"
id="text36">Sortie :</text>
<text
x="502"
y="518"
class="mono"
id="text37">dist/**</text>
<!-- Lane 3 -->
<rect
x="890"
y="210"
width="270"
height="265"
rx="16"
class="box"
id="rect37" />
<text
x="912"
y="238"
class="h"
id="text38">Postbuild : ordre exact (fixe)</text>
<text
x="912"
y="264"
class="mono"
id="text39">1) inject-anchor-aliases.mjs</text>
<text
x="912"
y="286"
class="mono"
id="text40">2) dedupe-ids-dist.mjs</text>
<text
x="912"
y="308"
class="mono"
id="text41">3) build-para-index.mjs</text>
<text
x="912"
y="330"
class="mono"
id="text42">4) build-annotations-index.mjs</text>
<text
x="912"
y="352"
class="mono"
id="text43">5) pagefind</text>
<text
x="912"
y="380"
class="p"
id="text44">Sorties PROD :</text>
<text
x="912"
y="402"
class="mono"
id="text45">dist/para-index.json</text>
<text
x="912"
y="424"
class="mono"
id="text46">dist/annotations-index.json</text>
<text
x="912"
y="446"
class="mono"
id="text47">dist/pagefind/**</text>
<!-- Lane 4 -->
<rect
x="1230"
y="210"
width="210"
height="200"
rx="16"
class="box"
id="rect47" />
<text
x="1252"
y="238"
class="h"
id="text48">DEV vs PROD : indices</text>
<text
x="1252"
y="262"
class="p"
id="text49">PROD (statique) lit dans :</text>
<text
x="1252"
y="284"
class="mono"
id="text50">dist/*.json</text>
<text
x="1252"
y="308"
class="p"
id="text51">DEV (astro dev) sert depuis :</text>
<text
x="1252"
y="330"
class="mono"
id="text52">public/*.json</text>
<text
x="1252"
y="356"
class="small"
id="text53"><tspan
sodipodi:role="line"
id="tspan75"
x="1252"
y="356">DEV : predev copie/génère</tspan><tspan
sodipodi:role="line"
id="tspan76"
x="1252"
y="369.75">les index pour éviter les 404.</tspan></text>
<rect
x="1230"
y="430"
width="210"
height="215"
rx="16"
class="boxAlt"
id="rect53" />
<text
x="1252"
y="458"
class="h"
id="text54">“Proposer” : 2 modes</text>
<text
x="1252"
y="482"
class="p"
id="text55">A) Direct client → Gitea API</text>
<text
x="1252"
y="504"
class="small"
id="text56">⚠️ CORS/auth/token (déconseillé)</text>
<text
x="1252"
y="536"
class="p"
id="text57">B) Bridge same-origin (reco)</text>
<text
x="1252"
y="558"
class="mono"
id="text58">PUBLIC_ISSUE_BRIDGE_PATH</text>
<text
x="1252"
y="582"
class="small"
id="text59"><tspan
sodipodi:role="line"
id="tspan77"
x="1252"
y="582">UI → bridge → Gitea</tspan><tspan
sodipodi:role="line"
id="tspan78"
x="1252"
y="596.18488">(secrets côté serveur)</tspan></text>
<rect
x="1230"
y="665"
width="210"
height="120"
rx="16"
class="box"
id="rect59" />
<text
x="1252"
y="693"
class="h"
id="text60">Gitea (Issues)</text>
<text
x="1252"
y="717"
class="p"
id="text61">Création + suivi</text>
<text
x="1252"
y="739"
class="mono"
id="text62">/issues/new</text>
<text
x="1232"
y="763"
class="small"
id="text63">Labels/assignee selon règles d’équipe.</text>
<!-- Callout (rappel pro) -->
<rect
x="470"
y="590"
width="720"
height="120"
rx="14"
class="call"
id="rect63" />
<text
x="490"
y="618"
class="h"
id="text64">Rappel “pro” (anti régression)</text>
<text
x="490"
y="642"
class="p"
id="text65">• L’ordre du postbuild ne doit pas changer sans raison : il garantit ancres stables + index cohérents.</text>
<text
x="490"
y="664"
class="p"
id="text66">• DEV sert des index dans <tspan
class="mono"
id="tspan65">public/</tspan> ; PROD lit dans <tspan
class="mono"
id="tspan66">dist/</tspan>.</text>
<text
x="490"
y="686"
class="p"
id="text67">• Pour “Proposer”, préférer le bridge same-origin : pas de token côté navigateur.</text>
<!-- Arrows -->
<path
class="arrow"
d="M400 460 C430 460, 450 450, 480 480"
id="path67" />
<path
class="arrow"
d="M810 480 C840 480, 860 470, 890 430"
id="path68" />
<path
class="arrowSoft dash"
d="M1030 460 C1120 500, 1180 520, 1230 330"
id="path69" />
<path
class="arrow"
d="M1180 590 C1210 590, 1220 590, 1230 540"
id="path70" />
<path
class="arrow"
d="M1340 645 C1340 660, 1340 660, 1340 665"
id="path71" />
<!-- Foot -->
<text
x="44"
y="916"
class="sub"
id="text71">Astuce : si Inkscape affichait “noir”, c’était très souvent des CSS variables. Ici : couleurs explicites + fond explicite.</text>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,510 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg53"
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview53"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="655.97579"
inkscape:cy="478.66868"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg53" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Machine éditoriale (v2, verbatim)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">Détails scripts/fichiers — 2026-02-20</text>
<rect
x="40"
y="140"
width="460"
height="236.05144"
class="box"
id="rect5" />
<text
x="58"
y="170"
class="h"
id="text6">Sources (repo)</text>
<text
x="58"
y="194"
class="t"
id="text7">Contenu : src/content/** (MD/MDX)</text>
<text
x="58"
y="212"
class="t"
id="text8">Annotations : src/annotations/** (YAML)</text>
<text
x="58"
y="230"
class="t"
id="text9">UI : src/layouts + src/components + global.css</text>
<text
x="58"
y="248"
class="s"
id="text10">Plugin paragraph-ids ajoute des ids stables sur paragraphes</text>
<rect
x="166"
y="296"
width="190"
height="28"
class="chip"
id="rect10" />
<text
x="180"
y="315"
class="mono"
id="text11"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">rehype-paragraph-ids.js</text>
<rect
x="560"
y="140"
width="500"
height="239.42511"
class="box"
id="rect11" />
<text
x="578"
y="170"
class="h"
id="text12">Build (Astro static)</text>
<text
x="578"
y="194"
class="t"
id="text13">astro build → dist/**/index.html</text>
<text
x="578"
y="212"
class="t"
id="text14">meta Pagefind: edition/level/status/version</text>
<text
x="578"
y="230"
class="t"
id="text15">Layout : EditionLayout + SiteLayout</text>
<text
x="578"
y="248"
class="s"
id="text16">data-pagefind-body = zone indexée</text>
<rect
x="750"
y="300"
width="128"
height="28"
class="chip2"
id="rect16" />
<text
x="764"
y="319"
class="mono"
id="text17"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm run build</text>
<rect
x="560"
y="430"
width="500"
height="280"
class="box2"
id="rect17" />
<text
x="578"
y="460"
class="h"
id="text18">Postbuild (qualité + recherche + indexes)</text>
<text
x="578"
y="484"
class="t"
id="text19">Aliases d'ancres (backward compat)</text>
<text
x="578"
y="502"
class="t"
id="text20">Dédoublonnage d'IDs (anti-régression)</text>
<text
x="578"
y="520"
class="t"
id="text21">Index des paragraphes (para-index)</text>
<text
x="578"
y="538"
class="t"
id="text22">Index des annotations (annotations-index)</text>
<text
x="578"
y="556"
class="t"
id="text23">Pagefind (recherche full-text)</text>
<rect
x="690.71106"
y="578.59302"
width="246"
height="28"
class="chip"
id="rect23" />
<text
x="704.71106"
y="597.59302"
class="mono"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">inject-anchor-aliases.mjs</text>
<rect
x="705"
y="622"
width="214"
height="28"
class="chip"
id="rect24" />
<text
x="719"
y="641"
class="mono"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dedupe-ids-dist.mjs</text>
<rect
x="710.3858"
y="664.52344"
width="206"
height="28"
class="chip2"
id="rect25" />
<text
x="724.3858"
y="683.52344"
class="mono"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">build-para-index.mjs</text>
<rect
x="1265"
y="660"
width="254"
height="28"
class="chip2"
id="rect26" />
<text
x="1279"
y="679"
class="mono"
id="text27">build-annotations-index.mjs</text>
<rect
x="1120"
y="140"
width="440"
height="240"
class="box"
id="rect27" />
<text
x="1138"
y="170"
class="h"
id="text28">Artefacts (dist/)</text>
<text
x="1138"
y="194"
class="mono"
id="text29">HTML statique + assets</text>
<text
x="1138"
y="212"
class="mono"
id="text30">dist/pagefind/**</text>
<text
x="1138"
y="230"
class="mono"
id="text31">dist/para-index.json</text>
<text
x="1138"
y="248"
class="mono"
id="text32">dist/annotations-index.json</text>
<text
x="1138"
y="266"
class="s"
id="text33">(en dev) recopiés dans public/*-index.json</text>
<rect
x="1120"
y="430"
width="441.2103"
height="280.95309"
class="box2"
id="rect33" />
<text
x="1138"
y="460"
class="h"
id="text34">Runtime navigateur (lecture)</text>
<text
x="1138"
y="484"
class="t"
id="text35">LocalToc sync (H2/H3)</text>
<text
x="1138"
y="502"
class="t"
id="text36">banner-follow + reading-follow__inner</text>
<text
x="1138"
y="520"
class="t"
id="text37">SidePanel: niveaux + annotations + propose</text>
<text
x="1138"
y="538"
class="s"
id="text38">Comportement lecture: H2/H3 unifiés (plus d’accordéon gênant)</text>
<rect
x="1263.4493"
y="588.65356"
width="150"
height="28"
class="chip2"
id="rect38" />
<text
x="1277.4493"
y="607.65356"
class="mono"
id="text39"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">SidePanel.astro</text>
<rect
x="1260.8547"
y="633.4342"
width="160"
height="28"
class="chip"
id="rect39" />
<text
x="1274.8547"
y="652.4342"
class="mono"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">LevelToggle.astro</text>
<rect
x="41.210289"
y="431.52798"
width="462.42056"
height="275.41605"
class="box2"
id="rect40" />
<text
x="58"
y="470"
class="h"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Flux “Proposer” (tickets)</text>
<text
x="58"
y="494"
class="t"
id="text42"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI collecte: page + paragraphe + type + message</text>
<text
x="58"
y="512"
class="t"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Création d'issue Gitea (labels)</text>
<text
x="58"
y="530"
class="t"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Lien retour: issue → page + id</text>
<text
x="58"
y="548"
class="s"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Option: bridge same-origin pour éviter CORS/auth</text>
<rect
x="60"
y="610"
width="150"
height="28"
class="chip"
id="rect45" />
<text
x="74"
y="629"
class="mono"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PUBLIC_GITEA_*</text>
<rect
x="230"
y="610"
width="262"
height="28"
class="chip2"
id="rect46" />
<text
x="244"
y="629"
class="mono"
id="text47"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PUBLIC_ISSUE_BRIDGE_PATH</text>
<path
d="M500 250 C530 250 530 250 560 250"
class="arrowA"
id="path47" />
<path
d="M810 400 C810 420 810 420 810 430"
class="arrowA"
id="path48" />
<path
d="M1060 250 C1090 250 1090 250 1120 250"
class="arrowA"
id="path49" />
<path
d="M1340 380 C1340 410 1340 410 1340 430"
class="arrow"
id="path50" />
<path
d="M500 540 C620 540 620 520 560 520"
class="arrow"
id="path51" />
<text
x="520"
y="525"
class="s"
id="text51">issues</text>
<rect
x="40"
y="820"
width="1520"
height="60"
class="box2"
id="rect51" />
<text
x="58"
y="850"
class="h"
id="text52">Conseil de maintenance</text>
<text
x="58"
y="874"
class="s"
id="text53">Toute évolution UI/indices doit rester déterministe : build identique sur Mac, CI, et NAS. En cas de hotfix, re-sync via PR.</text>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,613 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1500"
height="1050"
viewBox="0 0 1500 1050"
role="img"
aria-label="Archicratie — Machine éditoriale (verbatim) : scripts, indices DEV/PROD, postbuild exact, proposer direct vs bridge"
version="1.1"
id="svg77"
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim-v3.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview77"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.83714286"
inkscape:cx="756.14334"
inkscape:cy="367.32082"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg77" />
<defs
id="defs2">
<style
id="style1">
/* Inkscape-safe: pas de CSS variables */
.title{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:26px; font-weight:900; fill:#0f172a;}
.sub{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:650; fill:#475569;}
.laneT{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:900; fill:#0f172a;}
.laneN{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:650; fill:#475569;}
.h{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:900; fill:#0f172a;}
.p{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:650; fill:#475569;}
.small{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:11px; font-weight:650; fill:#475569;}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Liberation Mono&quot;,&quot;Courier New&quot;,monospace; font-size:11px; font-weight:800; fill:#334155;}
.canvas{fill:#f8fafc; stroke:#e2e8f0; stroke-width:1;}
.lane{fill:#f1f5f9; stroke:#cbd5e1; stroke-width:1;}
.laneAlt{fill:#eef2f7; stroke:#cbd5e1; stroke-width:1;}
.box{fill:#ffffff; stroke:#94a3b8; stroke-width:1.4;}
.boxAlt{fill:#f8fafc; stroke:#94a3b8; stroke-width:1.4;}
.call{fill:#ffffff; fill-opacity:.72; stroke:#cbd5e1; stroke-width:1.1;}
.arrow{stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#ah);}
.arrowSoft{stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#ahSoft);}
.dash{stroke-dasharray:7 6;}
</style>
<marker
id="ah"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#64748b"
id="path1" />
</marker>
<marker
id="ahSoft"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#94a3b8"
id="path2" />
</marker>
</defs>
<rect
x="0.5"
y="0.5"
width="1499"
height="1049"
rx="26"
class="canvas"
id="rect2" />
<text
x="44"
y="54"
class="title"
id="text2">Archicratie — Machine éditoriale (verbatim technique)</text>
<text
x="44"
y="82"
class="sub"
id="text3">3 ajouts “pro” inclus : (1) indices DEV vs PROD, (2) fork Proposer direct vs bridge, (3) ordre postbuild exact.</text>
<!-- Lanes -->
<rect
x="40"
y="115"
width="420"
height="880"
rx="18"
class="lane"
id="rect3" />
<rect
x="480"
y="115"
width="520"
height="880"
rx="18"
class="laneAlt"
id="rect4" />
<rect
x="1020"
y="115"
width="440"
height="880"
rx="18"
class="lane"
id="rect5" />
<text
x="60"
y="148"
class="laneT"
id="text5">A) Entrées &amp; canon</text>
<text
x="60"
y="170"
class="laneN"
id="text6">Ce qui est versionné et alimente la build</text>
<text
x="500"
y="148"
class="laneT"
id="text7">B) Build + postbuild</text>
<text
x="500"
y="170"
class="laneN"
id="text8">Astro (static) + scripts (ordre fixe)</text>
<text
x="1040"
y="148"
class="laneT"
id="text9">C) Runtime (DEV/PROD) + “Proposer”</text>
<text
x="1040"
y="170"
class="laneN"
id="text10">Indices servis, UI, et création d’issues</text>
<!-- A) Entrées -->
<rect
x="70"
y="210"
width="360"
height="165"
rx="16"
class="box"
id="rect10" />
<text
x="92"
y="238"
class="h"
id="text11">Contenu canon (pages)</text>
<text
x="92"
y="260"
class="p"
id="text12">Astro Content : MD / MDX (pages, chapitres, etc.)</text>
<text
x="92"
y="284"
class="mono"
id="text13">src/content/**</text>
<text
x="92"
y="310"
class="small"
id="text14">Layouts/TOC/reading-follow consomment ces pages.</text>
<text
x="92"
y="334"
class="small"
id="text15">Les IDs de paragraphes doivent rester stables (anti-régression).</text>
<rect
x="70"
y="395"
width="360"
height="190"
rx="16"
class="boxAlt"
id="rect15" />
<text
x="92"
y="423"
class="h"
id="text16">Annotations (surcouche)</text>
<text
x="92"
y="445"
class="p"
id="text17">YAML : notes, refs, illus, commentaires par paragraphe.</text>
<text
x="92"
y="468"
class="mono"
id="text18">src/annotations/**</text>
<text
x="92"
y="494"
class="p"
id="text19">Indexé en JSON pour le SidePanel :</text>
<text
x="92"
y="516"
class="mono"
id="text20">dist/annotations-index.json (PROD)</text>
<text
x="92"
y="538"
class="mono"
id="text21">public/annotations-index.json (DEV)</text>
<rect
x="70"
y="605"
width="360"
height="170"
rx="16"
class="box"
id="rect21" />
<text
x="92"
y="633"
class="h"
id="text22">Scripts (import &amp; qualité)</text>
<text
x="92"
y="655"
class="p"
id="text23">Import DOCX, checks d’IDs, aliases d’ancres, etc.</text>
<text
x="92"
y="678"
class="mono"
id="text24">scripts/import-docx.mjs</text>
<text
x="92"
y="700"
class="mono"
id="text25">scripts/check-anchors.mjs</text>
<text
x="92"
y="724"
class="small"
id="text26">Objectif : build reproductible + compat backward (ancres).</text>
<!-- B) Build + postbuild -->
<rect
x="510"
y="210"
width="460"
height="190"
rx="16"
class="box"
id="rect26" />
<text
x="532"
y="238"
class="h"
id="text27">Build Astro (static)</text>
<text
x="532"
y="260"
class="p"
id="text28">Génère le HTML + assets + routes (output: static).</text>
<text
x="532"
y="284"
class="mono"
id="text29">npm run build</text>
<text
x="532"
y="306"
class="mono"
id="text30">→ astro build → dist/**</text>
<text
x="532"
y="330"
class="p"
id="text31">UI d’édition (côté pages) :</text>
<text
x="532"
y="352"
class="mono"
id="text32">src/layouts/EditionLayout.astro</text>
<text
x="532"
y="374"
class="mono"
id="text33">src/components/SidePanel.astro</text>
<rect
x="510"
y="420"
width="460"
height="330"
rx="16"
class="boxAlt"
id="rect33" />
<text
x="532"
y="448"
class="h"
id="text34">Postbuild (ordre exact = contrat)</text>
<text
x="532"
y="472"
class="p"
id="text35">À conserver tel quel pour éviter les régressions.</text>
<text
x="532"
y="500"
class="mono"
id="text36">1) node scripts/inject-anchor-aliases.mjs</text>
<text
x="532"
y="522"
class="mono"
id="text37">2) node scripts/dedupe-ids-dist.mjs</text>
<text
x="532"
y="544"
class="mono"
id="text38"><tspan
sodipodi:role="line"
id="tspan79"
x="532"
y="544">3) node scripts/build-para-index.mjs</tspan><tspan
sodipodi:role="line"
id="tspan80"
x="532"
y="557.75">--in dist --out dist/para-index.json</tspan></text>
<text
x="532"
y="578"
class="mono"
id="text39"><tspan
sodipodi:role="line"
id="tspan81"
x="532"
y="578">4) node scripts/build-annotations-index.mjs</tspan><tspan
sodipodi:role="line"
id="tspan82"
x="532"
y="591.75">--in src/annotations --out dist/annotations-index.json</tspan></text>
<text
x="532"
y="612"
class="mono"
id="text40">5) npx pagefind --site dist (→ dist/pagefind/**)</text>
<text
x="532"
y="638"
class="p"
id="text41">Sorties PROD (statique) :</text>
<text
x="532"
y="660"
class="mono"
id="text42">dist/para-index.json</text>
<text
x="532"
y="682"
class="mono"
id="text43">dist/annotations-index.json</text>
<text
x="532"
y="704"
class="mono"
id="text44">dist/pagefind/**</text>
<text
x="532"
y="736"
class="small"
id="text45">Mini-règle : inject (aliases) AVANT dedupe, et indices AVANT pagefind.</text>
<rect
x="510"
y="770"
width="460"
height="195"
rx="16"
class="box"
id="rect45" />
<text
x="532"
y="798"
class="h"
id="text46">DEV server : indices “public/” (mini-ajout #1)</text>
<text
x="532"
y="822"
class="p"
id="text47">En DEV, l’UI lit via HTTP depuis <tspan
class="mono"
id="tspan46">public/</tspan> (pas <tspan
class="mono"
id="tspan47">dist/</tspan>).</text>
<text
x="532"
y="846"
class="mono"
id="text48">predev: build-annotations-index → public/annotations-index.json</text>
<text
x="532"
y="868"
class="mono"
id="text49">predev: build-para-index (depuis dist) → public/para-index.json</text>
<text
x="532"
y="892"
class="small"
id="text50">Si ces fichiers manquent : 404 (normal) → relancer <tspan
class="mono"
id="tspan49">npm run dev</tspan> (predev).</text>
<!-- C) Runtime + proposer -->
<rect
x="1050"
y="210"
width="380"
height="200"
rx="16"
class="box"
id="rect50" />
<text
x="1072"
y="238"
class="h"
id="text51">Runtime PROD</text>
<text
x="1072"
y="262"
class="p"
id="text52">Site statique servi depuis dist/**</text>
<text
x="1072"
y="286"
class="mono"
id="text53">dist/*.html</text>
<text
x="1072"
y="308"
class="mono"
id="text54">dist/para-index.json</text>
<text
x="1072"
y="330"
class="mono"
id="text55">dist/annotations-index.json</text>
<text
x="1072"
y="352"
class="mono"
id="text56">dist/pagefind/**</text>
<text
x="1072"
y="378"
class="small"
id="text57">Ces artefacts sont reproductibles via CI.</text>
<rect
x="1050"
y="430"
width="380"
height="250"
rx="16"
class="boxAlt"
id="rect57" />
<text
x="1072"
y="458"
class="h"
id="text58">“Proposer” (mini-ajout #2)</text>
<text
x="1072"
y="482"
class="p"
id="text59">Depuis SidePanel / ProposeModal (UI).</text>
<text
x="1072"
y="512"
class="p"
id="text60">Mode A — Direct navigateur → Gitea API</text>
<text
x="1072"
y="534"
class="small"
id="text61">⚠️ CORS + auth + token : fragile / déconseillé.</text>
<text
x="1072"
y="566"
class="p"
id="text62">Mode B — Bridge same-origin (recommandé)</text>
<text
x="1072"
y="588"
class="mono"
id="text63">PUBLIC_ISSUE_BRIDGE_PATH (ex: /bridge/issues)</text>
<text
x="1072"
y="612"
class="small"
id="text64">Le serveur/proxy ajoute l’auth (secrets) et appelle Gitea côté backend.</text>
<text
x="1072"
y="638"
class="small"
id="text65">Résultat : pas de secret exposé au client + moins de soucis CORS.</text>
<rect
x="1050"
y="700"
width="380"
height="165"
rx="16"
class="box"
id="rect65" />
<text
x="1072"
y="728"
class="h"
id="text66">Gitea : Issues</text>
<text
x="1072"
y="752"
class="p"
id="text67">Création de tickets (PR/CI séparés du flux éditorial)</text>
<text
x="1072"
y="776"
class="mono"
id="text68">/issues/new</text>
<text
x="1072"
y="798"
class="mono"
id="text69">labels / assignee / templates</text>
<text
x="1072"
y="828"
class="small"
id="text70">Le workflow Git/PR/CI reste la source canon (pas la prod).</text>
<!-- Callout -->
<rect
x="1050"
y="885"
width="380"
height="95.972694"
rx="14"
class="call"
id="rect70" />
<text
x="1072"
y="912"
class="h"
id="text71">Mini-ajout #3 — ordre postbuild</text>
<text
x="1072"
y="936"
class="p"
id="text72"><tspan
sodipodi:role="line"
id="tspan77"
x="1072"
y="936">inject-anchor-aliases → dedupe-ids → para-index</tspan><tspan
sodipodi:role="line"
id="tspan78"
x="1072"
y="951.47437">→ annotations-index → pagefind</tspan></text>
<text
x="1072"
y="968"
class="small"
id="text73">C’est le “contrat” anti-régression : si tu changes l’ordre, tu re-tests tout.</text>
<!-- Arrows (liaisons principales) -->
<path
class="arrow"
d="M430 300 C455 300, 475 300, 510 300"
id="path73" />
<path
class="arrow"
d="M430 500 C455 500, 480 490, 510 520"
id="path74" />
<path
class="arrow"
d="M970 330 C1000 330, 1010 330, 1050 330"
id="path75" />
<path
class="arrowSoft dash"
d="M 967.84983,823.85666 C 1006.4505,737.84983 1012.5597,719.6587 1050,610"
id="path76"
sodipodi:nodetypes="cc" />
<path
class="arrow"
d="M1240 680 C1240 695, 1240 695, 1240 700"
id="path77" />
<text
x="44"
y="1028"
class="sub"
id="text77">Inkscape-safe : couleurs &amp; fond explicites (zéro var()).</text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,634 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="1020"
viewBox="0 0 1600 1020"
version="1.1"
id="svg77"
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-machine-editoriale-verbatim.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview77"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="524.05446"
inkscape:cy="684.41755"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="235"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg77" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.h3 { font: 700 14px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<!-- Header -->
<text
x="40"
y="45"
class="title"
id="text1">Archicratie – Web Edition : Machine éditoriale VERBATIM (Proposer / Citer / /_auth/whoami / aliases / apply-ticket)</text>
<text
x="40"
y="75"
class="small"
id="text2">Sources “vraies” : src/layouts/EditionLayout.astro (WHOAMI_PATH=&quot;/_auth/whoami&quot;, GITEA_* via import.meta.env.PUBLIC_*), src/anchors/anchor-aliases.json, scripts/inject-anchor-aliases.mjs, scripts/apply-ticket.mjs --alias.</text>
<!-- ZONE A: Runtime -->
<rect
x="35"
y="110"
width="1530"
height="420"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">A — Runtime (navigateur) : paragraphe → outils (¶ / Citer / Proposer) → issue Gitea</text>
<!-- Reader -->
<rect
x="60"
y="175"
width="380"
height="160"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="205"
class="h2"
id="text4">Utilisateur (lecteur/éditeur)</text>
<text
x="80"
y="230"
class="txt"
id="text5">• lit une page</text>
<text
x="80"
y="252"
class="txt"
id="text6"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• sur un paragraphe : Citer / Proposer / Marque-page</text>
<text
x="80"
y="274"
class="txt"
id="text7">• Proposer visible uniquement si “editors”</text>
<text
x="80"
y="300"
class="small"
id="text8">(le gate est runtime via whoami)</text>
<!-- Astro page + EditionLayout -->
<rect
x="470"
y="175"
width="560"
height="330"
rx="12"
class="box"
id="rect8" />
<text
x="490"
y="205"
class="h2"
id="text9">Site Astro statique — EditionLayout</text>
<text
x="490"
y="230"
class="mono"
id="text10">src/layouts/EditionLayout.astro</text>
<text
x="490"
y="260"
class="h3"
id="text11">Variables publiques injectées</text>
<text
x="490"
y="282"
class="txt"
id="text13">• <tspan
class="mono"
id="tspan11">PUBLIC_GITEA_BASE</tspan>, <tspan
class="mono"
id="tspan12">PUBLIC_GITEA_OWNER</tspan>, <tspan
class="mono"
id="tspan13">PUBLIC_GITEA_REPO</tspan></text>
<text
x="490"
y="304"
class="txt"
id="text14">• si une manque : giteaReady=false → Proposer désactivé</text>
<text
x="490"
y="338"
class="h3"
id="text15">Outils paragraphe</text>
<text
x="490"
y="362"
class="txt"
id="text17"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Citer : copie une citation structurée (titre + URL#ancre)</text>
<text
x="490"
y="384"
class="txt"
id="text18"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Proposer : modal 2 étapes → ouvre <tspan
class="mono"
id="tspan17"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/issues/new?...</tspan></text>
<text
x="490"
y="418"
class="h3"
id="text19"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:14px;line-height:normal;font-family:sans-serif">Gate “editors” (whoami)</text>
<text
x="490"
y="440"
class="txt"
id="text20"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• <tspan
class="mono"
id="tspan19"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">WHOAMI_PATH=&quot;/_auth/whoami&quot;</tspan> + fetch same-origin</text>
<text
x="490"
y="462"
class="txt"
id="text21"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• lit header <tspan
class="mono"
id="tspan20"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">Remote-Groups</tspan> → affiche/retire Proposer du DOM</text>
<!-- Edge/Auth -->
<rect
x="1060"
y="175"
width="250"
height="160"
rx="12"
class="box"
id="rect21" />
<text
x="1080"
y="205"
class="h2"
id="text22">Traefik edge</text>
<text
x="1080"
y="230"
class="txt"
id="text23">• Host(archicratie…)</text>
<text
x="1080"
y="252"
class="txt"
id="text24">• middleware : chain-auth</text>
<text
x="1080"
y="274"
class="txt"
id="text25">• route <tspan
class="mono"
id="tspan24">/_auth/whoami</tspan></text>
<text
x="1080"
y="300"
class="small"
id="text26">forward-auth vers Authelia</text>
<rect
x="1060"
y="350"
width="250"
height="155"
rx="12"
class="box"
id="rect26" />
<text
x="1080"
y="380"
class="h2"
id="text27">Authelia + LLDAP</text>
<text
x="1080"
y="405"
class="txt"
id="text28">• forward-auth : <tspan
class="mono"
id="tspan27">:9091</tspan></text>
<text
x="1080"
y="427"
class="txt"
id="text29">• groupes via LDAP</text>
<text
x="1080"
y="449"
class="txt"
id="text30">• injecte headers Remote-*</text>
<text
x="1080"
y="471"
class="small"
id="text31">non auth ⇒ 302 vers auth.*</text>
<!-- Gitea issue -->
<rect
x="1330"
y="175"
width="235"
height="330"
rx="12"
class="box"
id="rect31" />
<text
x="1350"
y="205"
class="h2"
id="text32">Gitea (UI)</text>
<text
x="1350"
y="230"
class="txt"
id="text33">Issue préremplie :</text>
<text
x="1350"
y="255"
class="mono"
id="text34">BASE/OWNER/REPO/issues/new</text>
<text
x="1350"
y="287"
class="txt"
id="text35">Contenu typique :</text>
<text
x="1350"
y="309"
class="txt"
id="text36">• URL page + <tspan
class="mono"
id="tspan35">#p-…</tspan></text>
<text
x="1350"
y="331"
class="txt"
id="text37">• Type / State / Category</text>
<text
x="1350"
y="353"
class="txt"
id="text38">• proposition / commentaire</text>
<text
x="1350"
y="385"
class="txt"
id="text39">Résultat :</text>
<text
x="1350"
y="407"
class="txt"
id="text40">• issue = backlog éditorial</text>
<text
x="1350"
y="429"
class="txt"
id="text41">• labels (CI/bot) pour tri</text>
<!-- Runtime connections -->
<path
d="M 440 260 L 470 260"
class="line"
id="path41" />
<path
d="M 1030 470 L 1060 470"
class="dash"
id="path42" />
<path
d="M 1310 320 L 1330 320"
class="line"
id="path43" />
<path
d="M 1030 270 L 1060 270"
class="dash"
id="path44" />
<rect
x="60"
y="355"
width="380"
height="150"
rx="12"
class="note"
id="rect44" />
<text
x="80"
y="385"
class="h2"
id="text44">Contrat runtime (robuste)</text>
<text
x="80"
y="410"
class="txt"
id="text45">• Citer marche sans droits (copie + lien)</text>
<text
x="80"
y="432"
class="txt"
id="text46">• Proposer n’existe pas si non “editors”</text>
<text
x="80"
y="454"
class="txt"
id="text47">• whoami renvoie 302 login si non auth</text>
<text
x="80"
y="476"
class="txt"
id="text48">• si PUBLIC_GITEA_* faux → 404/login loop</text>
<!-- ZONE B: CI / labels -->
<rect
x="35"
y="545"
width="1530"
height="210"
rx="18"
class="zone"
id="rect48" />
<text
x="60"
y="580"
class="h2"
id="text49">B — Automatisation : issue → labels + checks qualité</text>
<rect
x="60"
y="610"
width="520"
height="120"
rx="12"
class="box"
id="rect49" />
<text
x="80"
y="640"
class="h2"
id="text50">Gitea Actions (workflows)</text>
<text
x="80"
y="665"
class="txt"
id="text51">• triggers : issues opened / edited (labels)</text>
<text
x="80"
y="687"
class="txt"
id="text52">• checks build : anchors / aliases / inline-js / dist audit</text>
<text
x="80"
y="709"
class="small"
id="text53">token API requis côté job (ex : FORGE_TOKEN) pour écrire labels</text>
<rect
x="610"
y="610"
width="430"
height="120"
rx="12"
class="box"
id="rect53" />
<text
x="630"
y="640"
class="h2"
id="text54">Runner</text>
<text
x="630"
y="665"
class="txt"
id="text55">• conteneur : <tspan
class="mono"
id="tspan54">gitea-act-runner (gitea/act_runner)</tspan></text>
<text
x="630"
y="687"
class="txt"
id="text56">• exécute jobs (souvent en conteneur)</text>
<text
x="630"
y="709"
class="txt"
id="text57">• appelle API Gitea pour labels</text>
<rect
x="1070"
y="610"
width="465"
height="120"
rx="12"
class="box"
id="rect57" />
<text
x="1090"
y="640"
class="h2"
id="text58">Gitea (API) + labels</text>
<text
x="1090"
y="665"
class="txt"
id="text59">• labels = tri natif (type/state/cat)</text>
<text
x="1090"
y="687"
class="txt"
id="text60">• backlog propre, opérable</text>
<text
x="1090"
y="709"
class="small"
id="text61">si 401 : token manquant/mauvais droits</text>
<path
d="M 580 670 L 610 670"
class="line"
id="path61" />
<path
d="M 1040 670 L 1070 670"
class="line"
id="path62" />
<!-- ZONE C: Re-integration + anchors -->
<rect
x="35"
y="780"
width="1530"
height="210"
rx="18"
class="zone"
id="rect62" />
<text
x="400"
y="815"
class="h2"
id="text62"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">C — Réintégration : correction → contenu web + stabilité des ancres (aliases build-time)</text>
<rect
x="60"
y="850"
width="520"
height="115"
rx="12"
class="box"
id="rect63" />
<text
x="80"
y="880"
class="h2"
id="text63">Apply-ticket</text>
<text
x="80"
y="905"
class="mono"
id="text64">scripts/apply-ticket.mjs &lt;issue_number&gt; --alias</text>
<text
x="80"
y="929"
class="txt"
id="text65">• applique patch dans src/content/…</text>
<text
x="80"
y="951"
class="txt"
id="text66">• écrit alias old→new dans <tspan
class="mono"
id="tspan65">src/anchors/anchor-aliases.json</tspan></text>
<rect
x="610.74738"
y="848.78973"
width="591.40692"
height="117.42059"
rx="12"
class="box"
id="rect66" />
<text
x="630"
y="880"
class="h2"
id="text67">Aliases canon + injection</text>
<text
x="630"
y="905"
class="mono"
id="text68">src/anchors/anchor-aliases.json</text>
<text
x="630"
y="929"
class="txt"
id="text69">postbuild : <tspan
class="mono"
id="tspan68">node scripts/inject-anchor-aliases.mjs</tspan></text>
<text
x="630"
y="951"
class="txt"
id="text70">• injecte <tspan
class="mono"
id="tspan69">&lt;span id=&quot;oldId&quot; class=&quot;para-alias&quot;&gt;</tspan> avant newId dans dist/**/index.html</text>
<rect
x="1227.5703"
y="850"
width="331.42966"
height="115"
rx="12"
class="box"
id="rect70" />
<text
x="1240"
y="880"
class="h2"
id="text71"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Preuves (tests)</text>
<text
x="1240"
y="905"
class="txt"
id="text72"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• <tspan
class="mono"
id="tspan71"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/check-anchor-aliases.mjs</tspan></text>
<text
x="1240"
y="929"
class="txt"
id="text73"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• <tspan
class="mono"
id="tspan72"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/verify-anchor-aliases-in-dist.mjs</tspan></text>
<text
x="1240"
y="951"
class="txt"
id="text74"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• <tspan
class="mono"
id="tspan73"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/check-anchors.mjs</tspan></text>
<path
d="M 1495 545 L 1495 505"
class="dash"
id="path74" />
<path
d="M 320 730 L 320 850"
class="line"
id="path75" />
<path
d="M 580 908 L 610 908"
class="line"
id="path76" />
<path
d="M 1202.0514,908 H 1226"
class="line"
id="path77"
sodipodi:nodetypes="cc" />
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,631 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="1020"
viewBox="0 0 1600 1020"
version="1.1"
id="svg77"
sodipodi:docname="archicratie-web-edition-machine-editoriale.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-machine-editoriale.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview77"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="409.07716"
inkscape:cy="573.0711"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg77" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.h3 { font: 700 14px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<!-- Header -->
<text
x="40"
y="45"
class="title"
id="text1">Archicratie – Web Edition : Machine éditoriale VERBATIM (Proposer / Citer / /_auth/whoami / aliases / apply-ticket)</text>
<text
x="40"
y="75"
class="small"
id="text2">Sources “vraies” : src/layouts/EditionLayout.astro (WHOAMI_PATH=&quot;/_auth/whoami&quot;, GITEA_* via import.meta.env.PUBLIC_*), src/anchors/anchor-aliases.json, scripts/inject-anchor-aliases.mjs, scripts/apply-ticket.mjs --alias.</text>
<!-- ZONE A: Runtime -->
<rect
x="35"
y="110"
width="1530"
height="420"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">A — Runtime (navigateur) : paragraphe → outils (¶ / Citer / Proposer) → issue Gitea</text>
<!-- Reader -->
<rect
x="60"
y="175"
width="380"
height="160"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="205"
class="h2"
id="text4">Utilisateur (lecteur/éditeur)</text>
<text
x="80"
y="230"
class="txt"
id="text5">• lit une page</text>
<text
x="80"
y="252"
class="txt"
id="text6">• sur un paragraphe : ¶ / Citer / Proposer</text>
<text
x="80"
y="274"
class="txt"
id="text7">• Proposer visible uniquement si “editors”</text>
<text
x="80"
y="300"
class="small"
id="text8">(le gate est runtime via whoami)</text>
<!-- Astro page + EditionLayout -->
<rect
x="470"
y="175"
width="560"
height="330"
rx="12"
class="box"
id="rect8" />
<text
x="490"
y="205"
class="h2"
id="text9">Site Astro statique — EditionLayout</text>
<text
x="490"
y="230"
class="mono"
id="text10">src/layouts/EditionLayout.astro</text>
<text
x="490"
y="260"
class="h3"
id="text11">Variables publiques injectées</text>
<text
x="490"
y="282"
class="txt"
id="text13">• <tspan
class="mono"
id="tspan11">PUBLIC_GITEA_BASE</tspan>, <tspan
class="mono"
id="tspan12">PUBLIC_GITEA_OWNER</tspan>, <tspan
class="mono"
id="tspan13">PUBLIC_GITEA_REPO</tspan></text>
<text
x="490"
y="304"
class="txt"
id="text14">• si une manque : giteaReady=false → Proposer désactivé</text>
<text
x="490"
y="338"
class="h3"
id="text15">Outils paragraphe</text>
<text
x="490"
y="360"
class="txt"
id="text16">• ¶ : lien d’ancre vers <tspan
class="mono"
id="tspan15">#p-…</tspan></text>
<text
x="490"
y="382"
class="txt"
id="text17">• Citer : copie une citation structurée (titre + URL#ancre)</text>
<text
x="490"
y="404"
class="txt"
id="text18">• Proposer : modal 2 étapes → ouvre <tspan
class="mono"
id="tspan17">/issues/new?...</tspan></text>
<text
x="490"
y="438"
class="h3"
id="text19">Gate “editors” (whoami)</text>
<text
x="490"
y="460"
class="txt"
id="text20">• <tspan
class="mono"
id="tspan19">WHOAMI_PATH=&quot;/_auth/whoami&quot;</tspan> + fetch same-origin</text>
<text
x="490"
y="482"
class="txt"
id="text21">• lit header <tspan
class="mono"
id="tspan20">Remote-Groups</tspan> → affiche/retire Proposer du DOM</text>
<!-- Edge/Auth -->
<rect
x="1060"
y="175"
width="250"
height="160"
rx="12"
class="box"
id="rect21" />
<text
x="1080"
y="205"
class="h2"
id="text22">Traefik edge</text>
<text
x="1080"
y="230"
class="txt"
id="text23">• Host(archicratie…)</text>
<text
x="1080"
y="252"
class="txt"
id="text24">• middleware : chain-auth</text>
<text
x="1080"
y="274"
class="txt"
id="text25">• route <tspan
class="mono"
id="tspan24">/_auth/whoami</tspan></text>
<text
x="1080"
y="300"
class="small"
id="text26">forward-auth vers Authelia</text>
<rect
x="1060"
y="350"
width="250"
height="155"
rx="12"
class="box"
id="rect26" />
<text
x="1080"
y="380"
class="h2"
id="text27">Authelia + LLDAP</text>
<text
x="1080"
y="405"
class="txt"
id="text28">• forward-auth : <tspan
class="mono"
id="tspan27">:9091</tspan></text>
<text
x="1080"
y="427"
class="txt"
id="text29">• groupes via LDAP</text>
<text
x="1080"
y="449"
class="txt"
id="text30">• injecte headers Remote-*</text>
<text
x="1080"
y="471"
class="small"
id="text31">non auth ⇒ 302 vers auth.*</text>
<!-- Gitea issue -->
<rect
x="1330"
y="175"
width="220.47655"
height="328.7897"
rx="12"
class="box"
id="rect31" />
<text
x="1350"
y="205"
class="h2"
id="text32">Gitea (UI)</text>
<text
x="1350"
y="230"
class="txt"
id="text33">Issue préremplie :</text>
<text
x="1350"
y="255"
class="mono"
id="text34">BASE/OWNER/REPO/issues/new</text>
<text
x="1350"
y="287"
class="txt"
id="text35">Contenu typique :</text>
<text
x="1350"
y="309"
class="txt"
id="text36">• URL page + <tspan
class="mono"
id="tspan35">#p-…</tspan></text>
<text
x="1350"
y="331"
class="txt"
id="text37">• Type / State / Category</text>
<text
x="1350"
y="353"
class="txt"
id="text38">• proposition / commentaire</text>
<text
x="1350"
y="385"
class="txt"
id="text39">Résultat :</text>
<text
x="1350"
y="407"
class="txt"
id="text40">• issue = backlog éditorial</text>
<text
x="1350"
y="429"
class="txt"
id="text41">• labels (CI/bot) pour tri</text>
<!-- Runtime connections -->
<path
d="M 440 260 L 470 260"
class="line"
id="path41" />
<path
d="M 1030 470 L 1060 470"
class="dash"
id="path42" />
<path
d="M 1310 320 L 1330 320"
class="line"
id="path43" />
<path
d="M 1030 270 L 1060 270"
class="dash"
id="path44" />
<rect
x="60"
y="355"
width="380"
height="150"
rx="12"
class="note"
id="rect44" />
<text
x="80"
y="385"
class="h2"
id="text44">Contrat runtime (robuste)</text>
<text
x="80"
y="410"
class="txt"
id="text45">• Citer marche sans droits (copie + lien)</text>
<text
x="80"
y="432"
class="txt"
id="text46">• Proposer n’existe pas si non “editors”</text>
<text
x="80"
y="454"
class="txt"
id="text47">• whoami renvoie 302 login si non auth</text>
<text
x="80"
y="476"
class="txt"
id="text48">• si PUBLIC_GITEA_* faux → 404/login loop</text>
<!-- ZONE B: CI / labels -->
<rect
x="35"
y="545"
width="1530"
height="210"
rx="18"
class="zone"
id="rect48" />
<text
x="60"
y="580"
class="h2"
id="text49">B — Automatisation : issue → labels + checks qualité</text>
<rect
x="60"
y="610"
width="520"
height="120"
rx="12"
class="box"
id="rect49" />
<text
x="80"
y="640"
class="h2"
id="text50">Gitea Actions (workflows)</text>
<text
x="80"
y="665"
class="txt"
id="text51">• triggers : issues opened / edited (labels)</text>
<text
x="80"
y="687"
class="txt"
id="text52">• checks build : anchors / aliases / inline-js / dist audit</text>
<text
x="80"
y="709"
class="small"
id="text53">token API requis côté job (ex : FORGE_TOKEN) pour écrire labels</text>
<rect
x="610"
y="610"
width="430"
height="120"
rx="12"
class="box"
id="rect53" />
<text
x="630"
y="640"
class="h2"
id="text54">Runner</text>
<text
x="630"
y="665"
class="txt"
id="text55">• conteneur : <tspan
class="mono"
id="tspan54">gitea-act-runner (gitea/act_runner)</tspan></text>
<text
x="630"
y="687"
class="txt"
id="text56">• exécute jobs (souvent en conteneur)</text>
<text
x="630"
y="709"
class="txt"
id="text57">• appelle API Gitea pour labels</text>
<rect
x="1070"
y="610"
width="465"
height="120"
rx="12"
class="box"
id="rect57" />
<text
x="1090"
y="640"
class="h2"
id="text58">Gitea (API) + labels</text>
<text
x="1090"
y="665"
class="txt"
id="text59">• labels = tri natif (type/state/cat)</text>
<text
x="1090"
y="687"
class="txt"
id="text60">• backlog propre, opérable</text>
<text
x="1090"
y="709"
class="small"
id="text61">si 401 : token manquant/mauvais droits</text>
<path
d="M 580 670 L 610 670"
class="line"
id="path61" />
<path
d="M 1040 670 L 1070 670"
class="line"
id="path62" />
<!-- ZONE C: Re-integration + anchors -->
<rect
x="35"
y="780"
width="1530"
height="210"
rx="18"
class="zone"
id="rect62" />
<text
x="300"
y="815"
class="h2"
id="text62"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">C — Réintégration : correction → contenu web + stabilité des ancres (aliases build-time)</text>
<rect
x="60"
y="850"
width="520"
height="115"
rx="12"
class="box"
id="rect63" />
<text
x="80"
y="880"
class="h2"
id="text63">Apply-ticket</text>
<text
x="80"
y="905"
class="mono"
id="text64">scripts/apply-ticket.mjs &lt;issue_number&gt; --alias</text>
<text
x="80"
y="929"
class="txt"
id="text65">• applique patch dans src/content/…</text>
<text
x="80"
y="951"
class="txt"
id="text66">• écrit alias old→new dans <tspan
class="mono"
id="tspan65">src/anchors/anchor-aliases.json</tspan></text>
<rect
x="610"
y="850"
width="518.78973"
height="130.73373"
rx="12"
class="box"
id="rect66" />
<text
x="630"
y="880"
class="h2"
id="text67">Aliases canon + injection</text>
<text
x="630"
y="905"
class="mono"
id="text68">src/anchors/anchor-aliases.json</text>
<text
x="630"
y="929"
class="txt"
id="text69">postbuild : <tspan
class="mono"
id="tspan68">node scripts/inject-anchor-aliases.mjs</tspan></text>
<text
x="630"
y="951"
class="txt"
id="text70"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan77"
x="630"
y="951">• injecte</tspan><tspan
sodipodi:role="line"
id="tspan78"
x="630"
y="967.25">&lt;span id=&quot;oldId&quot; class=&quot;para-alias&quot;&gt; avant newId dans dist/**/index.html</tspan></text>
<rect
x="1160"
y="850"
width="375"
height="115"
rx="12"
class="box"
id="rect70" />
<text
x="1180"
y="880"
class="h2"
id="text71">Preuves (tests)</text>
<text
x="1180"
y="905"
class="txt"
id="text72">• <tspan
class="mono"
id="tspan71">scripts/check-anchor-aliases.mjs</tspan></text>
<text
x="1180"
y="929"
class="txt"
id="text73">• <tspan
class="mono"
id="tspan72">scripts/verify-anchor-aliases-in-dist.mjs</tspan></text>
<text
x="1180"
y="951"
class="txt"
id="text74">• <tspan
class="mono"
id="tspan73">scripts/check-anchors.mjs</tspan></text>
<path
d="M 1495 545 L 1495 505"
class="dash"
id="path74" />
<path
d="M 220,730 V 850"
class="line"
id="path75" />
<path
d="M 580 908 L 610 908"
class="line"
id="path76" />
<path
d="M 1130 908 L 1160 908"
class="line"
id="path77" />
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

+618
View File
@@ -0,0 +1,618 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="980"
viewBox="0 0 1600 980"
version="1.1"
id="svg66"
sodipodi:docname="diagram.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview66"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="1002.118"
inkscape:cy="617.85174"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg66" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<!-- Header -->
<text
x="40"
y="45"
class="title"
id="text1">Archicratie – Web Edition : schéma global (Local Mac Studio vs NAS Synology DS220+)</text>
<text
x="40"
y="75"
class="small"
id="text2">Lecture : (1) utilisateur → DSM → Traefik/Authelia → site ; (2) dev → package → slot green/blue → switch Traefik (provider file) ; (3) proposer → issues → runner → labels.</text>
<!-- LOCAL ZONE -->
<rect
x="35"
y="110"
width="520"
height="820"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">LOCAL — Mac Studio (atelier de dev)</text>
<rect
x="60"
y="175"
width="470"
height="95"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="205"
class="h2"
id="text4">Repo Astro (édition)</text>
<text
x="80"
y="230"
class="txt"
id="text5">• src/content/ (contenu web)</text>
<text
x="80"
y="250"
class="txt"
id="text6">• scripts tooling (anchors, import docx, apply-ticket)</text>
<rect
x="60"
y="295"
width="470"
height="80"
rx="12"
class="box"
id="rect6" />
<text
x="80"
y="325"
class="h2"
id="text7">Build statique</text>
<text
x="80"
y="350"
class="txt"
id="text8">npm run build → dist/ (site statique)</text>
<rect
x="60"
y="400"
width="470"
height="95"
rx="12"
class="box"
id="rect8" />
<text
x="80"
y="430"
class="h2"
id="text9">Release pack</text>
<text
x="80"
y="455"
class="txt"
id="text10">archive .tar.gz + .sha256</text>
<text
x="80"
y="475"
class="txt"
id="text11">destination NAS : /volume2/docker/archicratie-web/incoming/</text>
<rect
x="60"
y="520"
width="470"
height="120"
rx="12"
class="note"
id="rect11" />
<text
x="80"
y="550"
class="h2"
id="text12">Centre de vérité</text>
<text
x="80"
y="575"
class="txt"
id="text13">Tu commits/push vers Gitea (NAS) :</text>
<text
x="80"
y="598"
class="txt"
id="text14">• code + docs + diagrammes (SVG)</text>
<text
x="80"
y="621"
class="txt"
id="text15">• issues = backlog éditorial</text>
<!-- NAS ZONE -->
<rect
x="590"
y="110"
width="975"
height="820"
rx="18"
class="zone"
id="rect15" />
<text
x="615"
y="145"
class="h2"
id="text16">DISTANT — NAS Synology DS220+ (DSM + Container Manager)</text>
<!-- USERS -->
<rect
x="615"
y="175"
width="275"
height="90"
rx="12"
class="box"
id="rect16" />
<text
x="635"
y="205"
class="h2"
id="text17">Utilisateurs (web)</text>
<text
x="635"
y="230"
class="txt"
id="text18">• visiteurs</text>
<text
x="635"
y="250"
class="txt"
id="text19">• éditeurs (accès protégé)</text>
<!-- DSM Reverse Proxy -->
<rect
x="920"
y="175"
width="615"
height="90"
rx="12"
class="box"
id="rect19" />
<text
x="945"
y="205"
class="h2"
id="text20">DSM Reverse Proxy (HTTPS public → Traefik)</text>
<text
x="945"
y="230"
class="txt"
id="text21">• pointe vers 127.0.0.1:18080 (Traefik edge)</text>
<text
x="945"
y="252"
class="txt"
id="text22">• bascule/rollback = switch Traefik (reload) ; DSM reste stable</text>
<!-- Edge Traefik -->
<rect
x="920"
y="295"
width="615"
height="110"
rx="12"
class="box"
id="rect22" />
<text
x="945"
y="325"
class="h2"
id="text23">Traefik (edge) — écoute 127.0.0.1:18080</text>
<text
x="945"
y="350"
class="txt"
id="text24">• entrée unique derrière DSM</text>
<text
x="945"
y="372"
class="txt"
id="text25">• middlewares : sanitize-remote + forward-auth Authelia</text>
<text
x="945"
y="394"
class="txt"
id="text26">• un seul backend site actif (blue OU green) via 20-archicratie-backend.yml</text>
<!-- Auth Stack -->
<rect
x="615"
y="310"
width="275"
height="250"
rx="12"
class="box"
id="rect26" />
<text
x="635"
y="340"
class="h2"
id="text27">Auth stack</text>
<text
x="635"
y="365"
class="txt"
id="text28">Authelia</text>
<text
x="635"
y="387"
class="small"
id="text29">• forward-auth : /api/authz/forward-auth</text>
<text
x="635"
y="415"
class="txt"
id="text30">LLDAP</text>
<text
x="635"
y="438"
class="small"
id="text31">• annuaire LDAP “source of truth”</text>
<text
x="635"
y="466"
class="txt"
id="text32">Redis</text>
<text
x="635"
y="489"
class="small"
id="text33">• sessions / cache (selon config)</text>
<text
x="635"
y="525"
class="small"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan68"
x="635"
y="525">Objectif : SSO/MFA + anti lock-out</tspan><tspan
sodipodi:role="line"
id="tspan69"
x="635"
y="540">déploiement progressif)</tspan></text>
<!-- Web Blue/Green -->
<rect
x="920"
y="435"
width="300"
height="135"
rx="12"
class="box"
id="rect34" />
<text
x="945"
y="465"
class="h2"
id="text35">web_blue (slot A)</text>
<text
x="945"
y="490"
class="txt"
id="text36">127.0.0.1:8081 → container:80</text>
<text
x="945"
y="515"
class="txt"
id="text37">sert dist/ (Nginx/HTTP)</text>
<text
x="945"
y="540"
class="small"
id="text38">ne jamais modifier si LIVE</text>
<rect
x="1235"
y="435"
width="300"
height="135"
rx="12"
class="box"
id="rect38" />
<text
x="1260"
y="465"
class="h2"
id="text39">web_green (slot B)</text>
<text
x="1260"
y="490"
class="txt"
id="text40">127.0.0.1:8082 → container:80</text>
<text
x="1260"
y="515"
class="txt"
id="text41">sert dist/ (Nginx/HTTP)</text>
<text
x="1260"
y="540"
class="small"
id="text42">slot “next” (staging)</text>
<!-- Switch script -->
<rect
x="847.41852"
y="587.45636"
width="691.17664"
height="69.928497"
rx="13.486373"
class="note"
id="rect42" />
<text
x="932.38275"
y="617.42059"
class="txt"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan67"
x="932.38275"
y="617.42059">switch-archicratie.sh : bascule blue/green en réécrivant 20-archicratie-backend.yml</tspan><tspan
sodipodi:role="line"
x="932.38275"
y="633.67059"
id="tspan3">puis reload Traefik (provider file)</tspan></text>
<!-- Gitea -->
<rect
x="615"
y="690"
width="520"
height="160"
rx="12"
class="box"
id="rect43" />
<text
x="635"
y="720"
class="h2"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Gitea (forge web + API)</text>
<text
x="635"
y="745"
class="txt"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• repo = centre de vérité</text>
<text
x="635"
y="768"
class="txt"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• issues = backlog</text>
<text
x="635"
y="791"
class="txt"
id="text47"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• labels = tri natif (type/state/cat)</text>
<text
x="635"
y="814"
class="small"
id="text48"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">Note : parfois laissé sans forward-auth (runner/accès API) selon réglage</text>
<!-- Runner -->
<rect
x="1160"
y="690"
width="375"
height="170"
rx="12"
class="box"
id="rect48" />
<text
x="1185"
y="720"
class="h2"
id="text49">Gitea Actions Runner (act_runner)</text>
<text
x="1185"
y="745"
class="txt"
id="text50">• exécute les workflows</text>
<text
x="1185"
y="768"
class="txt"
id="text51">• doit monter /var/run/docker.sock</text>
<text
x="1185"
y="791"
class="txt"
id="text52">• jobs en conteneur (ex : python:3.12-slim)</text>
<text
x="1185"
y="814"
class="small"
id="text53"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan78"
x="1185"
y="814">• applique labels via API avec PAT (FORGE_TOKEN)</tspan><tspan
sodipodi:role="line"
id="tspan79"
x="1185"
y="829">— sinon 401</tspan></text>
<!-- Connections -->
<!-- User -> DSM -->
<path
d="M 890 220 L 920 220"
class="line"
id="path53" />
<!-- DSM -> Traefik -->
<path
d="M 1225 265 L 1225 295"
class="line"
id="path54" />
<!-- Traefik -> Web (one active) -->
<path
d="M 1100 405 L 1070 435"
class="dash"
id="path55" />
<path
d="M 1355 405 L 1385 435"
class="dash"
id="path56" />
<!-- Traefik -> Auth -->
<path
d="M 920 350 L 890 350"
class="line"
id="path57" />
<!-- DSM -> Gitea (via router / host rules) -->
<path
d="M 917.44325,255.3177 883.05597,688.03328"
class="dash"
id="path58"
sodipodi:nodetypes="cc" />
<!-- Gitea -> Runner -->
<path
d="M 1135 710 L 1160 750"
class="line"
id="path59" />
<!-- Runner -> Gitea API -->
<path
d="M 1160 805 L 1135 740"
class="dash"
id="path60" />
<!-- Local -> NAS incoming -->
<path
d="M 530 448 L 615 448"
class="line"
id="path61" />
<!-- Legend -->
<rect
x="60"
y="670"
width="470"
height="245"
rx="12"
class="note"
id="rect61" />
<text
x="80"
y="700"
class="h2"
id="text61">Légende / invariants (ce qui casse “pour de vrai”)</text>
<text
x="80"
y="725"
class="txt"
id="text62"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan70"
x="80"
y="725">• Prod safe : on ne touche jamais au slot LIVE ;</tspan><tspan
sodipodi:role="line"
id="tspan71"
x="80"
y="741.25">build/test sur l’autre ; switch Traefik ; DSM ne change pas.</tspan></text>
<text
x="80"
y="768"
class="txt"
id="text63"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan76"
x="80"
y="768">• Runner : sans docker.sock → aucun job ;</tspan><tspan
sodipodi:role="line"
id="tspan77"
x="80"
y="784.40051">sans FORGE_TOKEN → 401 (labels).</tspan></text>
<text
x="80"
y="811"
class="txt"
id="text64"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan74"
x="80"
y="811">• Edge : Traefik :18080 derrière DSM ; sanitize-remote</tspan><tspan
sodipodi:role="line"
id="tspan75"
x="80"
y="827.25">+ forward-auth Authelia.</tspan></text>
<text
x="80"
y="854"
class="txt"
id="text65"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan1"
x="80"
y="854">• Blue/green : 8081/8082 ; Traefik décide le LIVE</tspan><tspan
sodipodi:role="line"
id="tspan2"
x="80"
y="870.25">(DSM pointe toujours sur :18080).</tspan></text>
<text
x="80"
y="891"
class="small"
id="text66"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan72"
x="80"
y="891">Astuce : exporte en PNG/PDF pour lecture “grand public”,</tspan><tspan
sodipodi:role="line"
id="tspan73"
x="80"
y="906">garde SVG comme source éditable.</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

+546
View File
@@ -0,0 +1,546 @@
# RUNBOOK — Déploiement Blue/Green (NAS DS220+)
> Objectif : déployer une release **sans casser**, avec rollback immédiat.
## 0) Portée
Ce runbook décrit le déploiement de l’édition web Archicratie sur NAS (Synology), en mode blue/green :
- `web_blue` : upstream staging → `127.0.0.1:8081`
- `web_green` : upstream live → `127.0.0.1:8082`
- Edge Traefik publie :
- `staging.archicratie.trans-hands.synology.me` → 8081
- `archicratie.trans-hands.synology.me` → 8082
## 1) Pré-requis
- Accès shell NAS (user `archicratia`) + `sudo`
- Docker Compose Synology nécessite souvent :
- `sudo env DOCKER_API_VERSION=1.43 docker compose ...`
- Les fichiers edge Traefik sont dans :
- `/volume2/docker/edge/config/dynamic/`
## 2) Répertoires canon (NAS)
On considère ces chemins (adapter si besoin, mais rester cohérent) :
- Base : `/volume2/docker/archicratie-web`
- Releases : `/volume2/docker/archicratie-web/releases/YYYYMMDD-HHMMSS/app`
- Symlink actif : `/volume2/docker/archicratie-web/current` → pointe vers le `.../app` actif
## 3) Garde-fous (AVANT toute action)
### 3.1 Snapshot de l’état actuel
en bash :
cd /volume2/docker/archicratie-web
ls -la current || true
readlink current || true
### 3.2 Vérifier l’état live/staging upstream direct
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
### 3.3 Vérifier l’état edge (host routing)
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
curl -sSI -H 'Host: archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
Si tu n’es pas authentifié, tu verras un 302 vers auth... : c’est normal.
## 4) Procédure de déploiement (release pack → nouvelle release)
### 4.1 Déposer le pack
Hypothèse : tu as un .tgz “release pack” (issu de release-pack.sh) dans incoming/ :
cd /volume2/docker/archicratie-web
ls -la incoming | tail -n 20
### 4.2 Créer un répertoire release
TS="$(date +%Y%m%d-%H%M%S)"
REL="/volume2/docker/archicratie-web/releases/$TS"
APP="$REL/app"
sudo mkdir -p "$APP"
### 4.3 Extraire le pack
PKG="/volume2/docker/archicratie-web/incoming/archicratie-web.tar.gz" # adapter au nom réel
sudo tar -xzf "$PKG" -C "$APP"
### 4.4 Sanity check (fichiers attendus)
sudo test -f "$APP/Dockerfile" && echo "OK Dockerfile"
sudo test -f "$APP/docker-compose.yml" && echo "OK compose"
sudo test -f "$APP/astro.config.mjs" && echo "OK astro config"
sudo test -f "$APP/src/layouts/EditionLayout.astro" && echo "OK layout"
sudo test -f "$APP/src/pages/archicrat-ia/index.astro" && echo "OK archicrat-ia index"
sudo test -f "$APP/docs/diagrams/archicratie-web-edition-global-verbatim-v2.svg" && echo "OK diagrams"
### 4.5 Permissions (crucial sur Synology)
But : archicratia:users doit pouvoir traverser le parent + lire le contenu.
sudo chown -R archicratia:users "$REL"
sudo chmod -R u+rwX,g+rX,o-rwx "$REL"
sudo chmod 750 "$REL" "$APP"
Vérifier :
ls -ld "$REL" "$APP"
ls -la "$APP" | head
## 5) Activation : basculer current vers la nouvelle release
### 5.1 Backup du current existant
cd /volume2/docker/archicratie-web
TS2="$(date +%F-%H%M%S)"
# on backup "current" (symlink ou dossier)
if [ -e current ] || [ -L current ]; then
sudo mv -f current "current.BAK.$TS2"
echo "✅ backup: current.BAK.$TS2"
fi
### 5.2 Recréer current (symlink propre)
sudo ln -s "$APP" current
ls -la current
readlink current
sudo test -f current/docker-compose.yml && echo "✅ OK: current/docker-compose.yml"
Si cd current échoue, c’est que current n’est pas un symlink correct OU que le parent n’est pas traversable (permissions).
## 6) Build & run : (re)construire web_blue/web_green
### 6.1 Vérifier la config compose
cd /volume2/docker/archicratie-web/current
sudo env DOCKER_API_VERSION=1.43 docker compose -f docker-compose.yml config \
| grep -nE 'services:|web_blue:|web_green:|context:|dockerfile:|PUBLIC_SITE|REQUIRE_PUBLIC_SITE' \
| sed -n '1,220p'
### 6.2 Build propre (recommandé si changement de code/config)
sudo env DOCKER_API_VERSION=1.43 docker compose build --no-cache web_blue web_green
### 6.3 Up (force recreate)
sudo env DOCKER_API_VERSION=1.43 docker compose up -d --force-recreate web_blue web_green
### 6.4 Vérifier upstream direct (8081/8082)
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
## 7) Tests de non-régression (MINIMAL CHECKLIST)
À exécuter systématiquement après up.
### 7.1 Upstreams directs
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
### 7.2 Canonical (anti “localhost en prod”)
curl -sS http://127.0.0.1:8081/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
Attendu :
blue (8081) → https://staging.archicratie.../
green (8082) → https://archicratie.../
### 7.3 Edge routing (Host header + diag)
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/_auth/whoami \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
### 7.4 Smoke UI (manuel)
Home : lien “Essai-thèse — ArchiCraT-IA” → /archicrat-ia/
TOC global : liens /archicrat-ia/* (pas de préfixe /archicratie/archicrat-ia/*)
Reading-follow/TOC local : scroll ok
## 8) Rollback (si un seul test est mauvais)
Objectif : revenir immédiatement à l’état précédent.
### 8.1 Repointer current sur l’ancien backup
cd /volume2/docker/archicratie-web
ls -la current.BAK.* | tail -n 5
# choisir le plus récent
OLD="current.BAK.YYYY-MM-DD-HHMMSS"
sudo rm -f current
sudo ln -s "$(readlink -f "$OLD")" current 2>/dev/null || sudo ln -s "$(readlink "$OLD")" current
ls -la current
readlink current
### 8.2 Rebuild + recreate
cd /volume2/docker/archicratie-web/current
sudo env DOCKER_API_VERSION=1.43 docker compose build --no-cache web_blue web_green
sudo env DOCKER_API_VERSION=1.43 docker compose up -d --force-recreate web_blue web_green
### 8.3 Re-tester la checklist (section 7)
Si rollback OK : investiguer en environnement isolé (staging upstream uniquement, ou release dans un autre current).
## 9) Notes opérationnelles
Ne jamais modifier dist/ “à la main” sur NAS.
Si un hotfix prod est indispensable : documenter et backporter via PR Gitea.
Le canonical dépend du build : PUBLIC_SITE doit être injecté (voir runbook ENV-PUBLIC_SITE).
## 10) CI Deploy (Gitea Actions) — Gate SKIP / HOTPATCH / FULL (merge-proof) + preuves
Cette section documente le comportement **canonique** du workflow :
- `.gitea/workflows/deploy-staging-live.yml`
Objectif : **zéro surprise**.
On ne veut plus “penser à force=1”.
Le gate doit décider automatiquement, y compris sur des **merge commits**.
### 10.1 — Principe (ce que fait réellement le gate)
Le job `deploy` calcule les fichiers modifiés entre :
- `BEFORE` = commit précédent (avant le push sur main)
- `AFTER` = commit actuel (après le push / merge sur main)
Puis il classe le déploiement dans un mode :
- **MODE=full**
- rebuild image + restart `archicratie-web-blue` (8081) + `archicratie-web-green` (8082)
- warmup endpoints (para-index, annotations-index, pagefind.js)
- vérification canonical staging + live
- **MODE=hotpatch**
- rebuild d’un `annotations-index.json` consolidé depuis `src/annotations/**`
- patch direct dans les conteneurs en cours d’exécution (blue+green)
- copie des médias modifiés `public/media/**` vers `/usr/share/nginx/html/media/**`
- smoke sur `/annotations-index.json` des deux ports
- **MODE=skip**
- pas de déploiement (on évite le bruit)
⚠️ Important : le mode “hotpatch” **ne rebuild pas** Astro.
Donc toute modification de contenu, routes, scripts, anchors, etc. doit déclencher **full**.
### 10.2 — Matrice de décision (règles officielles)
Le gate définit deux flags :
- `HAS_FULL=1` si changement “build-impacting”
- `HAS_HOTPATCH=1` si changement “annotations/media only”
Règle de priorité :
1) Si `HAS_FULL=1` → **MODE=full**
2) Sinon si `HAS_HOTPATCH=1` → **MODE=hotpatch**
3) Sinon → **MODE=skip**
#### 10.2.1 — Changements qui déclenchent FULL (build-impacting)
Exemples typiques (non exhaustif, mais on couvre le cœur) :
- `src/content/**` (contenu MD/MDX)
- `src/pages/**` (routes Astro)
- `src/anchors/**` (aliases d’ancres)
- `scripts/**` (tooling postbuild : injection, index, tests)
- `src/layouts/**`, `src/components/**`, `src/styles/**` (rendu et scripts inline)
- `astro.config.mjs`, `package.json`, `package-lock.json`
- `Dockerfile`, `docker-compose.yml`, `nginx.conf`
- `.gitea/workflows/**` (changement infra CI/CD)
=> On veut **full** pour garantir cohérence et éviter “site partiellement mis à jour”.
#### 10.2.2 — Changements qui déclenchent HOTPATCH (sans rebuild)
Uniquement :
- `src/annotations/**` (shards YAML)
- `public/media/**` (assets média)
=> On veut hotpatch pour vitesse et éviter rebuild NAS.
### 10.3 — “Merge-proof” : pourquoi on ne lit PAS seulement `git show $SHA`
Sur un merge commit, `git show --name-only $SHA` peut être trompeur selon le contexte.
La méthode robuste est :
- utiliser `event.json` (Gitea Actions) pour récupérer `before` et `after`
- calculer `git diff --name-only BEFORE AFTER`
C’est ce qui rend le gate **merge-proof**.
### 10.4 — Tests de preuve A/B (reproductibles)
Ces tests valident le gate sans ambiguïté.
But : vérifier que le mode choisi est EXACTEMENT celui attendu.
#### Test A — toucher `src/content/...` (FULL auto)
1) Créer une branche test
2) Modifier 1 fichier dans `src/content/` (ex : ajouter une ligne de commentaire non destructive)
3) PR → merge dans `main`
4) Vérifier dans `deploy-staging-live.yml` :
Attendus :
- `Gate flags: HAS_FULL=1 HAS_HOTPATCH=0`
- `✅ build-impacting change -> MODE=full (rebuild+restart)`
- Les étapes FULL (blue puis green) s’exécutent réellement
#### Test B — toucher `src/annotations/...` uniquement (HOTPATCH auto)
1) Créer une branche test
2) Modifier 1 fichier sous `src/annotations/**` (ex: un champ comment, ts, etc.)
3) PR → merge dans `main`
4) Vérifier dans `deploy-staging-live.yml` :
Attendus :
- `Gate flags: HAS_FULL=0 HAS_HOTPATCH=1`
- `✅ annotations/media change -> MODE=hotpatch`
- Les étapes FULL sont “skip” (durée 0s)
- L’étape HOTPATCH s’exécute réellement
### 10.5 — Preuve opérationnelle côté NAS (2 URLs + 2 commandes)
But : prouver que staging+live servent bien les endpoints essentiels (et que le déploiement n’a pas “fait semblant”).
#### 10.5.1 — Deux URLs à vérifier (staging et live)
- Staging (blue) : `http://127.0.0.1:8081/`
- Live (green) : `http://127.0.0.1:8082/`
#### 10.5.2 — Deux commandes minimales (zéro débat)
```bash
curl -fsSI http://127.0.0.1:8081/ | head -n 1
curl -fsSI http://127.0.0.1:8082/ | head -n 1
---
## 10) CI Deploy (Gitea Actions) — Gate SKIP / HOTPATCH / FULL (merge-proof) + preuves
Cette section documente le comportement **canonique** du workflow :
- `.gitea/workflows/deploy-staging-live.yml`
Objectif : **zéro surprise**.
On ne veut plus “penser à force=1”.
Le gate doit décider automatiquement, y compris sur des **merge commits**.
### 10.1 — Principe (ce que fait réellement le gate)
Le job `deploy` calcule les fichiers modifiés entre :
- `BEFORE` = commit précédent (avant le push sur main)
- `AFTER` = commit actuel (après le push / merge sur main)
Puis il classe le déploiement dans un mode :
- **MODE=full**
- rebuild image + restart `archicratie-web-blue` (8081) + `archicratie-web-green` (8082)
- warmup endpoints (para-index, annotations-index, pagefind.js)
- vérification canonical staging + live
- **MODE=hotpatch**
- rebuild d’un `annotations-index.json` consolidé depuis `src/annotations/**`
- patch direct dans les conteneurs en cours d’exécution (blue+green)
- copie des médias modifiés `public/media/**` vers `/usr/share/nginx/html/media/**`
- smoke sur `/annotations-index.json` des deux ports
- **MODE=skip**
- pas de déploiement (on évite le bruit)
⚠️ Important : le mode “hotpatch” **ne rebuild pas** Astro.
Donc toute modification de contenu, routes, scripts, anchors, etc. doit déclencher **full**.
### 10.2 — Matrice de décision (règles officielles)
Le gate définit deux flags :
- `HAS_FULL=1` si changement “build-impacting”
- `HAS_HOTPATCH=1` si changement “annotations/media only”
Règle de priorité :
1) Si `HAS_FULL=1` → **MODE=full**
2) Sinon si `HAS_HOTPATCH=1` → **MODE=hotpatch**
3) Sinon → **MODE=skip**
#### 10.2.1 — Changements qui déclenchent FULL (build-impacting)
Exemples typiques (non exhaustif, mais on couvre le cœur) :
- `src/content/**` (contenu MD/MDX)
- `src/pages/**` (routes Astro)
- `src/anchors/**` (aliases d’ancres)
- `scripts/**` (tooling postbuild : injection, index, tests)
- `src/layouts/**`, `src/components/**`, `src/styles/**` (rendu et scripts inline)
- `astro.config.mjs`, `package.json`, `package-lock.json`
- `Dockerfile`, `docker-compose.yml`, `nginx.conf`
- `.gitea/workflows/**` (changement infra CI/CD)
=> On veut **full** pour garantir cohérence et éviter “site partiellement mis à jour”.
#### 10.2.2 — Changements qui déclenchent HOTPATCH (sans rebuild)
Uniquement :
- `src/annotations/**` (shards YAML)
- `public/media/**` (assets média)
=> On veut hotpatch pour vitesse et éviter rebuild NAS.
### 10.3 — “Merge-proof” : pourquoi on ne lit PAS seulement `git show $SHA`
Sur un merge commit, `git show --name-only $SHA` peut être trompeur selon le contexte.
La méthode robuste est :
- utiliser `event.json` (Gitea Actions) pour récupérer `before` et `after`
- calculer `git diff --name-only BEFORE AFTER`
C’est ce qui rend le gate **merge-proof**.
### 10.4 — Tests de preuve A/B (reproductibles)
Ces tests valident le gate sans ambiguïté.
But : vérifier que le mode choisi est EXACTEMENT celui attendu.
#### Test A — toucher `src/content/...` (FULL auto)
1) Créer une branche test
2) Modifier 1 fichier dans `src/content/` (ex : ajouter une ligne de commentaire non destructive)
3) PR → merge dans `main`
4) Vérifier dans `deploy-staging-live.yml` :
Attendus :
- `Gate flags: HAS_FULL=1 HAS_HOTPATCH=0`
- `✅ build-impacting change -> MODE=full (rebuild+restart)`
- Les étapes FULL (blue puis green) s’exécutent réellement
#### Test B — toucher `src/annotations/...` uniquement (HOTPATCH auto)
1) Créer une branche test
2) Modifier 1 fichier sous `src/annotations/**` (ex: un champ comment, ts, etc.)
3) PR → merge dans `main`
4) Vérifier dans `deploy-staging-live.yml` :
Attendus :
- `Gate flags: HAS_FULL=0 HAS_HOTPATCH=1`
- `✅ annotations/media change -> MODE=hotpatch`
- Les étapes FULL sont “skip” (durée 0s)
- L’étape HOTPATCH s’exécute réellement
### 10.5 — Preuve opérationnelle côté NAS (2 URLs + 2 commandes)
But : prouver que staging+live servent bien les endpoints essentiels (et que le déploiement n’a pas “fait semblant”).
#### 10.5.1 — Deux URLs à vérifier (staging et live)
- Staging (blue) : `http://127.0.0.1:8081/`
- Live (green) : `http://127.0.0.1:8082/`
#### 10.5.2 — Deux commandes minimales (zéro débat)
en bash :
curl -fsSI http://127.0.0.1:8081/ | head -n 1
curl -fsSI http://127.0.0.1:8082/ | head -n 1
Attendu : HTTP/1.1 200 OK des deux côtés.
10.6 — Preuve “alias injection” (ancre ancienne → nouvelle) sur une page
Contexte : lorsqu’un paragraphe change (ex: ticket “Proposer” appliqué),
l’ID de paragraphe peut changer, mais on doit préserver les liens anciens via :
src/anchors/anchor-aliases.json
injection build-time dans dist (span .para-alias)
10.6.1 — Check rapide (staging + live)
Remplacer OLD/NEW par tes ids réels :
Attendu : HTTP/1.1 200 OK des deux côtés.
10.6 — Preuve “alias injection” (ancre ancienne → nouvelle) sur une page
Contexte : lorsqu’un paragraphe change (ex: ticket “Proposer” appliqué),
l’ID de paragraphe peut changer, mais on doit préserver les liens anciens via :
src/anchors/anchor-aliases.json
injection build-time dans dist (span .para-alias)
10.6.1 — Check rapide (staging + live)
Remplacer OLD/NEW par tes ids réels :
OLD="p-1-60c7ea48"
NEW="p-1-a21087b0"
for P in 8081 8082; do
echo "=== $P ==="
HTML="$(curl -fsS "http://127.0.0.1:${P}/archicrat-ia/chapitre-3/" | tr -d '\r')"
echo "OLD count: $(printf '%s' "$HTML" | grep -o "$OLD" | wc -l | tr -d ' ')"
echo "NEW count: $(printf '%s' "$HTML" | grep -o "$NEW" | wc -l | tr -d ' ')"
printf '%s\n' "$HTML" | grep -nE "$OLD|$NEW|class=\"para-alias\"" | head -n 40 || true
done
Attendu :
présence d’un alias : <span id="$OLD" class="para-alias"...>
présence du nouveau paragraphe : <p id="$NEW">...
10.6.2 — Check “lien ancien ne casse pas” (HTTP 200)
for P in 8081 8082; do
curl -fsSI "http://127.0.0.1:${P}/archicrat-ia/chapitre-3/#${OLD}" | head -n 1
done
Attendu : HTTP/1.1 200 OK et navigation fonctionnelle côté navigateur.
10.7 — Troubleshooting gate (symptômes typiques)
Symptom 1 : job bloqué “Set up job” très longtemps
Causes fréquentes :
runner indisponible / capacity saturée
runner ne récupère pas les tâches (fetch_timeout trop court + réseau instable)
erreur dans “Gate — decide …” qui casse bash (et donne l’impression d’un hang)
Commandes NAS (diagnostic rapide) :
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | grep -E 'gitea-act-runner|registry|archicratie-web'
docker logs --since 30m --tail 400 gitea-act-runner | tail -n 200
Symptom 2 : conditional binary operator expected
Cause :
test bash du type [[ "$X" == "1" && "$Y" == "2" ]] mal formé
variable vide non quotée
usage d’un opérateur non supporté dans la shell effective
Fix :
set -euo pipefail
toujours quoter : [[ "${VAR:-}" == "..." ]]
logguer BEFORE/AFTER/FORCE et s’assurer qu’ils ne sont pas vides
Symptom 3 : le gate liste “trop de fichiers” alors qu’on a changé 1 seul fichier
Cause :
comparaison faite sur le mauvais range (ex: git show sur merge, ou mauvais parent)
Fix :
toujours utiliser git diff --name-only "$BEFORE" "$AFTER" (merge-proof)
confirmer dans le log : Gate ctx: BEFORE=... AFTER=...
+147
View File
@@ -0,0 +1,147 @@
# RUNBOOK — Edge Traefik (routing + SSO Authelia)
> Objectif : comprendre et diagnostiquer rapidement qui route quoi, et pourquoi staging/live peuvent diverger.
## 0) Portée
Edge Traefik route plusieurs hosts vers des backends locaux (127.0.0.1:*), avec Auth via Authelia.
Répertoire :
- `/volume2/docker/edge/config/dynamic/`
Port d’entrée edge :
- `http://127.0.0.1:18080/` (entryPoint `web`)
- Les hosts publics pointent vers cet edge.
## 1) Fichiers dynamiques (canon)
### 00-smoke.yml
- route `/__smoke` vers le service `smoke_svc` → `127.0.0.1:18081`
### 10-core.yml
- définit les middlewares :
- `sanitize-remote`
- `authelia` (forwardAuth vers 9091)
- `chain-auth` (chain sanitize-remote + authelia)
### 20-archicratie-backend.yml
- définit service `archicratie_web` → `127.0.0.1:8082` (live upstream)
### 21-archicratie-staging.yml
- route staging host vers `127.0.0.1:8081` (staging upstream)
- applique middlewares `diag-staging@file` et `chain-auth@file`
- IMPORTANT : `diag-staging@file` doit exister
### 22-archicratie-authinfo-staging.yml
- route `/ _auth /` sur staging vers `whoami@file`
- applique `diag-staging-authinfo@file` + `chain-auth@file`
- IMPORTANT : `diag-staging-authinfo@file` doit exister
### 90-overlay-staging-fix.yml (overlay de diagnostic + fallback)
Rôle :
- **fournir** les middlewares manquants (`diag-staging`, `diag-staging-authinfo`)
- optionnel : fallback route si 21/22 sont cassés
- injecter un header `X-Archi-Router` pour identifier le routeur utilisé
### 92-overlay-live-fix.yml
- route live host `archicratie.trans-hands.synology.me` → `archicratie_web@file` (8082)
- route `/ _auth/whoami` → `whoami@file` (18081)
## 2) Diagnostiquer rapidement : quel routeur répond ?
### 2.1 Test “host header” (sans UI)
# en bash :
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/_auth/whoami \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
# Interprétation :
X-Archi-Router: staging@21 → routeur 21-archicratie-staging.yml OK
X-Archi-Router: staging-authinfo@22 → routeur authinfo OK
Si tu vois staging-fallback@90 → tu es tombé sur le fallback 90 (donc 21/22 potentiellement invalides)
### 2.2 Vérifier l’upstream direct derrière edge
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
Si 8081 et 8082 servent des versions différentes : c’est “normal” en blue/green, mais il faut savoir laquelle est censée être staging/live.
## 3) Diagnostiquer les erreurs Traefik (fichier invalide / middleware manquant)
### 3.1 Grep “level=error”
sudo docker logs edge-traefik --since 5m | grep -Ei 'level=error|middleware|router|service|yaml' | tail -n 80
# Cas typique :
middleware "diag-staging@file" does not exist
→ 21-archicratie-staging.yml référence un middleware absent. Solution : le définir (souvent dans 90-overlay-staging-fix.yml).
## 4) Procédure safe de modification (jamais en aveugle)
### 4.1 Backup
cd /volume2/docker/edge/config/dynamic
TS="$(date +%F-%H%M%S)"
sudo cp -a 90-overlay-staging-fix.yml "90-overlay-staging-fix.yml.bak.$TS"
### 4.2 Édition (ex : ajouter middlewares diag)
Faire une modif minimale
Ne pas casser les règles existantes (Host + PathPrefix)
Respecter les priorités (voir section 5)
### 4.3 Reload Traefik
sudo docker restart edge-traefik
### 4.4 Tests immédiats
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router'
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/_auth/whoami \
| grep -iE 'HTTP/|location:|x-archi-router'
## 5) Priorités Traefik (le point subtil)
Traefik choisit le routeur selon :
la correspondance de règle
la priority (plus grand gagne)
en cas d’égalité, l’ordre interne (à éviter)
### 5.1 Canon pour staging
21-archicratie-staging.yml : priority 10
22-archicratie-authinfo-staging.yml : priority 10000
90-overlay-staging-fix.yml :
fallback host : priority faible (ex: 5) pour ne PAS écraser 21
fallback whoami : priority < 10000 (ex: 9000) pour ne PAS écraser 22
=> On garde 90 comme filet de sécurité / diag, pas comme “source”.
## 6) Rollback (si un changement edge casse staging/live)
cd /volume2/docker/edge/config/dynamic
# choisir le bon backup
sudo mv -f 90-overlay-staging-fix.yml "90-overlay-staging-fix.yml.BAD.$(date +%F-%H%M%S)"
sudo cp -a 90-overlay-staging-fix.yml.bak.YYYY-MM-DD-HHMMSS 90-overlay-staging-fix.yml
sudo docker restart edge-traefik
Puis re-tests section 2.
## 7) Remarques
Les 302 Authelia sont normaux si non authentifié.
Un 404 “Not Found” depuis edge alors que 8081 répond : souvent routeur manquant / invalidé / middleware absent.
+114
View File
@@ -0,0 +1,114 @@
# RUNBOOK — PUBLIC_SITE (canonical + sitemap) “anti localhost en prod”
> Objectif : ne plus jamais voir `rel="canonical" href="http://localhost:4321/"` en staging/live.
## 0) Pourquoi c’est critique
Astro génère :
- `<link rel="canonical" href="...">`
- `sitemap-index.xml`
Ces valeurs dépendent de `site` dans `astro.config.mjs`.
Si `site` vaut `http://localhost:4321` au moment du build Docker, **la prod sortira des canonical faux** :
- SEO / partage / cohérence de navigation impactés
- confusion staging/live
## 1) Règle canonique
- `astro.config.mjs` :
# en js :
site: process.env.PUBLIC_SITE ?? "http://localhost:4321"
# Donc :
En DEV local : pas besoin de PUBLIC_SITE (fallback ok)
En build “déploiement” : on DOIT fournir PUBLIC_SITE
## 2) Exigence “antifragile”
### 2.1 Dockerfile (build stage)
On injecte PUBLIC_SITE au build et on peut le rendre obligatoire :
ARG PUBLIC_SITE
ARG REQUIRE_PUBLIC_SITE=0
ENV PUBLIC_SITE=$PUBLIC_SITE
# garde-fou :
RUN if [ "$REQUIRE_PUBLIC_SITE" = "1" ] && [ -z "$PUBLIC_SITE" ]; then \
echo "ERROR: PUBLIC_SITE is required (REQUIRE_PUBLIC_SITE=1)"; exit 1; \
fi
=> Si quelqu’un oublie l’URL en prod, le build casse au lieu de produire une release mauvaise.
## 3) docker-compose : blue/staging vs green/live
Objectif : injecter deux valeurs différentes, sans bricolage.
### 3.1 .env (NAS)
Exemple canonique :
PUBLIC_SITE_BLUE=https://staging.archicratie.trans-hands.synology.me
PUBLIC_SITE_GREEN=https://archicratie.trans-hands.synology.me
### 3.2 docker-compose.yml
web_blue :
REQUIRE_PUBLIC_SITE: "1"
PUBLIC_SITE: ${PUBLIC_SITE_BLUE}
web_green :
REQUIRE_PUBLIC_SITE: "1"
PUBLIC_SITE: ${PUBLIC_SITE_GREEN}
## 4) Tests (obligatoires après build)
### 4.1 Vérifier l’injection dans compose
sudo env DOCKER_API_VERSION=1.43 docker compose config \
| grep -nE 'PUBLIC_SITE|REQUIRE_PUBLIC_SITE|web_blue:|web_green:' | sed -n '1,200p'
### 4.2 Vérifier canonical (upstream direct)
curl -sS http://127.0.0.1:8081/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
# Attendu :
blue : https://staging.../
green : https://archicratie.../
## 5) Procédure de correction (si canonical est faux)
### 5.1 Vérifier astro.config.mjs dans la release courante
cd /volume2/docker/archicratie-web/current
grep -nE 'site:\s*process\.env\.PUBLIC_SITE' astro.config.mjs
### 5.2 Vérifier que Dockerfile exporte PUBLIC_SITE
grep -nE 'ARG PUBLIC_SITE|ENV PUBLIC_SITE|REQUIRE_PUBLIC_SITE' Dockerfile
### 5.3 Vérifier .env et compose
grep -nE 'PUBLIC_SITE_BLUE|PUBLIC_SITE_GREEN' .env
grep -nE 'PUBLIC_SITE|REQUIRE_PUBLIC_SITE' docker-compose.yml
### 5.4 Rebuild + recreate
sudo env DOCKER_API_VERSION=1.43 docker compose build --no-cache web_blue web_green
sudo env DOCKER_API_VERSION=1.43 docker compose up -d --force-recreate web_blue web_green
Puis tests section 4.
## 6) Notes
Cette mécanique doit être backportée dans Gitea (source canonique), sinon ça re-cassera au prochain pack.
En DEV local, conserver le fallback http://localhost:4321 est utile et normal.
+524 -806
View File
File diff suppressed because it is too large. Load diff
+10 -13
View File
@@ -4,35 +4,32 @@
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"dev": "node scripts/write-dev-whoami.mjs && astro dev",
"preview": "astro preview",
"astro": "astro",
"clean": "rm -rf dist",
"build": "astro build",
"build:clean": "npm run clean && npm run build",
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && npx pagefind --site dist",
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && node scripts/build-para-index.mjs && node scripts/build-annotations-index.mjs && node scripts/purge-dist-dev-whoami.mjs && npx pagefind --site dist",
"import": "node scripts/import-docx.mjs",
"apply:ticket": "node scripts/apply-ticket.mjs",
"audit:dist": "node scripts/audit-dist.mjs",
"build:para-index": "node scripts/build-para-index.mjs",
"build:annotations-index": "node scripts/build-annotations-index.mjs",
"test:aliases": "node scripts/check-anchor-aliases.mjs",
"test:anchors": "node scripts/check-anchors.mjs",
"test:anchors:update": "node scripts/check-anchors.mjs --update",
"test": "npm run test:aliases && npm run build:clean && npm run audit:dist && node scripts/verify-anchor-aliases-in-dist.mjs && npm run test:anchors && node scripts/check-inline-js.mjs",
"test:annotations": "node scripts/check-annotations.mjs",
"test:annotations:media": "node scripts/check-annotations-media.mjs",
"test": "npm run test:aliases && npm run build:clean && npm run audit:dist && node scripts/verify-anchor-aliases-in-dist.mjs && npm run test:anchors && npm run test:annotations && npm run test:annotations:media && node scripts/check-inline-js.mjs",
"ci": "CI=1 npm test"
},
"dependencies": {
"@astrojs/mdx": "^4.3.13",
"astro": "^5.16.11"
"@astrojs/mdx": "^5.0.0",
"astro": "^6.0.2"
},
"devDependencies": {
"@astrojs/sitemap": "^3.7.0",
"@astrojs/sitemap": "^3.7.1",
"mammoth": "^1.11.0",
"pagefind": "^1.4.0",
"rehype-autolink-headings": "^7.1.0",
View File
Whitespace-only changes.
+1 -1
View File
@@ -1 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone","orientation":"any"}
+899
View File
@@ -0,0 +1,899 @@
#!/usr/bin/env node
// scripts/apply-annotation-ticket.mjs
//
// Applique un ticket Gitea "type/media | type/reference | type/comment" vers:
//
// ✅ src/annotations/<oeuvre>/<chapitre>/<paraId>.yml (sharding par paragraphe)
// ✅ public/media/<oeuvre>/<chapitre>/<paraId>/<file>
//
// Compat rétro : lit (si présent) l'ancien monolithe:
// src/annotations/<oeuvre>/<chapitre>.yml
// et deep-merge NON destructif dans le shard lors d'une nouvelle application,
// pour permettre une migration progressive sans perte.
//
// Robuste, idempotent, non destructif.
// DRY RUN si --dry-run
// Options: --dry-run --no-download --verify --strict --commit --close
//
// Env requis:
// FORGE_API = base API Gitea (LAN) ex: http://192.168.1.20:3000
// FORGE_TOKEN = PAT Gitea (repo + issues)
//
// Env optionnel:
// GITEA_OWNER / GITEA_REPO (sinon auto-détecté via git remote)
// ANNO_DIR (défaut: src/annotations)
// PUBLIC_DIR (défaut: public)
// MEDIA_ROOT (défaut URL: /media)
//
// Ticket attendu (body):
// Chemin: /archicrat-ia/chapitre-4/
// Ancre: #p-0-xxxxxxxx
// Type: type/media | type/reference | type/comment
//
// Exit codes:
// 0 ok
// 1 erreur fatale
// 2 refus (strict/verify/usage)
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
import YAML from "yaml";
/* ---------------------------------- usage --------------------------------- */
function usage(exitCode = 0) {
console.log(`
apply-annotation-ticket — applique un ticket SidePanel (media/ref/comment) vers src/annotations/ (shard par paragraphe)
Usage:
node scripts/apply-annotation-ticket.mjs <issue_number> [--dry-run] [--no-download] [--verify] [--strict] [--commit] [--close]
Flags:
--dry-run : n'écrit rien (affiche un aperçu)
--no-download : n'essaie pas de télécharger les pièces jointes (media)
--verify : vérifie que (page, ancre) existent (dist/para-index.json si dispo, sinon baseline)
--strict : refuse si URL ref invalide (http/https) OU caption media vide OU verify impossible
--commit : git add + git commit (commit dans la branche courante)
--close : ferme le ticket (nécessite --commit)
Env requis:
FORGE_API = base API Gitea (LAN) ex: http://192.168.1.20:3000
FORGE_TOKEN = PAT Gitea (repo + issues)
Env optionnel:
GITEA_OWNER / GITEA_REPO (sinon auto-détecté via git remote)
ANNO_DIR (défaut: src/annotations)
PUBLIC_DIR (défaut: public)
MEDIA_ROOT (défaut URL: /media)
Exit codes:
0 ok
1 erreur fatale
2 refus (strict/verify/close sans commit / incohérence)
`);
process.exit(exitCode);
}
/* ---------------------------------- args ---------------------------------- */
const argv = process.argv.slice(2);
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) usage(0);
const issueNum = Number(argv[0]);
if (!Number.isFinite(issueNum) || issueNum <= 0) {
console.error("❌ Numéro de ticket invalide.");
usage(2);
}
const DRY_RUN = argv.includes("--dry-run");
const NO_DOWNLOAD = argv.includes("--no-download");
const DO_VERIFY = argv.includes("--verify");
const STRICT = argv.includes("--strict");
const DO_COMMIT = argv.includes("--commit");
const DO_CLOSE = argv.includes("--close");
if (DO_CLOSE && !DO_COMMIT) {
console.error("❌ --close nécessite --commit.");
process.exit(2);
}
if (typeof fetch !== "function") {
console.error("❌ fetch() indisponible. Utilise Node 18+.");
process.exit(1);
}
/* --------------------------------- config --------------------------------- */
const CWD = process.cwd();
const ANNO_DIR = path.join(CWD, process.env.ANNO_DIR || "src", "annotations");
const PUBLIC_DIR = path.join(CWD, process.env.PUBLIC_DIR || "public");
const MEDIA_URL_ROOT = String(process.env.MEDIA_ROOT || "/media").replace(/\/+$/, "");
/* --------------------------------- helpers -------------------------------- */
function getEnv(name, fallback = "") {
return (process.env[name] ?? fallback).trim();
}
function run(cmd, args, opts = {}) {
const r = spawnSync(cmd, args, { stdio: "inherit", ...opts });
if (r.error) throw r.error;
if (r.status !== 0) throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
}
function runQuiet(cmd, args, opts = {}) {
const r = spawnSync(cmd, args, { encoding: "utf8", stdio: "pipe", ...opts });
if (r.error) throw r.error;
if (r.status !== 0) {
const out = (r.stdout || "") + (r.stderr || "");
throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${out}`);
}
return r.stdout || "";
}
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
function inferOwnerRepoFromGit() {
const r = spawnSync("git", ["remote", "get-url", "origin"], { encoding: "utf-8" });
if (r.status !== 0) return null;
const u = (r.stdout || "").trim();
const m = u.match(/[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/);
if (!m?.groups) return null;
return { owner: m.groups.owner, repo: m.groups.repo };
}
function gitHasStagedChanges() {
const r = spawnSync("git", ["diff", "--cached", "--quiet"]);
return r.status === 1;
}
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function pickLine(body, key) {
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:\\s*([^\\n\\r]+)`, "mi");
const m = String(body || "").match(re);
return m ? m[1].trim() : "";
}
function pickSection(body, markers) {
const text = String(body || "").replace(/\r\n/g, "\n");
const idx = markers
.map((m) => ({ m, i: text.toLowerCase().indexOf(m.toLowerCase()) }))
.filter((x) => x.i >= 0)
.sort((a, b) => a.i - b.i)[0];
if (!idx) return "";
const start = idx.i + idx.m.length;
const tail = text.slice(start);
const stops = ["\n## ", "\n---", "\nJustification", "\nProposition", "\nSources"];
let end = tail.length;
for (const s of stops) {
const j = tail.toLowerCase().indexOf(s.toLowerCase());
if (j >= 0 && j < end) end = j;
}
return tail.slice(0, end).trim();
}
function normalizeChemin(chemin) {
let c = String(chemin || "").trim();
if (!c) return "";
if (!c.startsWith("/")) c = "/" + c;
if (!c.endsWith("/")) c = c + "/";
c = c.replace(/\/{2,}/g, "/");
return c;
}
function normalizePageKeyFromChemin(chemin) {
// ex: /archicrat-ia/chapitre-4/ => archicrat-ia/chapitre-4
return normalizeChemin(chemin).replace(/^\/+|\/+$/g, "");
}
function normalizeAnchorId(s) {
let a = String(s || "").trim();
if (a.startsWith("#")) a = a.slice(1);
return a;
}
function assert(cond, msg, code = 1) {
if (!cond) {
const e = new Error(msg);
e.__exitCode = code;
throw e;
}
}
function isPlainObject(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function paraIndexFromId(id) {
const m = String(id).match(/^p-(\d+)-/i);
return m ? Number(m[1]) : Number.NaN;
}
function isHttpUrl(u) {
try {
const x = new URL(String(u));
return x.protocol === "http:" || x.protocol === "https:";
} catch {
return false;
}
}
function stableSortByTs(arr) {
if (!Array.isArray(arr)) return;
arr.sort((a, b) => {
const ta = Date.parse(a?.ts || "") || 0;
const tb = Date.parse(b?.ts || "") || 0;
if (ta !== tb) return ta - tb;
return JSON.stringify(a).localeCompare(JSON.stringify(b));
});
}
function normPage(s) {
let x = String(s || "").trim();
if (!x) return "";
// retire origin si on a une URL complète
x = x.replace(/^https?:\/\/[^/]+/i, "");
// enlève query/hash
x = x.split("#")[0].split("?")[0];
// enlève index.html
x = x.replace(/index\.html$/i, "");
// enlève slashs de bord
x = x.replace(/^\/+/, "").replace(/\/+$/, "");
return x;
}
/* ------------------------------ para-index (verify + order) ------------------------------ */
async function loadParaOrderFromDist(pageKey) {
const distIdx = path.join(CWD, "dist", "para-index.json");
if (!(await exists(distIdx))) return null;
let j;
try {
j = JSON.parse(await fs.readFile(distIdx, "utf8"));
} catch {
return null;
}
const want = normPage(pageKey);
// Support A) { items:[{id,page,...}, ...] } (ou variantes)
const items = Array.isArray(j?.items)
? j.items
: Array.isArray(j?.index?.items)
? j.index.items
: null;
if (items) {
const ids = [];
for (const it of items) {
// page peut être dans plein de clés différentes
const pageCand = normPage(
it?.page ??
it?.pageKey ??
it?.path ??
it?.route ??
it?.href ??
it?.url ??
""
);
// id peut être dans plein de clés différentes
let id = String(it?.id ?? it?.paraId ?? it?.anchorId ?? it?.anchor ?? "");
if (id.startsWith("#")) id = id.slice(1);
if (pageCand === want && id) ids.push(id);
}
if (ids.length) return ids;
}
// Support B) { byId: { "p-...": { page:"...", ... }, ... } }
if (j?.byId && typeof j.byId === "object") {
const ids = Object.keys(j.byId)
.filter((id) => {
const meta = j.byId[id] || {};
const pageCand = normPage(meta.page ?? meta.pageKey ?? meta.path ?? meta.route ?? meta.url ?? "");
return pageCand === want;
});
if (ids.length) {
ids.sort((a, b) => {
const ia = paraIndexFromId(a);
const ib = paraIndexFromId(b);
if (Number.isFinite(ia) && Number.isFinite(ib) && ia !== ib) return ia - ib;
return String(a).localeCompare(String(b));
});
return ids;
}
}
// Support C) { pages: { "archicrat-ia/chapitre-4": { ids:[...] } } } (ou variantes)
if (j?.pages && typeof j.pages === "object") {
// essaie de trouver la bonne clé même si elle est /.../ ou .../index.html
const keys = Object.keys(j.pages);
const hit = keys.find((k) => normPage(k) === want);
if (hit) {
const pg = j.pages[hit];
if (Array.isArray(pg?.ids)) return pg.ids.map(String);
if (Array.isArray(pg?.paras)) return pg.paras.map(String);
}
}
return null;
}
async function tryVerifyAnchor(pageKey, anchorId) {
// 1) dist/para-index.json : order complet si possible
const order = await loadParaOrderFromDist(pageKey);
if (order) return order.includes(anchorId);
// 1bis) dist/para-index.json : fallback “best effort” => recherche brute (IDs quasi uniques)
const distIdx = path.join(CWD, "dist", "para-index.json");
if (await exists(distIdx)) {
try {
const raw = await fs.readFile(distIdx, "utf8");
if (raw.includes(`"${anchorId}"`) || raw.includes(`"#${anchorId}"`)) {
return true;
}
} catch {
// ignore
}
}
// 2) tests/anchors-baseline.json (fallback)
const base = path.join(CWD, "tests", "anchors-baseline.json");
if (await exists(base)) {
try {
const j = JSON.parse(await fs.readFile(base, "utf8"));
const candidates = [];
if (j?.pages && typeof j.pages === "object") {
for (const [k, v] of Object.entries(j.pages)) {
if (!Array.isArray(v)) continue;
if (normPage(k).includes(normPage(pageKey))) candidates.push(...v);
}
}
if (Array.isArray(j?.entries)) {
for (const it of j.entries) {
const p = String(it?.page || "");
const ids = it?.ids;
if (Array.isArray(ids) && normPage(p).includes(normPage(pageKey))) candidates.push(...ids);
}
}
if (candidates.length) return candidates.some((x) => String(x) === anchorId);
} catch {
// ignore
}
}
return null; // cannot verify
}
/* ----------------------------- deep merge helpers (non destructive) ----------------------------- */
function keyMedia(x) {
return String(x?.src || "");
}
function keyRef(x) {
return `${x?.url || ""}||${x?.label || ""}||${x?.kind || ""}||${x?.citation || ""}`;
}
function keyComment(x) {
return String(x?.text || "").trim();
}
function uniqUnion(dstArr, srcArr, keyFn) {
const out = Array.isArray(dstArr) ? [...dstArr] : [];
const seen = new Set(out.map((x) => keyFn(x)));
for (const it of (Array.isArray(srcArr) ? srcArr : [])) {
const k = keyFn(it);
if (!k) continue;
if (!seen.has(k)) {
seen.add(k);
out.push(it);
}
}
return out;
}
function deepMergeEntry(dst, src) {
if (!isPlainObject(dst) || !isPlainObject(src)) return;
for (const [k, v] of Object.entries(src)) {
if (k === "media" && Array.isArray(v)) {
dst.media = uniqUnion(dst.media, v, keyMedia);
continue;
}
if (k === "refs" && Array.isArray(v)) {
dst.refs = uniqUnion(dst.refs, v, keyRef);
continue;
}
if (k === "comments_editorial" && Array.isArray(v)) {
dst.comments_editorial = uniqUnion(dst.comments_editorial, v, keyComment);
continue;
}
if (isPlainObject(v)) {
if (!isPlainObject(dst[k])) dst[k] = {};
deepMergeEntry(dst[k], v);
continue;
}
if (Array.isArray(v)) {
const cur = Array.isArray(dst[k]) ? dst[k] : [];
const seen = new Set(cur.map((x) => JSON.stringify(x)));
const out = [...cur];
for (const it of v) {
const s = JSON.stringify(it);
if (!seen.has(s)) {
seen.add(s);
out.push(it);
}
}
dst[k] = out;
continue;
}
// scalar: set only if missing/empty
if (!(k in dst) || dst[k] == null || dst[k] === "") {
dst[k] = v;
}
}
}
/* ----------------------------- annotations I/O ----------------------------- */
async function loadAnnoDocYaml(fileAbs, pageKey) {
if (!(await exists(fileAbs))) {
return { schema: 1, page: pageKey, paras: {} };
}
const raw = await fs.readFile(fileAbs, "utf8");
let doc;
try {
doc = YAML.parse(raw);
} catch (e) {
throw new Error(`${path.relative(CWD, fileAbs)}: parse failed: ${String(e?.message ?? e)}`);
}
assert(isPlainObject(doc), `${path.relative(CWD, fileAbs)}: doc must be an object`, 2);
assert(doc.schema === 1, `${path.relative(CWD, fileAbs)}: schema must be 1`, 2);
assert(isPlainObject(doc.paras), `${path.relative(CWD, fileAbs)}: missing object key "paras"`, 2);
if (doc.page != null) {
const got = String(doc.page).replace(/^\/+/, "").replace(/\/+$/, "");
assert(got === pageKey, `${path.relative(CWD, fileAbs)}: page mismatch (page="${doc.page}" vs path="${pageKey}")`, 2);
} else {
doc.page = pageKey;
}
return doc;
}
function sortParasObject(paras, order) {
const keys = Object.keys(paras || {});
const idx = new Map();
if (Array.isArray(order)) order.forEach((id, i) => idx.set(String(id), i));
keys.sort((a, b) => {
const ha = idx.has(a);
const hb = idx.has(b);
if (ha && hb) return idx.get(a) - idx.get(b);
if (ha && !hb) return -1;
if (!ha && hb) return 1;
const ia = paraIndexFromId(a);
const ib = paraIndexFromId(b);
if (Number.isFinite(ia) && Number.isFinite(ib) && ia !== ib) return ia - ib;
return String(a).localeCompare(String(b));
});
const out = {};
for (const k of keys) out[k] = paras[k];
return out;
}
async function saveAnnoDocYaml(fileAbs, doc, order = null) {
await fs.mkdir(path.dirname(fileAbs), { recursive: true });
doc.paras = sortParasObject(doc.paras, order);
for (const e of Object.values(doc.paras || {})) {
if (!isPlainObject(e)) continue;
stableSortByTs(e.media);
stableSortByTs(e.refs);
stableSortByTs(e.comments_editorial);
}
const out = YAML.stringify(doc);
await fs.writeFile(fileAbs, out, "utf8");
}
/* ------------------------------ gitea helpers ------------------------------ */
function apiBaseNorm(forgeApiBase) {
return forgeApiBase.replace(/\/+$/, "");
}
async function giteaGET(url, token) {
const res = await fetch(url, {
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"User-Agent": "archicratie-apply-annotation/1.0",
},
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} GET ${url}\n${t}`);
}
return await res.json();
}
async function fetchIssue({ forgeApiBase, owner, repo, token, issueNum }) {
const url = `${apiBaseNorm(forgeApiBase)}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
return await giteaGET(url, token);
}
async function fetchIssueAssets({ forgeApiBase, owner, repo, token, issueNum }) {
// Gitea: /issues/{index}/assets
const url = `${apiBaseNorm(forgeApiBase)}/api/v1/repos/${owner}/${repo}/issues/${issueNum}/assets`;
try {
const json = await giteaGET(url, token);
return Array.isArray(json) ? json : [];
} catch {
return [];
}
}
async function postIssueComment({ forgeApiBase, owner, repo, token, issueNum, comment }) {
const url = `${apiBaseNorm(forgeApiBase)}/api/v1/repos/${owner}/${repo}/issues/${issueNum}/comments`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-apply-annotation/1.0",
},
body: JSON.stringify({ body: comment }),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} POST comment ${url}\n${t}`);
}
}
async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment }) {
if (comment) await postIssueComment({ forgeApiBase, owner, repo, token, issueNum, comment });
const url = `${apiBaseNorm(forgeApiBase)}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
const res = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-apply-annotation/1.0",
},
body: JSON.stringify({ state: "closed" }),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} closing issue: ${url}\n${t}`);
}
}
/* ------------------------------ media helpers ------------------------------ */
function inferMediaTypeFromFilename(name) {
const n = String(name || "").toLowerCase();
if (/\.(png|jpe?g|webp|gif|svg)$/.test(n)) return "image";
if (/\.(mp4|webm|mov|m4v)$/.test(n)) return "video";
if (/\.(mp3|wav|ogg|m4a)$/.test(n)) return "audio";
return "link";
}
function sanitizeFilename(name) {
return String(name || "file")
.replace(/[\/\\]/g, "_")
.replace(/[^\w.\-]+/g, "_")
.replace(/_+/g, "_")
.slice(0, 180);
}
async function downloadToFile(url, token, destAbs) {
const res = await fetch(url, {
headers: {
Authorization: `token ${token}`,
"User-Agent": "archicratie-apply-annotation/1.0",
},
redirect: "follow",
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`download failed HTTP ${res.status}: ${url}\n${t}`);
}
const buf = Buffer.from(await res.arrayBuffer());
await fs.mkdir(path.dirname(destAbs), { recursive: true });
await fs.writeFile(destAbs, buf);
return buf.length;
}
/* ------------------------------ type parsers ------------------------------ */
function parseReferenceBlock(body) {
const block =
pickSection(body, ["Référence (à compléter):", "Reference (à compléter):"]) ||
pickSection(body, ["Référence:", "Reference:"]);
const lines = String(block || "").split(/\r?\n/).map((l) => l.trim());
const get = (k) => {
const re = new RegExp(`^[-*]\\s*${escapeRegExp(k)}\\s*:\\s*(.*)$`, "i");
const m = lines.map((l) => l.match(re)).find(Boolean);
return (m?.[1] ?? "").trim();
};
return {
url: get("URL") || "",
label: get("Label") || "",
kind: get("Kind") || "",
citation: get("Citation") || get("Passage") || get("Extrait") || "",
rawBlock: block || "",
};
}
/* ----------------------------------- main ---------------------------------- */
async function main() {
const token = getEnv("FORGE_TOKEN");
assert(token, "❌ FORGE_TOKEN manquant.", 2);
const forgeApiBase = getEnv("FORGE_API") || getEnv("FORGE_BASE");
assert(forgeApiBase, "❌ FORGE_API (ou FORGE_BASE) manquant.", 2);
const inferred = inferOwnerRepoFromGit() || {};
const owner = getEnv("GITEA_OWNER", inferred.owner || "");
const repo = getEnv("GITEA_REPO", inferred.repo || "");
assert(owner && repo, "❌ Impossible de déterminer owner/repo. Fix: export GITEA_OWNER=... GITEA_REPO=...", 2);
console.log(`🔎 Fetch ticket #${issueNum} from ${owner}/${repo} …`);
const issue = await fetchIssue({ forgeApiBase, owner, repo, token, issueNum });
if (issue?.pull_request) {
console.error(`❌ #${issueNum} est une Pull Request, pas un ticket annotations.`);
process.exit(2);
}
const body = String(issue.body || "").replace(/\r\n/g, "\n");
const title = String(issue.title || "");
const type = pickLine(body, "Type").toLowerCase();
const chemin = normalizeChemin(pickLine(body, "Chemin"));
const ancre = normalizeAnchorId(pickLine(body, "Ancre"));
assert(chemin, "Ticket: Chemin manquant.", 2);
assert(ancre && /^p-\d+-/i.test(ancre), `Ticket: Ancre invalide ("${ancre}")`, 2);
assert(type, "Ticket: Type manquant.", 2);
const pageKey = normalizePageKeyFromChemin(chemin);
assert(pageKey, "Ticket: impossible de dériver pageKey.", 2);
const paraOrder = DO_VERIFY ? await loadParaOrderFromDist(pageKey) : null;
if (DO_VERIFY) {
const ok = await tryVerifyAnchor(pageKey, ancre);
if (ok === false) {
throw Object.assign(new Error(`Ticket verify: ancre introuvable pour page "${pageKey}" => ${ancre}`), { __exitCode: 2 });
}
if (ok === null) {
if (STRICT) {
throw Object.assign(
new Error(`Ticket verify (strict): impossible de vérifier (pas de dist/para-index.json ou baseline)`),
{ __exitCode: 2 }
);
}
console.warn("⚠️ verify: impossible de vérifier (pas de dist/para-index.json ou baseline) — on continue.");
}
}
// ✅ shard path: src/annotations/<pageKey>/<paraId>.yml
const shardAbs = path.join(ANNO_DIR, ...pageKey.split("/"), `${ancre}.yml`);
const shardRel = path.relative(CWD, shardAbs).replace(/\\/g, "/");
// legacy monolith: src/annotations/<pageKey>.yml (read-only, for migration)
const legacyAbs = path.join(ANNO_DIR, `${pageKey}.yml`);
console.log("✅ Parsed:", { type, chemin, ancre: `#${ancre}`, pageKey, annoFile: shardRel });
// load shard doc
const doc = await loadAnnoDocYaml(shardAbs, pageKey);
if (!isPlainObject(doc.paras[ancre])) doc.paras[ancre] = {};
const entry = doc.paras[ancre];
// merge legacy entry into shard in-memory (non destructive) to keep compat + enable progressive migration
if (await exists(legacyAbs)) {
try {
const legacy = await loadAnnoDocYaml(legacyAbs, pageKey);
const legacyEntry = legacy?.paras?.[ancre];
if (isPlainObject(legacyEntry)) {
deepMergeEntry(entry, legacyEntry);
}
} catch {
// ignore legacy parse issues; shard still applies new data
}
}
const touchedFiles = [];
const notes = [];
let changed = false;
const nowIso = new Date().toISOString();
if (type === "type/comment") {
const comment = pickSection(body, ["Commentaire:", "Comment:", "Commentaires:"]) || "";
const text = comment.trim();
assert(text.length >= 3, "Ticket comment: bloc 'Commentaire:' introuvable ou trop court.", 2);
if (!Array.isArray(entry.comments_editorial)) entry.comments_editorial = [];
const item = { text, status: "new", ts: nowIso, fromIssue: issueNum };
const before = entry.comments_editorial.length;
entry.comments_editorial = uniqUnion(entry.comments_editorial, [item], keyComment);
if (entry.comments_editorial.length !== before) {
changed = true;
notes.push(`+ comment added (len=${text.length})`);
} else {
notes.push(`~ comment already present (dedup)`);
}
stableSortByTs(entry.comments_editorial);
}
else if (type === "type/reference") {
const ref = parseReferenceBlock(body);
assert(ref.url || ref.label, "Ticket reference: renseigne au moins - URL: ou - Label: dans le ticket.", 2);
if (STRICT && ref.url && !isHttpUrl(ref.url)) {
throw Object.assign(new Error(`Ticket reference (strict): URL invalide (http/https requis): "${ref.url}"`), { __exitCode: 2 });
}
if (!Array.isArray(entry.refs)) entry.refs = [];
const item = {
url: ref.url || "",
label: ref.label || (ref.url ? ref.url : "Référence"),
kind: ref.kind || "",
ts: nowIso,
fromIssue: issueNum,
};
if (ref.citation) item.citation = ref.citation;
const before = entry.refs.length;
entry.refs = uniqUnion(entry.refs, [item], keyRef);
if (entry.refs.length !== before) {
changed = true;
notes.push(`+ reference added (${item.url ? "url" : "label"})`);
} else {
notes.push(`~ reference already present (dedup)`);
}
stableSortByTs(entry.refs);
}
else if (type === "type/media") {
if (!Array.isArray(entry.media)) entry.media = [];
const caption = (title || "").trim();
if (STRICT && !caption) {
throw Object.assign(new Error("Ticket media (strict): caption vide (titre de ticket requis)."), { __exitCode: 2 });
}
const captionFinal = caption || ".";
const atts = NO_DOWNLOAD ? [] : await fetchIssueAssets({ forgeApiBase, owner, repo, token, issueNum });
if (!atts.length) notes.push("! no assets found (nothing to download).");
for (const a of atts) {
const name = sanitizeFilename(a?.name || `asset-${a?.id || "x"}`);
const dl = a?.browser_download_url || a?.download_url || "";
if (!dl) { notes.push(`! asset missing download url: ${name}`); continue; }
const mediaDirAbs = path.join(PUBLIC_DIR, "media", ...pageKey.split("/"), ancre);
const destAbs = path.join(mediaDirAbs, name);
const urlPath = `${MEDIA_URL_ROOT}/${pageKey}/${ancre}/${name}`.replace(/\/{2,}/g, "/");
if (await exists(destAbs)) {
notes.push(`~ media already exists: ${urlPath}`);
} else if (!DRY_RUN) {
const bytes = await downloadToFile(dl, token, destAbs);
notes.push(`+ downloaded ${name} (${bytes} bytes) -> ${urlPath}`);
touchedFiles.push(path.relative(CWD, destAbs).replace(/\\/g, "/"));
changed = true;
} else {
notes.push(`(dry) would download ${name} -> ${urlPath}`);
changed = true;
}
const item = {
type: inferMediaTypeFromFilename(name),
src: urlPath,
caption: captionFinal,
credit: "",
ts: nowIso,
fromIssue: issueNum,
};
const before = entry.media.length;
entry.media = uniqUnion(entry.media, [item], keyMedia);
if (entry.media.length !== before) changed = true;
}
stableSortByTs(entry.media);
}
else {
throw Object.assign(new Error(`Type non supporté: "${type}"`), { __exitCode: 2 });
}
if (!changed) {
console.log("ℹ️ No changes to apply.");
for (const n of notes) console.log(" ", n);
return;
}
if (DRY_RUN) {
console.log("\n--- DRY RUN (no write) ---");
console.log(`Would update: ${shardRel}`);
for (const n of notes) console.log(" ", n);
console.log("\nExcerpt (resulting entry):");
console.log(YAML.stringify({ [ancre]: doc.paras[ancre] }).trimEnd());
console.log("\n✅ Dry-run terminé.");
return;
}
await saveAnnoDocYaml(shardAbs, doc, paraOrder);
touchedFiles.unshift(shardRel);
console.log(`✅ Updated: ${shardRel}`);
for (const n of notes) console.log(" ", n);
if (DO_COMMIT) {
run("git", ["add", ...touchedFiles], { cwd: CWD });
if (!gitHasStagedChanges()) {
console.log("ℹ️ Nothing to commit (aucun changement staged).");
return;
}
const msg = `anno: apply ticket #${issueNum} (${pageKey}#${ancre} ${type})`;
run("git", ["commit", "-m", msg], { cwd: CWD });
const sha = runQuiet("git", ["rev-parse", "--short", "HEAD"], { cwd: CWD }).trim();
console.log(`✅ Committed: ${msg} (${sha})`);
if (DO_CLOSE) {
const comment = `✅ Appliqué par apply-annotation-ticket.\nCommit: ${sha}`;
await closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment });
console.log(`✅ Ticket #${issueNum} fermé.`);
}
} else {
console.log("\nNext (manuel) :");
console.log(` git diff -- ${touchedFiles[0]}`);
console.log(` git add ${touchedFiles.join(" ")}`);
console.log(` git commit -m "anno: apply ticket #${issueNum} (${pageKey}#${ancre} ${type})"`);
}
}
main().catch((e) => {
const code = e?.__exitCode || 1;
console.error("💥", e?.message || e);
process.exit(code);
});
+37 -9
View File
@@ -39,7 +39,7 @@ Env (recommandé):
Notes:
- Si dist/<chemin>/index.html est absent, le script lance "npm run build" sauf si --no-build.
- Sauvegarde automatique: <fichier>.bak.issue-<N> (uniquement si on écrit)
- Sauvegarde automatique: .tmp/apply-ticket/<fichier>.bak.issue-<N> (uniquement si on écrit)
- Avec --alias : le script rebuild pour identifier le NOUVEL id, puis écrit l'alias old->new.
- Refuse automatiquement les Pull Requests (PR) : ce ne sont pas des tickets éditoriaux.
`);
@@ -89,6 +89,7 @@ const CWD = process.cwd();
const CONTENT_ROOT = path.join(CWD, "src", "content");
const DIST_ROOT = path.join(CWD, "dist");
const ALIASES_FILE = path.join(CWD, "src", "anchors", "anchor-aliases.json");
const BACKUP_ROOT = path.join(CWD, ".tmp", "apply-ticket");
/* -------------------------- utils texte / matching -------------------------- */
@@ -158,6 +159,17 @@ function bestBlockMatchIndex(blocks, targetText) {
return best;
}
function rankedBlockMatches(blocks, targetText, limit = 5) {
return blocks
.map((b, i) => ({
i,
score: scoreText(b, targetText),
excerpt: stripMd(b).slice(0, 140),
}))
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
function splitParagraphBlocks(mdxText) {
const raw = String(mdxText ?? "").replace(/\r\n/g, "\n");
return raw.split(/\n{2,}/);
@@ -612,18 +624,15 @@ async function main() {
const original = await fs.readFile(contentFile, "utf-8");
const blocks = splitParagraphBlocks(original);
const best = bestBlockMatchIndex(blocks, targetText);
const ranked = rankedBlockMatches(blocks, targetText, 5);
const best = ranked[0] || { i: -1, score: -1, excerpt: "" };
const runnerUp = ranked[1] || null;
// seuil de sécurité
// seuil absolu
if (best.i < 0 || best.score < 40) {
console.error("❌ Match trop faible: je refuse de remplacer automatiquement.");
console.error(`➡️ Score=${best.score}. Recommandation: ticket avec 'Texte actuel (copie exacte du paragraphe)'.`);
const ranked = blocks
.map((b, i) => ({ i, score: scoreText(b, targetText), excerpt: stripMd(b).slice(0, 140) }))
.sort((a, b) => b.score - a.score)
.slice(0, 5);
console.error("Top candidates:");
for (const r of ranked) {
console.error(` #${r.i + 1} score=${r.score} ${r.excerpt}${r.excerpt.length >= 140 ? "…" : ""}`);
@@ -631,6 +640,20 @@ async function main() {
process.exit(2);
}
// seuil relatif : si le 2e est trop proche du 1er, on refuse aussi
if (runnerUp) {
const ambiguityGap = best.score - runnerUp.score;
if (ambiguityGap < 15) {
console.error("❌ Match ambigu: le meilleur candidat est trop proche du second.");
console.error(`➡️ best=${best.score} / second=${runnerUp.score} / gap=${ambiguityGap}`);
console.error("Top candidates:");
for (const r of ranked) {
console.error(` #${r.i + 1} score=${r.score} ${r.excerpt}${r.excerpt.length >= 140 ? "…" : ""}`);
}
process.exit(2);
}
}
const beforeBlock = blocks[best.i];
const afterBlock = proposition.trim();
@@ -651,7 +674,10 @@ async function main() {
}
// backup uniquement si on écrit
const bakPath = `${contentFile}.bak.issue-${issueNum}`;
const relContentFile = path.relative(CWD, contentFile);
const bakPath = path.join(BACKUP_ROOT, `${relContentFile}.bak.issue-${issueNum}`);
await fs.mkdir(path.dirname(bakPath), { recursive: true });
if (!(await fileExists(bakPath))) {
await fs.writeFile(bakPath, original, "utf-8");
}
@@ -684,6 +710,8 @@ async function main() {
}
// garde-fous rapides
run("node", ["scripts/check-anchor-aliases.mjs"], { cwd: CWD });
run("node", ["scripts/verify-anchor-aliases-in-dist.mjs"], { cwd: CWD });
run("npm", ["run", "test:anchors"], { cwd: CWD });
run("node", ["scripts/check-inline-js.mjs"], { cwd: CWD });
}
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env node
// scripts/build-annotations-index.mjs
// Construit dist/annotations-index.json à partir de src/annotations/**/*.yml
// Supporte:
// - monolith : src/annotations/<pageKey>.yml
// - shard : src/annotations/<pageKey>/<paraId>.yml (paraId = p-<n>-...)
// Invariants:
// - doc.schema === 1
// - doc.page (si présent) == pageKey déduit du chemin
// - shard: doc.paras doit contenir EXACTEMENT la clé paraId (sinon fail)
//
// Deep-merge non destructif (media/refs/comments dédupliqués), tri stable.
import fs from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";
const ROOT = process.cwd();
const ANNO_ROOT = path.join(ROOT, "src", "annotations");
const DIST_DIR = path.join(ROOT, "dist");
const OUT = path.join(DIST_DIR, "annotations-index.json");
function assert(cond, msg) {
if (!cond) throw new Error(msg);
}
function isObj(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function isArr(x) {
return Array.isArray(x);
}
function normPath(s) {
return String(s || "")
.replace(/\\/g, "/")
.replace(/^\/+|\/+$/g, "");
}
function paraNum(pid) {
const m = String(pid).match(/^p-(\d+)-/i);
return m ? Number(m[1]) : Number.POSITIVE_INFINITY;
}
function stableSortByTs(arr) {
if (!Array.isArray(arr)) return;
arr.sort((a, b) => {
const ta = Date.parse(a?.ts || "") || 0;
const tb = Date.parse(b?.ts || "") || 0;
if (ta !== tb) return ta - tb;
return JSON.stringify(a).localeCompare(JSON.stringify(b));
});
}
function keyMedia(x) { return String(x?.src || ""); }
function keyRef(x) {
return `${x?.url || ""}||${x?.label || ""}||${x?.kind || ""}||${x?.citation || ""}`;
}
function keyComment(x) { return String(x?.text || "").trim(); }
function uniqUnion(dst, src, keyFn) {
const out = isArr(dst) ? [...dst] : [];
const seen = new Set(out.map((x) => keyFn(x)));
for (const it of (isArr(src) ? src : [])) {
const k = keyFn(it);
if (!k) continue;
if (!seen.has(k)) {
seen.add(k);
out.push(it);
}
}
return out;
}
function deepMergeEntry(dst, src) {
if (!isObj(dst) || !isObj(src)) return;
for (const [k, v] of Object.entries(src)) {
if (k === "media" && isArr(v)) { dst.media = uniqUnion(dst.media, v, keyMedia); continue; }
if (k === "refs" && isArr(v)) { dst.refs = uniqUnion(dst.refs, v, keyRef); continue; }
if (k === "comments_editorial" && isArr(v)) { dst.comments_editorial = uniqUnion(dst.comments_editorial, v, keyComment); continue; }
if (isObj(v)) {
if (!isObj(dst[k])) dst[k] = {};
deepMergeEntry(dst[k], v);
continue;
}
if (isArr(v)) {
const cur = isArr(dst[k]) ? dst[k] : [];
const seen = new Set(cur.map((x) => JSON.stringify(x)));
const out = [...cur];
for (const it of v) {
const s = JSON.stringify(it);
if (!seen.has(s)) { seen.add(s); out.push(it); }
}
dst[k] = out;
continue;
}
// scalar: set only if missing/empty
if (!(k in dst) || dst[k] == null || dst[k] === "") dst[k] = v;
}
}
async function walk(dir) {
const out = [];
const ents = await fs.readdir(dir, { withFileTypes: true });
for (const e of ents) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...await walk(p));
else if (e.isFile() && /\.ya?ml$/i.test(e.name)) out.push(p);
}
return out;
}
function inferExpectedFromRel(relNoExt) {
const parts = relNoExt.split("/").filter(Boolean);
const last = parts.at(-1) || "";
const isShard = parts.length > 1 && /^p-\d+-/i.test(last); // ✅ durcissement
const pageKey = isShard ? parts.slice(0, -1).join("/") : relNoExt;
const paraId = isShard ? last : null;
return { isShard, pageKey, paraId };
}
function validateAndNormalizeDoc(doc, relFile, expectedPageKey, expectedParaId) {
assert(isObj(doc), `${relFile}: doc must be an object`);
assert(doc.schema === 1, `${relFile}: schema must be 1`);
assert(isObj(doc.paras), `${relFile}: missing object key "paras"`);
const gotPage = doc.page != null ? normPath(doc.page) : "";
const expPage = normPath(expectedPageKey);
if (gotPage) {
assert(
gotPage === expPage,
`${relFile}: page mismatch (page="${doc.page}" vs path="${expectedPageKey}")`
);
} else {
doc.page = expPage;
}
if (expectedParaId) {
const keys = Object.keys(doc.paras || {}).map(String);
assert(
keys.includes(expectedParaId),
`${relFile}: shard mismatch: must contain paras["${expectedParaId}"]`
);
assert(
keys.length === 1 && keys[0] === expectedParaId,
`${relFile}: shard invariant violated: shard file must contain ONLY paras["${expectedParaId}"] (got: ${keys.join(", ")})`
);
}
return doc;
}
async function main() {
const pages = {};
const errors = [];
await fs.mkdir(DIST_DIR, { recursive: true });
const files = await walk(ANNO_ROOT);
for (const fp of files) {
const rel = normPath(path.relative(ANNO_ROOT, fp));
const relNoExt = rel.replace(/\.ya?ml$/i, "");
const { isShard, pageKey, paraId } = inferExpectedFromRel(relNoExt);
try {
const raw = await fs.readFile(fp, "utf8");
const doc = YAML.parse(raw) || {};
if (!isObj(doc) || doc.schema !== 1) continue;
validateAndNormalizeDoc(
doc,
`src/annotations/${rel}`,
pageKey,
isShard ? paraId : null
);
const pg = (pages[pageKey] ??= { paras: {} });
if (isShard) {
const entry = doc.paras[paraId];
if (!isObj(pg.paras[paraId])) pg.paras[paraId] = {};
if (isObj(entry)) deepMergeEntry(pg.paras[paraId], entry);
stableSortByTs(pg.paras[paraId].media);
stableSortByTs(pg.paras[paraId].refs);
stableSortByTs(pg.paras[paraId].comments_editorial);
} else {
for (const [pid, entry] of Object.entries(doc.paras || {})) {
const p = String(pid);
if (!isObj(pg.paras[p])) pg.paras[p] = {};
if (isObj(entry)) deepMergeEntry(pg.paras[p], entry);
stableSortByTs(pg.paras[p].media);
stableSortByTs(pg.paras[p].refs);
stableSortByTs(pg.paras[p].comments_editorial);
}
}
} catch (e) {
errors.push({ file: `src/annotations/${rel}`, error: String(e?.message || e) });
}
}
for (const [pageKey, pg] of Object.entries(pages)) {
const keys = Object.keys(pg.paras || {});
keys.sort((a, b) => {
const ia = paraNum(a);
const ib = paraNum(b);
if (Number.isFinite(ia) && Number.isFinite(ib) && ia !== ib) return ia - ib;
return String(a).localeCompare(String(b));
});
const next = {};
for (const k of keys) next[k] = pg.paras[k];
pg.paras = next;
}
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
pages,
stats: {
pages: Object.keys(pages).length,
paras: Object.values(pages).reduce((n, p) => n + Object.keys(p.paras || {}).length, 0),
errors: errors.length,
},
errors,
};
if (errors.length) {
throw new Error(`${errors[0].file}: ${errors[0].error}`);
}
await fs.writeFile(OUT, JSON.stringify(out), "utf8");
console.log(`✅ annotations-index: pages=${out.stats.pages} paras=${out.stats.paras} -> dist/annotations-index.json`);
}
main().catch((e) => {
console.error(`FAIL: build-annotations-index crashed: ${e?.stack || e?.message || e}`);
process.exit(1);
});
+148
View File
@@ -0,0 +1,148 @@
// scripts/build-para-index.mjs
import fs from "node:fs/promises";
import path from "node:path";
function parseArgs(argv) {
const out = { inDir: "dist", outFile: "dist/para-index.json" };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--in" && argv[i + 1]) {
out.inDir = argv[++i];
continue;
}
if (a.startsWith("--in=")) {
out.inDir = a.slice("--in=".length);
continue;
}
if (a === "--out" && argv[i + 1]) {
out.outFile = argv[++i];
continue;
}
if (a.startsWith("--out=")) {
out.outFile = a.slice("--out=".length);
continue;
}
}
return out;
}
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function walk(dir) {
const out = [];
const ents = await fs.readdir(dir, { withFileTypes: true });
for (const e of ents) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...(await walk(p)));
else out.push(p);
}
return out;
}
function stripTags(html) {
return String(html || "")
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<[^>]+>/g, " ");
}
function decodeEntities(s) {
// minimal, volontairement (évite dépendances)
return String(s || "")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
}
function normalizeSpaces(s) {
return decodeEntities(s).replace(/\s+/g, " ").trim();
}
function relPageFromIndexHtml(inDirAbs, fileAbs) {
const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/");
if (!/index\.html$/i.test(rel)) return null;
// dist/<page>/index.html -> "/<page>/"
const page = "/" + rel.replace(/index\.html$/i, "");
return page;
}
async function main() {
const { inDir, outFile } = parseArgs(process.argv.slice(2));
const CWD = process.cwd();
const inDirAbs = path.isAbsolute(inDir) ? inDir : path.join(CWD, inDir);
const outAbs = path.isAbsolute(outFile) ? outFile : path.join(CWD, outFile);
// ✅ antifragile: si dist/ (ou inDir) absent -> on SKIP proprement
if (!(await exists(inDirAbs))) {
console.log(`ℹ️ para-index: skip (input missing): ${inDir}`);
process.exit(0);
}
const files = (await walk(inDirAbs)).filter((p) => /index\.html$/i.test(p));
if (!files.length) {
console.log(`ℹ️ para-index: skip (no index.html found in): ${inDir}`);
process.exit(0);
}
const items = [];
const byId = Object.create(null);
// <p ... id="p-...">...</p>
// (regex volontairement stricte sur l'id pour éviter faux positifs)
const reP = /<p\b([^>]*\bid\s*=\s*["'](p-\d+-[^"']+)["'][^>]*)>([\s\S]*?)<\/p>/gi;
for (const f of files) {
const page = relPageFromIndexHtml(inDirAbs, f);
if (!page) continue;
const html = await fs.readFile(f, "utf8");
let m;
while ((m = reP.exec(html))) {
const id = m[2];
const inner = m[3];
if (byId[id] != null) continue; // protège si jamais doublons
const text = normalizeSpaces(stripTags(inner));
if (!text) continue;
byId[id] = items.length;
items.push({ id, page, text });
}
}
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
items,
byId,
};
await fs.mkdir(path.dirname(outAbs), { recursive: true });
await fs.writeFile(outAbs, JSON.stringify(out), "utf8");
console.log(`✅ para-index: items=${items.length} -> ${path.relative(CWD, outAbs)}`);
}
main().catch((e) => {
console.error("FAIL: build-para-index crashed:", e);
process.exit(1);
});
+34 -3
View File
@@ -74,7 +74,24 @@ function loadAllowMissing() {
return new Set(arr.map(String));
}
function loadAcceptedResets() {
const p = path.resolve("config/anchor-churn-allowlist.json");
if (!fssync.existsSync(p)) return {};
const raw = fssync.readFileSync(p, "utf8").trim();
if (!raw) return {};
const data = JSON.parse(raw);
if (!data || typeof data !== "object" || Array.isArray(data)) {
throw new Error("anchor-churn-allowlist.json must be an object");
}
const accepted = data.accepted_resets || {};
if (!accepted || typeof accepted !== "object" || Array.isArray(accepted)) {
throw new Error("anchor-churn-allowlist.json: accepted_resets must be an object");
}
return accepted;
}
const ALLOW_MISSING = loadAllowMissing();
const ACCEPTED_RESETS = loadAcceptedResets();
async function buildSnapshot() {
const absDist = path.resolve(DIST_DIR);
@@ -139,6 +156,7 @@ function diffPage(prevIds, curIds) {
let failed = false;
let changedPages = 0;
let acceptedPages = 0;
for (const p of pages) {
const prevIds = base[p] || null;
@@ -172,6 +190,7 @@ function diffPage(prevIds, curIds) {
const prevN = prevIds.length || 1;
const churn = (added.length + removed.length) / prevN;
const removedRatio = removed.length / prevN;
const acceptedReason = ACCEPTED_RESETS[p] || null;
console.log(
`~ ${p} prev=${prevIds.length} now=${curIds.length}` +
@@ -182,11 +201,23 @@ function diffPage(prevIds, curIds) {
console.log(` removed: ${removed.slice(0, 20).join(", ")}${removed.length > 20 ? " …" : ""}`);
}
if (prevIds.length >= MIN_PREV && churn > THRESHOLD) failed = true;
if (prevIds.length >= MIN_PREV && removedRatio > THRESHOLD) failed = true;
const exceeds =
(prevIds.length >= MIN_PREV && churn > THRESHOLD) ||
(prevIds.length >= MIN_PREV && removedRatio > THRESHOLD);
if (exceeds && acceptedReason) {
acceptedPages += 1;
console.log(` ✅ accepted reset: ${acceptedReason}`);
continue;
}
if (exceeds) failed = true;
}
console.log(`\nSummary: pages compared=${pages.length}, pages changed=${changedPages}`);
console.log(
`\nSummary: pages compared=${pages.length}, pages changed=${changedPages}, accepted resets=${acceptedPages}`
);
if (failed) {
console.error(`FAIL: anchor churn above threshold (threshold=${pct(THRESHOLD)} minPrev=${MIN_PREV})`);
process.exit(1);
+104
View File
@@ -0,0 +1,104 @@
import fs from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";
const CWD = process.cwd();
const ANNO_DIR = path.join(CWD, "src", "annotations");
const PUBLIC_DIR = path.join(CWD, "public");
async function exists(p) {
try { await fs.access(p); return true; } catch { return false; }
}
async function walk(dir) {
const out = [];
const ents = await fs.readdir(dir, { withFileTypes: true });
for (const e of ents) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...(await walk(p)));
else out.push(p);
}
return out;
}
function parseDoc(raw, fileAbs) {
if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
return YAML.parse(raw);
}
function isPlainObject(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function toPublicPathFromUrl(urlPath) {
// "/media/..." -> "public/media/..."
const clean = String(urlPath || "").split("?")[0].split("#")[0];
if (!clean.startsWith("/media/")) return null;
return path.join(PUBLIC_DIR, clean.replace(/^\/+/, ""));
}
async function main() {
if (!(await exists(ANNO_DIR))) {
console.log("✅ annotations-media: aucun src/annotations — rien à vérifier.");
process.exit(0);
}
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
let checked = 0;
let missing = 0;
const notes = [];
// Optim: éviter de vérifier 100 fois le même fichier media
const seenMedia = new Set(); // src string
for (const f of files) {
const rel = path.relative(CWD, f).replace(/\\/g, "/");
const raw = await fs.readFile(f, "utf8");
let doc;
try { doc = parseDoc(raw, f); }
catch (e) {
missing++;
notes.push(`- PARSE FAIL: ${rel} (${String(e?.message ?? e)})`);
continue;
}
if (!isPlainObject(doc) || doc.schema !== 1 || !isPlainObject(doc.paras)) continue;
for (const [paraId, entry] of Object.entries(doc.paras)) {
const media = entry?.media;
if (!Array.isArray(media)) continue;
for (const m of media) {
const src = String(m?.src || "");
if (!src.startsWith("/media/")) continue; // externes ok, ou autres conventions futures
// dédupe
if (seenMedia.has(src)) continue;
seenMedia.add(src);
checked++;
const p = toPublicPathFromUrl(src);
if (!p) continue;
if (!(await exists(p))) {
missing++;
notes.push(`- MISSING MEDIA: ${src} (from ${rel} para ${paraId})`);
}
}
}
}
if (missing > 0) {
console.error(`FAIL: annotations media missing (checked=${checked} missing=${missing})`);
for (const n of notes) console.error(n);
process.exit(1);
}
console.log(`✅ annotations-media OK: checked=${checked}`);
}
main().catch((e) => {
console.error("FAIL: check-annotations-media crashed:", e);
process.exit(1);
});
+224
View File
@@ -0,0 +1,224 @@
// scripts/check-annotations.mjs
import fs from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";
const CWD = process.cwd();
const ANNO_DIR = path.join(CWD, "src", "annotations");
const DIST_DIR = path.join(CWD, "dist");
const ALIASES_PATH = path.join(CWD, "src", "anchors", "anchor-aliases.json");
async function exists(p) {
try { await fs.access(p); return true; } catch { return false; }
}
async function walk(dir) {
const out = [];
const ents = await fs.readdir(dir, { withFileTypes: true });
for (const e of ents) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...(await walk(p)));
else out.push(p);
}
return out;
}
function escRe(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function normalizePageKey(s) {
return String(s || "").replace(/^\/+/, "").replace(/\/+$/, "");
}
function isPlainObject(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function isParaId(s) {
return /^p-\d+-/i.test(String(s || ""));
}
/**
* Supporte:
* - monolith: src/annotations/<pageKey>.yml -> pageKey = rel sans ext
* - shard : src/annotations/<pageKey>/<paraId>.yml -> pageKey = dirname(rel), paraId = basename
*
* shard seulement si le fichier est dans un sous-dossier (anti cas pathologique).
*/
function inferFromFile(fileAbs) {
const rel = path.relative(ANNO_DIR, fileAbs).replace(/\\/g, "/");
const relNoExt = rel.replace(/\.(ya?ml|json)$/i, "");
const parts = relNoExt.split("/").filter(Boolean);
const base = parts[parts.length - 1] || "";
const dirParts = parts.slice(0, -1);
const isShard = dirParts.length > 0 && isParaId(base);
const pageKey = isShard ? dirParts.join("/") : relNoExt;
const paraId = isShard ? base : "";
return { pageKey: normalizePageKey(pageKey), paraId };
}
async function loadAliases() {
if (!(await exists(ALIASES_PATH))) return {};
try {
const raw = await fs.readFile(ALIASES_PATH, "utf8");
const json = JSON.parse(raw);
return isPlainObject(json) ? json : {};
} catch {
return {};
}
}
function parseDoc(raw, fileAbs) {
if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
return YAML.parse(raw);
}
function getAlias(aliases, pageKey, oldId) {
// supporte:
// 1) { "<pageKey>": { "<old>": "<new>" } }
// 2) { "<old>": "<new>" }
const k1 = String(pageKey || "");
const k2 = k1 ? ("/" + k1.replace(/^\/+|\/+$/g, "") + "/") : "";
const a1 = (aliases?.[k1]?.[oldId]) || (k2 ? aliases?.[k2]?.[oldId] : "");
if (a1) return String(a1);
const a2 = aliases?.[oldId];
if (a2) return String(a2);
return "";
}
async function main() {
if (!(await exists(ANNO_DIR))) {
console.log("✅ annotations: aucun dossier src/annotations — rien à vérifier.");
process.exit(0);
}
if (!(await exists(DIST_DIR))) {
console.error("FAIL: dist/ absent. Lance d’abord `npm run build` (ou `npm test`).");
process.exit(1);
}
const aliases = await loadAliases();
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
// perf: cache HTML par page (shards = beaucoup de fichiers pour 1 page)
const htmlCache = new Map(); // pageKey -> html
const missingDistPage = new Set(); // pageKey
let pagesSeen = new Set();
let checked = 0;
let failures = 0;
const notes = [];
for (const f of files) {
const rel = path.relative(CWD, f).replace(/\\/g, "/");
const raw = await fs.readFile(f, "utf8");
let doc;
try {
doc = parseDoc(raw, f);
} catch (e) {
failures++;
notes.push(`- PARSE FAIL: ${rel} (${String(e?.message ?? e)})`);
continue;
}
if (!isPlainObject(doc) || doc.schema !== 1) {
failures++;
notes.push(`- INVALID: ${rel} (schema must be 1)`);
continue;
}
const { pageKey, paraId: shardParaId } = inferFromFile(f);
if (doc.page != null && normalizePageKey(doc.page) !== pageKey) {
failures++;
notes.push(`- PAGE MISMATCH: ${rel} (page="${doc.page}" != path="${pageKey}")`);
continue;
}
if (!isPlainObject(doc.paras)) {
failures++;
notes.push(`- INVALID: ${rel} (missing object key "paras")`);
continue;
}
// shard invariant (fort) : doit contenir paras[paraId]
if (shardParaId) {
if (!Object.prototype.hasOwnProperty.call(doc.paras, shardParaId)) {
failures++;
notes.push(`- SHARD MISMATCH: ${rel} (expected paras["${shardParaId}"] present)`);
continue;
}
// si extras -> warning (non destructif)
const keys = Object.keys(doc.paras);
if (!(keys.length === 1 && keys[0] === shardParaId)) {
notes.push(`- WARN shard has extra paras: ${rel} (expected only "${shardParaId}", got ${keys.join(", ")})`);
}
}
pagesSeen.add(pageKey);
const distFile = path.join(DIST_DIR, pageKey, "index.html");
if (!(await exists(distFile))) {
if (!missingDistPage.has(pageKey)) {
missingDistPage.add(pageKey);
failures++;
notes.push(`- MISSING PAGE: dist/${pageKey}/index.html (from ${rel})`);
} else {
notes.push(`- WARN missing page already reported: dist/${pageKey}/index.html (from ${rel})`);
}
continue;
}
let html = htmlCache.get(pageKey);
if (!html) {
html = await fs.readFile(distFile, "utf8");
htmlCache.set(pageKey, html);
}
for (const paraId of Object.keys(doc.paras)) {
checked++;
if (!isParaId(paraId)) {
failures++;
notes.push(`- INVALID ID: ${rel} (${paraId})`);
continue;
}
const re = new RegExp(`\\bid=["']${escRe(paraId)}["']`, "g");
if (re.test(html)) continue;
const alias = getAlias(aliases, pageKey, paraId);
if (alias) {
const re2 = new RegExp(`\\bid=["']${escRe(alias)}["']`, "g");
if (re2.test(html)) {
notes.push(`- WARN alias used: ${pageKey} ${paraId} -> ${alias}`);
continue;
}
}
failures++;
notes.push(`- MISSING ID: ${pageKey} (#${paraId})`);
}
}
const warns = notes.filter((x) => x.startsWith("- WARN"));
const pages = pagesSeen.size;
if (failures > 0) {
console.error(`FAIL: annotations invalid (pages=${pages} checked=${checked} failures=${failures})`);
for (const n of notes) console.error(n);
process.exit(1);
}
for (const w of warns) console.log(w);
console.log(`✅ annotations OK: pages=${pages} checked=${checked} warnings=${warns.length}`);
}
main().catch((e) => {
console.error("FAIL: annotations check crashed:", e);
process.exit(1);
});
+35 -12
View File
@@ -114,7 +114,6 @@ async function runMammoth(docxPath, assetsOutDirWebRoot) {
);
let html = result.value || "";
// Mammoth gives relative src="image-xx.png" ; we will prefix later
return html;
}
@@ -182,6 +181,25 @@ async function exists(p) {
try { await fs.access(p); return true; } catch { return false; }
}
/**
* ✅ compat:
* - ancien : collection="archicratie" + slug="archicrat-ia/chapitre-3"
* - nouveau : collection="archicrat-ia" + slug="chapitre-3"
*
* But : toujours écrire dans src/content/archicrat-ia/<slugSansPrefix>.mdx
*/
function normalizeDest(collection, slug) {
let outCollection = String(collection || "").trim();
let outSlug = String(slug || "").trim().replace(/^\/+|\/+$/g, "");
if (outCollection === "archicratie" && outSlug.startsWith("archicrat-ia/")) {
outCollection = "archicrat-ia";
outSlug = outSlug.replace(/^archicrat-ia\//, "");
}
return { outCollection, outSlug };
}
async function main() {
const args = parseArgs(process.argv);
const manifestPath = path.resolve(args.manifest);
@@ -203,11 +221,14 @@ async function main() {
for (const it of selected) {
const docxPath = path.resolve(it.source);
const outFile = path.resolve("src/content", it.collection, `${it.slug}.mdx`);
const { outCollection, outSlug } = normalizeDest(it.collection, it.slug);
const outFile = path.resolve("src/content", outCollection, `${outSlug}.mdx`);
const outDir = path.dirname(outFile);
const assetsPublicDir = path.posix.join("/imported", it.collection, it.slug);
const assetsDiskDir = path.resolve("public", "imported", it.collection, it.slug);
const assetsPublicDir = path.posix.join("/imported", outCollection, outSlug);
const assetsDiskDir = path.resolve("public", "imported", outCollection, outSlug);
if (!(await exists(docxPath))) {
throw new Error(`Missing source docx: ${docxPath}`);
@@ -241,18 +262,20 @@ async function main() {
html = rewriteLocalImageLinks(html, assetsPublicDir);
body = html.trim() ? html : "<p>(Import vide)</p>";
}
const defaultVersion = process.env.PUBLIC_RELEASE || "0.1.0";
// ✅ IMPORTANT: archicrat-ia partage edition/status avec archicratie (pas de migration frontmatter)
const schemaDefaultsByCollection = {
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
ia: { edition: "ia", status: "cas_pratique", level: 1 },
traite: { edition: "traite", status: "ontodynamique", level: 1 },
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
atlas: { edition: "atlas", status: "atlas", level: 1 },
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
"archicrat-ia": { edition: "archicrat-ia", status: "essai_these", level: 1 },
"cas-ia": { edition: "cas-ia", status: "application", level: 1 },
traite: { edition: "traite", status: "ontodynamique", level: 1 },
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
atlas: { edition: "atlas", status: "atlas", level: 1 },
};
const defaults = schemaDefaultsByCollection[it.collection] || { edition: it.collection, status: "draft", level: 1 };
const defaults = schemaDefaultsByCollection[outCollection] || { edition: outCollection, status: "draft", level: 1 };
const fm = [
"---",
@@ -282,4 +305,4 @@ async function main() {
main().catch((e) => {
console.error("\nERROR:", e?.message || e);
process.exit(1);
});
});
+34 -50
View File
@@ -14,6 +14,24 @@ const STRICT = argv.includes("--strict") || process.env.CI === "1" || process.en
function escRe(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
function normalizeRoute(route) {
let r = String(route || "").trim();
if (!r.startsWith("/")) r = "/" + r;
if (!r.endsWith("/")) r = r + "/";
r = r.replace(/\/{2,}/g, "/");
return r;
}
function countIdAttr(html, id) {
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "gi");
let c = 0;
@@ -22,7 +40,6 @@ function countIdAttr(html, id) {
}
function findStartTagWithId(html, id) {
// 1er élément qui porte id="..."
const re = new RegExp(
`<([a-zA-Z0-9:-]+)\\b[^>]*\\bid=(["'])${escRe(id)}\\2[^>]*>`,
"i"
@@ -36,34 +53,10 @@ function isInjectedAliasSpan(html, id) {
const found = findStartTagWithId(html, id);
if (!found) return false;
if (found.tagName !== "span") return false;
// class="... para-alias ..."
return /\bclass=(["'])(?:(?!\1).)*\bpara-alias\b(?:(?!\1).)*\1/i.test(found.tag);
}
function normalizeRoute(route) {
let r = String(route || "").trim();
if (!r.startsWith("/")) r = "/" + r;
if (!r.endsWith("/")) r = r + "/";
r = r.replace(/\/{2,}/g, "/");
return r;
}
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
function hasId(html, id) {
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "i");
return re.test(html);
}
function injectBeforeId(html, newId, injectHtml) {
// insère juste avant la balise qui porte id="newId"
const re = new RegExp(
`(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`,
"i"
@@ -82,6 +75,7 @@ async function main() {
}
const raw = await fs.readFile(ALIASES_PATH, "utf-8");
/** @type {Record<string, Record<string,string>>} */
let aliases;
try {
@@ -89,6 +83,7 @@ async function main() {
} catch (e) {
throw new Error(`JSON invalide: ${ALIASES_PATH} (${e?.message || e})`);
}
if (!aliases || typeof aliases !== "object" || Array.isArray(aliases)) {
throw new Error(`Format invalide: attendu { route: { oldId: newId } } dans ${ALIASES_PATH}`);
}
@@ -114,10 +109,10 @@ async function main() {
console.log(msg);
warnCount++;
}
if (entries.length === 0) continue;
const rel = route.replace(/^\/+|\/+$/g, ""); // sans slash
const rel = route.replace(/^\/+|\/+$/g, "");
const htmlPath = path.join(DIST_ROOT, rel, "index.html");
if (!(await exists(htmlPath))) {
@@ -135,24 +130,8 @@ async function main() {
if (!oldId || !newId) continue;
const oldCount = countIdAttr(html, oldId);
if (oldCount > 0) {
// ✅ déjà injecté (idempotent)
if (isInjectedAliasSpan(html, oldId)) continue;
// ⛔️ oldId existe déjà "en vrai" (ex: <p id="oldId">)
// => alias inutile / inversé / obsolète
const found = findStartTagWithId(html, oldId);
const where = found ? `<${found.tagName} … id="${oldId}" …>` : `id="${oldId}"`;
const msg =
`⚠️ alias inutile/inversé: oldId déjà présent dans la page (${where}). ` +
`Supprime l'alias ${oldId} -> ${newId} (ou corrige le sens) pour route=${route}`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
continue;
}
// juste après avoir calculé oldCount
// ✅ déjà injecté => idempotent
if (oldCount > 0 && isInjectedAliasSpan(html, oldId)) {
if (STRICT && oldCount !== 1) {
throw new Error(`oldId dupliqué (${oldCount}) alors qu'il est censé être unique: ${route} id=${oldId}`);
@@ -160,18 +139,23 @@ async function main() {
continue;
}
// avant l'injection, après hasId(newId)
const newCount = countIdAttr(html, newId);
if (newCount !== 1) {
const msg = `⚠️ newId non-unique (${newCount}) : ${route} new=${newId} (injection ambiguë)`;
// ⛔️ oldId existe déjà "en vrai" => alias inutile/inversé
if (oldCount > 0) {
const found = findStartTagWithId(html, oldId);
const where = found ? `<${found.tagName} … id="${oldId}" …>` : `id="${oldId}"`;
const msg =
`⚠️ alias inutile/inversé: oldId déjà présent (${where}). ` +
`Supprime ${oldId} -> ${newId} (ou corrige le sens) pour route=${route}`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
continue;
}
if (!hasId(html, newId)) {
const msg = `⚠️ newId introuvable: ${route} old=${oldId} -> new=${newId}`;
// newId doit exister UNE fois (sinon injection ambiguë)
const newCount = countIdAttr(html, newId);
if (newCount !== 1) {
const msg = `⚠️ newId non-unique (${newCount}) : ${route} new=${newId} (injection ambiguë)`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env node
import process from "node:process";
function getEnv(name, fallback = "") {
return String(process.env[name] ?? fallback).trim();
}
function sh(value) {
return JSON.stringify(String(value ?? ""));
}
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function pickLine(body, key) {
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:\\s*([^\\n\\r]+)`, "mi");
const m = String(body || "").match(re);
return m ? m[1].trim() : "";
}
function pickHeadingValue(body, headingKey) {
const re = new RegExp(
`^##\\s*${escapeRegExp(headingKey)}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|\\n\\s*$)`,
"mi"
);
const m = String(body || "").match(re);
if (!m) return "";
const lines = m[1].split(/\r?\n/).map((l) => l.trim());
for (const l of lines) {
if (!l) continue;
if (l.startsWith("<!--")) continue;
return l.replace(/^\/?/, "/").trim();
}
return "";
}
function normalizeChemin(chemin) {
let c = String(chemin || "").trim();
if (!c) return "";
if (!c.startsWith("/")) c = "/" + c;
if (!c.endsWith("/")) c += "/";
return c;
}
function extractCheminFromAnyUrl(text) {
const s = String(text || "");
const m = s.match(/(\/[a-z0-9\-]+\/[a-z0-9\-\/]+\/)#p-\d+-[0-9a-f]{8}/i);
return m ? m[1] : "";
}
function inferType(issue) {
const title = String(issue?.title || "");
const body = String(issue?.body || "").replace(/\r\n/g, "\n");
const fromBody = String(pickLine(body, "Type") || "").trim().toLowerCase();
if (fromBody) return fromBody;
if (title.startsWith("[Correction]")) return "type/correction";
if (title.startsWith("[Fact-check]") || title.startsWith("[Vérification]")) return "type/fact-check";
return "";
}
function inferChemin(issue) {
const title = String(issue?.title || "");
const body = String(issue?.body || "").replace(/\r\n/g, "\n");
return normalizeChemin(
pickLine(body, "Chemin") ||
pickHeadingValue(body, "Chemin") ||
extractCheminFromAnyUrl(body) ||
extractCheminFromAnyUrl(title)
);
}
function labelsOf(issue) {
return Array.isArray(issue?.labels)
? issue.labels.map((l) => String(l?.name || "")).filter(Boolean)
: [];
}
function issueNumber(issue) {
return Number(issue?.number || issue?.index || 0);
}
function parseMeta(issue) {
const labels = labelsOf(issue);
const type = inferType(issue);
const chemin = inferChemin(issue);
const number = issueNumber(issue);
const hasApproved = labels.includes("state/approved");
const hasRejected = labels.includes("state/rejected");
const isProposer = type === "type/correction" || type === "type/fact-check";
const isOpen = String(issue?.state || "open") === "open";
const isPR = Boolean(issue?.pull_request);
const eligible =
number > 0 &&
isOpen &&
!isPR &&
hasApproved &&
!hasRejected &&
isProposer &&
Boolean(chemin);
return {
issue,
number,
type,
chemin,
labels,
hasApproved,
hasRejected,
eligible,
};
}
async function fetchJson(url, token) {
const res = await fetch(url, {
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"User-Agent": "archicratie-pick-proposer-issue/1.0",
},
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} ${url}\n${t}`);
}
return await res.json();
}
async function fetchIssue(apiBase, owner, repo, token, n) {
const url = `${apiBase}/api/v1/repos/${owner}/${repo}/issues/${n}`;
return await fetchJson(url, token);
}
async function listOpenIssues(apiBase, owner, repo, token) {
const out = [];
let page = 1;
const limit = 100;
while (true) {
const url = `${apiBase}/api/v1/repos/${owner}/${repo}/issues?state=open&page=${page}&limit=${limit}`;
const batch = await fetchJson(url, token);
if (!Array.isArray(batch) || batch.length === 0) break;
out.push(...batch);
if (batch.length < limit) break;
page += 1;
}
return out;
}
function emitNone(reason) {
process.stdout.write(
[
`TARGET_FOUND="0"`,
`TARGET_REASON=${sh(reason)}`,
`TARGET_PRIMARY_ISSUE=""`,
`TARGET_ISSUES=""`,
`TARGET_COUNT="0"`,
`TARGET_CHEMIN=""`,
].join("\n") + "\n"
);
}
async function main() {
const token = getEnv("FORGE_TOKEN");
const owner = getEnv("GITEA_OWNER");
const repo = getEnv("GITEA_REPO");
const apiBase = (getEnv("FORGE_API") || getEnv("FORGE_BASE")).replace(/\/+$/, "");
const explicit = Number(process.argv[2] || 0);
if (!token) throw new Error("Missing FORGE_TOKEN");
if (!owner || !repo) throw new Error("Missing GITEA_OWNER / GITEA_REPO");
if (!apiBase) throw new Error("Missing FORGE_API / FORGE_BASE");
let metas = [];
if (explicit > 0) {
const issue = await fetchIssue(apiBase, owner, repo, token, explicit);
const meta = parseMeta(issue);
if (!meta.eligible) {
emitNone(
!meta.hasApproved
? "explicit_issue_not_approved"
: meta.hasRejected
? "explicit_issue_rejected"
: !meta.type
? "explicit_issue_missing_type"
: !meta.chemin
? "explicit_issue_missing_chemin"
: "explicit_issue_not_eligible"
);
return;
}
const openIssues = await listOpenIssues(apiBase, owner, repo, token);
metas = openIssues.map(parseMeta).filter((m) => m.eligible && m.chemin === meta.chemin);
} else {
const openIssues = await listOpenIssues(apiBase, owner, repo, token);
metas = openIssues.map(parseMeta).filter((m) => m.eligible);
if (metas.length === 0) {
emitNone("no_open_approved_proposer_issue");
return;
}
metas.sort((a, b) => a.number - b.number);
const first = metas[0];
metas = metas.filter((m) => m.chemin === first.chemin);
}
metas.sort((a, b) => a.number - b.number);
if (metas.length === 0) {
emitNone("no_batch_for_path");
return;
}
const primary = metas[0];
const issues = metas.map((m) => String(m.number));
process.stdout.write(
[
`TARGET_FOUND="1"`,
`TARGET_REASON="ok"`,
`TARGET_PRIMARY_ISSUE=${sh(primary.number)}`,
`TARGET_ISSUES=${sh(issues.join(" "))}`,
`TARGET_COUNT=${sh(issues.length)}`,
`TARGET_CHEMIN=${sh(primary.chemin)}`,
].join("\n") + "\n"
);
}
main().catch((e) => {
console.error("💥 pick-proposer-issue:", e?.message || e);
process.exit(1);
});
+31
View File
@@ -0,0 +1,31 @@
// scripts/purge-dist-dev-whoami.mjs
import fs from "node:fs/promises";
import path from "node:path";
const CWD = process.cwd();
const targetDir = path.join(CWD, "dist", "_auth", "whoami");
const targetIndex = path.join(CWD, "dist", "_auth", "whoami", "index.html");
// Purge idempotente (force=true => pas d'erreur si absent)
async function rmSafe(p) {
try {
await fs.rm(p, { recursive: true, force: true });
return true;
} catch {
return false;
}
}
async function main() {
const removedIndex = await rmSafe(targetIndex);
const removedDir = await rmSafe(targetDir);
// Optionnel: si dist/_auth devient vide, on laisse tel quel (pas besoin de toucher)
const any = removedIndex || removedDir;
console.log(`✅ purge-dist-dev-whoami: ${any ? "purged" : "nothing to purge"}`);
}
main().catch((e) => {
console.error("❌ purge-dist-dev-whoami failed:", e);
process.exit(1);
});
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* seed-gitea-labels — crée les labels attendus (idempotent)
*
* Usage:
* FORGE_TOKEN=... FORGE_API=http://192.168.1.20:3000 node scripts/seed-gitea-labels.mjs
* (ou FORGE_BASE=https://gitea... si pas de FORGE_API)
*
* Optionnel:
* GITEA_OWNER / GITEA_REPO (sinon auto-détecté via git remote origin)
*/
import { spawnSync } from "node:child_process";
function getEnv(name, fallback = "") {
return (process.env[name] ?? fallback).trim();
}
function inferOwnerRepoFromGit() {
const r = spawnSync("git", ["remote", "get-url", "origin"], { encoding: "utf-8" });
if (r.status !== 0) return null;
const u = (r.stdout || "").trim();
const m = u.match(/[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/);
if (!m?.groups) return null;
return { owner: m.groups.owner, repo: m.groups.repo };
}
async function apiReq(base, token, method, path, payload = null) {
const url = `${base.replace(/\/+$/, "")}/api/v1${path}`;
const headers = {
Authorization: `token ${token}`,
Accept: "application/json",
"User-Agent": "archicratie-seed-labels/1.0",
};
const init = { method, headers };
if (payload != null) {
init.headers["Content-Type"] = "application/json";
init.body = JSON.stringify(payload);
}
const res = await fetch(url, init);
const text = await res.text().catch(() => "");
let json = null;
try { json = text ? JSON.parse(text) : null; } catch {}
if (!res.ok) throw new Error(`HTTP ${res.status} ${method} ${url}\n${text}`);
return json;
}
async function main() {
const token = getEnv("FORGE_TOKEN");
if (!token) throw new Error("FORGE_TOKEN manquant");
const inferred = inferOwnerRepoFromGit() || {};
const owner = getEnv("GITEA_OWNER", inferred.owner || "");
const repo = getEnv("GITEA_REPO", inferred.repo || "");
if (!owner || !repo) throw new Error("Impossible de déterminer owner/repo (GITEA_OWNER/GITEA_REPO ou git remote)");
const base = getEnv("FORGE_API") || getEnv("FORGE_BASE");
if (!base) throw new Error("FORGE_API ou FORGE_BASE manquant");
const wanted = [
// type/*
{ name: "type/comment", color: "1d76db", description: "Commentaire éditorial (site)" },
{ name: "type/media", color: "1d76db", description: "Media à intégrer (image/audio/video)" },
{ name: "type/correction", color: "1d76db", description: "Correction proposée" },
{ name: "type/fact-check", color: "1d76db", description: "Vérification / sourçage" },
// state/*
{ name: "state/a-trier", color: "0e8a16", description: "À trier" },
{ name: "state/recevable", color: "0e8a16", description: "Recevable" },
{ name: "state/a-sourcer", color: "0e8a16", description: "À sourcer" },
// scope/*
{ name: "scope/readers", color: "5319e7", description: "Signalé par lecteur" },
{ name: "scope/editors", color: "5319e7", description: "Signalé par éditeur" },
];
const labels = (await apiReq(base, token, "GET", `/repos/${owner}/${repo}/labels?limit=1000`)) || [];
const existing = new Set(labels.map((x) => x?.name).filter(Boolean));
let created = 0;
for (const L of wanted) {
if (existing.has(L.name)) continue;
await apiReq(base, token, "POST", `/repos/${owner}/${repo}/labels`, {
name: L.name,
color: L.color,
description: L.description,
});
created++;
console.log("✅ created:", L.name);
}
if (created === 0) console.log("ℹ️ seed: nothing to do (all labels already exist)");
else console.log(`✅ seed done: created=${created}`);
}
main().catch((e) => {
console.error("💥 seed-gitea-labels:", e?.message || e);
process.exit(1);
});
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
# switch-archicratie.sh — SAFE switch LIVE + STAGING (avec backups horodatés)
#
# Usage (NAS recommandé) :
# sudo bash -c 'LIVE_PORT=8081 /volume2/docker/archicratie-web/current/scripts/switch-archicratie.sh'
# sudo bash -c 'LIVE_PORT=8082 /volume2/docker/archicratie-web/current/scripts/switch-archicratie.sh'
#
# Usage (test local R&D, sans NAS) :
# D=/tmp/dynamic-test LIVE_PORT=8081 bash scripts/switch-archicratie.sh --dry-run
# D=/tmp/dynamic-test LIVE_PORT=8081 bash scripts/switch-archicratie.sh
usage() {
cat <<'EOF'
SAFE switch LIVE + STAGING (avec backups horodatés).
Variables / options :
LIVE_PORT=8081|8082 (obligatoire) port LIVE cible
D=/volume2/docker/edge/config/dynamic (optionnel) dossier des yml Traefik dynamiques
--dry-run n'écrit rien, affiche seulement ce qui serait fait
-h, --help aide
Exemples :
sudo bash -c 'LIVE_PORT=8082 /volume2/docker/archicratie-web/current/scripts/switch-archicratie.sh'
D=/tmp/dynamic-test LIVE_PORT=8081 bash scripts/switch-archicratie.sh --dry-run
EOF
}
DRY_RUN=0
for arg in "${@:-}"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) ;;
esac
done
D="${D:-/volume2/docker/edge/config/dynamic}"
F_LIVE="$D/20-archicratie-backend.yml"
F_STAG="$D/21-archicratie-staging.yml"
LIVE_PORT="${LIVE_PORT:-}"
if [[ "$LIVE_PORT" != "8081" && "$LIVE_PORT" != "8082" ]]; then
echo "❌ LIVE_PORT doit valoir 8081 ou 8082."
usage
exit 1
fi
if [[ ! -f "$F_LIVE" || ! -f "$F_STAG" ]]; then
echo "❌ Fichiers manquants :"
echo " $F_LIVE"
echo " $F_STAG"
echo " (Astuce R&D locale : mets D=/tmp/dynamic-test et crée 20/21 dedans.)"
exit 1
fi
OTHER_PORT="8081"
[[ "$LIVE_PORT" == "8081" ]] && OTHER_PORT="8082"
show_urls() {
local f="$1"
echo "— $f"
grep -nE '^\s*-\s*url:\s*".*"' "$f" || true
}
# Garde-fou : on attend au moins un "url:" dans chaque fichier
grep -qE '^\s*-\s*url:\s*"' "$F_LIVE" || { echo "❌ Format inattendu dans $F_LIVE (pas de - url: \")"; exit 1; }
grep -qE '^\s*-\s*url:\s*"' "$F_STAG" || { echo "❌ Format inattendu dans $F_STAG (pas de - url: \")"; exit 1; }
echo "Avant :"
show_urls "$F_LIVE"
show_urls "$F_STAG"
echo
echo "Plan : LIVE -> $LIVE_PORT ; STAGING -> $OTHER_PORT"
echo
if [[ "$DRY_RUN" == "1" ]]; then
echo "DRY-RUN : aucune écriture."
exit 0
fi
TS="$(date +%F-%H%M%S)"
cp -a "$F_LIVE" "$F_LIVE.bak.$TS"
cp -a "$F_STAG" "$F_STAG.bak.$TS"
# sed inplace portable (macOS vs Linux/DSM)
sed_inplace() {
local expr="$1" file="$2"
if [[ "$(uname -s)" == "Darwin" ]]; then
sed -i '' -e "$expr" "$file"
else
sed -i -e "$expr" "$file"
fi
}
# Remplacement ciblé UNIQUEMENT sur la ligne - url: "http://127.0.0.1:808X"
sed_inplace \
"s#^\([[:space:]]*-[[:space:]]*url:[[:space:]]*\"http://127\\.0\\.0\\.1:\\)808[12]\\(\"[[:space:]]*\)#\\1${LIVE_PORT}\\2#g" \
"$F_LIVE"
sed_inplace \
"s#^\([[:space:]]*-[[:space:]]*url:[[:space:]]*\"http://127\\.0\\.0\\.1:\\)808[12]\\(\"[[:space:]]*\)#\\1${OTHER_PORT}\\2#g" \
"$F_STAG"
# Post-check : on confirme que les fichiers contiennent bien les ports attendus
grep -qE "http://127\.0\.0\.1:${LIVE_PORT}\"" "$F_LIVE" || {
echo "❌ Post-check FAIL : $F_LIVE ne contient pas http://127.0.0.1:${LIVE_PORT}"
echo "➡️ rollback backups : $F_LIVE.bak.$TS / $F_STAG.bak.$TS"
exit 1
}
grep -qE "http://127\.0\.0\.1:${OTHER_PORT}\"" "$F_STAG" || {
echo "❌ Post-check FAIL : $F_STAG ne contient pas http://127.0.0.1:${OTHER_PORT}"
echo "➡️ rollback backups : $F_LIVE.bak.$TS / $F_STAG.bak.$TS"
exit 1
}
echo "✅ OK. Backups :"
echo " - $F_LIVE.bak.$TS"
echo " - $F_STAG.bak.$TS"
echo
echo "Après :"
show_urls "$F_LIVE"
show_urls "$F_STAG"
echo
echo "Smoke tests :"
echo " curl -sS -I http://127.0.0.1:${LIVE_PORT}/ | head -n 12"
echo " curl -sS -I http://127.0.0.1:${OTHER_PORT}/ | head -n 12"
echo " curl -sS -I -H 'Host: archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ | head -n 20"
echo " curl -sS -I -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ | head -n 20"
+1 -1
View File
@@ -205,7 +205,7 @@ for (const [route, mapping] of Object.entries(data)) {
newId,
htmlPath,
msg:
`oldId present but is NOT an injected alias span (<span class="para-alias">).</n` +
`oldId present but is NOT an injected alias span (<span class="para-alias">).\n` +
`Saw: ${seen}`,
});
continue;
+26
View File
@@ -0,0 +1,26 @@
import fs from "node:fs/promises";
import path from "node:path";
const OUT = path.join(process.cwd(), "public", "_auth", "whoami");
const groupsRaw = process.env.PUBLIC_WHOAMI_GROUPS ?? "editors";
const user = process.env.PUBLIC_WHOAMI_USER ?? "dev";
const name = process.env.PUBLIC_WHOAMI_NAME ?? "Dev Local";
const email = process.env.PUBLIC_WHOAMI_EMAIL ?? "area.technik@proton.me";
const groups = groupsRaw
.split(/[;,]/)
.map((s) => s.trim())
.filter(Boolean)
.join(",");
const body =
`Remote-User: ${user}\n` +
`Remote-Name: ${name}\n` +
`Remote-Email: ${email}\n` +
`Remote-Groups: ${groups}\n`;
await fs.mkdir(path.dirname(OUT), { recursive: true });
await fs.writeFile(OUT, body, "utf8");
console.log(`✅ dev whoami written: ${path.relative(process.cwd(), OUT)} (${groups})`);
Binary file not shown.
+51 -89
View File
@@ -1,6 +1,15 @@
version: 1
docs:
# =========================
# Document d’entrée
# =========================
- source: sources/docx/commencer/document-de-presentation.docx
collection: commencer
slug: document-de-presentation
title: "Document de présentation"
order: 0
# =========================
# Archicratie — Essai-thèse "ArchiCraT-IA"
# =========================
@@ -47,115 +56,68 @@ docs:
order: 70
# =========================
# IA — Cas pratique (1 page = 1 chapitre)
# NOTE: on n'inclut PAS le monolithe "Cas_IA-... .docx" dans le manifeste.
# Cas pratique — Gouvernance des systèmes IA
# =========================
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Introduction_generale—Mettre_en_scene_un_systeme_IA.docx
collection: ia
slug: cas-pratique/introduction
title: "Cas pratique — Introduction générale : Mettre en scène un système IA"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Introduction.docx
collection: cas-ia
slug: introduction
title: "Introduction générale — Mettre un système d’IA en scène"
order: 110
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_I—Epreuve_de_detectabilite.docx
collection: ia
slug: cas-pratique/chapitre-1
title: "Cas pratique — Chapitre I : Épreuve de détectabilité"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_1_Epreuve_de_detectabilite.docx
collection: cas-ia
slug: chapitre-1
title: "Chapitre I — Épreuve de détectabilité"
order: 120
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_II—Epreuve_topologique.docx
collection: ia
slug: cas-pratique/chapitre-2
title: "Cas pratique — Chapitre II : Épreuve topologique"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_2_Epreuve_Topologique.docx
collection: cas-ia
slug: chapitre-2
title: "Chapitre II — Épreuve topologique"
order: 130
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_III—Epreuve_archeogenetique.docx
collection: ia
slug: cas-pratique/chapitre-3
title: "Cas pratique — Chapitre III : Épreuve archéogénétique"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_3_Epreuve_archeogenetique.docx
collection: cas-ia
slug: chapitre-3
title: "Chapitre III — Épreuve archéogénétique"
order: 140
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_IV—Epreuve_morphologique.docx
collection: ia
slug: cas-pratique/chapitre-4
title: "Cas pratique — Chapitre IV : Épreuve morphologique"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_4_Epreuve_Morphologique.docx
collection: cas-ia
slug: chapitre-4
title: "Chapitre IV — Épreuve morphologique"
order: 150
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_V—Epreuve_historique.docx
collection: ia
slug: cas-pratique/chapitre-5
title: "Cas pratique — Chapitre V : Épreuve historique"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_5_Epreuve_Historique.docx
collection: cas-ia
slug: chapitre-5
title: "Chapitre V — Épreuve historique"
order: 160
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_VI—Epreuve_de_co-viabilite.docx
collection: ia
slug: cas-pratique/chapitre-6
title: "Cas pratique — Chapitre VI : Épreuve de co-viabilité"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_6_Epreuve_de_Co-viabilite.docx
collection: cas-ia
slug: chapitre-6
title: "Chapitre VI — Épreuve de co-viabilité"
order: 170
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_VII—Gestes_archicratiques_concrets_pour_un_systeme_IA.docx
collection: ia
slug: cas-pratique/chapitre-7
title: "Cas pratique — Chapitre VII : Gestes archicratiques concrets"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_7_Gestes_archicratiques_concrets_pour_un_systeme_IA.docx
collection: cas-ia
slug: chapitre-7
title: "Chapitre VII — Gestes archicratiques concrets pour un système d’IA"
order: 180
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Conclusion.docx
collection: ia
slug: cas-pratique/conclusion
title: "Cas pratique — Conclusion"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Conclusion.docx
collection: cas-ia
slug: conclusion
title: "Conclusion"
order: 190
- source: sources/docx/cas-ia/Cas_IA-Archicratie_et_gouvernance_des_systemes_IA-Annexe—Glossaire_archicratique_pour_audit_des_systemes_IA.docx
collection: ia
slug: cas-pratique/annexe-glossaire-audit
title: "Cas pratique — Annexe : Glossaire archicratique pour audit des systèmes IA"
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Annexe_Glossaire_Archicratique_Cas_IA.docx
collection: cas-ia
slug: annexe-glossaire-audit
title: "Annexe — Glossaire archicratique pour l’audit des systèmes d’IA"
order: 195
# =========================
# Traité — Ontodynamique générative (1 page = 1 chapitre)
# NOTE: on n'inclut PAS le monolithe "Traite-...-version_officielle.docx" dans le manifeste.
# =========================
- source: sources/docx/traite/Traite-Ontodynamique_Generative-Fondements_Archicratie-Introduction-version_officielle.docx
collection: traite
slug: ontodynamique/introduction
title: "Traité — Introduction"
order: 210
- source: sources/docx/traite/Traite-Ontodynamique_Generative-Fondements_Archicratie-Chapitre_1—Le_flux_ontogenetique-version_officielle.docx
collection: traite
slug: ontodynamique/chapitre-1
title: "Traité — Chapitre 1 : Le flux ontogénétique"
order: 220
- source: sources/docx/traite/Traite-Ontodynamique_Generative-Fondements_Archicratie-Chapitre_2—economie_du_reel-version_officielle.docx
collection: traite
slug: ontodynamique/chapitre-2
title: "Traité — Chapitre 2 : Économie du réel"
order: 230
- source: sources/docx/traite/Traite-Ontodynamique_Generative-Fondements_Archicratie-Chapitre_3—Le_reel_comme_systeme_regulateur-version_officielle.docx
collection: traite
slug: ontodynamique/chapitre-3
title: "Traité — Chapitre 3 : Le réel comme système régulateur"
order: 240
- source: sources/docx/traite/Traite-Ontodynamique_Generative-Fondements_Archicratie-Chapitre_4—Arcalite-structures_formes_invariants-version_officielle.docx
collection: traite
slug: ontodynamique/chapitre-4
title: "Traité — Chapitre 4 : Arcalité — structures, formes, invariants"
order: 250
- source: sources/docx/traite/Traite-Ontodynamique_Generative-Fondements_Archicratie-Chapitre_5-Cratialite-forces_flux_gradients-version_officielle.docx
collection: traite
slug: ontodynamique/chapitre-5
title: "Traité — Chapitre 5 : Cratialité — forces, flux, gradients"
order: 260
- source: sources/docx/traite/Traite-Ontodynamique_Generative-Fondements_Archicratie-Chapitre_6—Archicration-version_officielle.docx
collection: traite
slug: ontodynamique/chapitre-6
title: "Traité — Chapitre 6 : Archicration"
order: 270
# =========================
# Glossaire / Lexique
# =========================
@@ -169,4 +131,4 @@ docs:
collection: glossaire
slug: mini-glossaire-verbes
title: "Mini-glossaire des verbes de la scène archicratique"
order: 910
order: 910
-1
View File
@@ -1,2 +1 @@
{}
View File
Whitespace-only changes.
+26 -12
View File
@@ -1,26 +1,42 @@
---
import { getCollection } from "astro:content";
const { currentSlug } = Astro.props;
const {
currentSlug,
collection = "archicrat-ia",
basePath = "/archicrat-ia",
label = "Table des matières"
} = Astro.props;
const entries = (await getCollection("archicratie"))
.filter((e) => e.slug.startsWith("archicrat-ia/"))
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
const slugOf = (entry) => String(entry.id).replace(/\.(md|mdx)$/i, "");
const hrefOf = (entry) => `${basePath}/${slugOf(entry)}/`;
const href = (slug) => `/archicratie/${slug}/`;
const collator = new Intl.Collator("fr", { sensitivity: "base", numeric: true });
const entries = [...await getCollection(collection)].sort((a, b) => {
const ao = Number(a.data.order ?? 9999);
const bo = Number(b.data.order ?? 9999);
if (ao !== bo) return ao - bo;
const at = String(a.data.title ?? a.data.term ?? slugOf(a));
const bt = String(b.data.title ?? b.data.term ?? slugOf(b));
return collator.compare(at, bt);
});
---
<nav class="toc-global" aria-label="Table des matières — ArchiCraT-IA">
<nav class="toc-global" aria-label={label}>
<div class="toc-global__head">
<div class="toc-global__title">Table des matières</div>
<div class="toc-global__title">{label}</div>
</div>
<ol class="toc-global__list">
{entries.map((e) => {
const active = e.slug === currentSlug;
const slug = slugOf(e);
const active = slug === currentSlug;
return (
<li class={`toc-item ${active ? "is-active" : ""}`}>
<a class="toc-link" href={href(e.slug)} aria-current={active ? "page" : undefined}>
<a class="toc-link" href={hrefOf(e)} aria-current={active ? "page" : undefined}>
<span class="toc-link__row">
{active ? (
<span class="toc-active-indicator" aria-hidden="true">👉</span>
@@ -66,7 +82,6 @@ const href = (slug) => `/archicratie/${slug}/`;
opacity: .88;
}
/* On garde <ol> mais on neutralise tout marker/numéro */
.toc-global__list{
list-style: none;
margin: 0;
@@ -148,7 +163,6 @@ const href = (slug) => `/archicratie/${slug}/`;
scrollbar-gutter: stable;
}
@media (prefers-color-scheme: dark){
.toc-global{ background: rgba(255,255,255,0.04); }
.toc-link:hover{ background: rgba(255,255,255,0.06); }
@@ -162,4 +176,4 @@ const href = (slug) => `/archicratie/${slug}/`;
const active = document.querySelector(".toc-global .toc-item.is-active");
if (active) active.scrollIntoView({ block: "nearest" });
})();
</script>
</script>
+316
View File
@@ -0,0 +1,316 @@
---
const {
currentEntry,
allEntries = [],
} = Astro.props;
const slugOf = (entry) => String(entry.id).replace(/\.(md|mdx)$/i, "");
const hrefOf = (entry) => `/glossaire/${slugOf(entry)}/`;
const collator = new Intl.Collator("fr", { sensitivity: "base", numeric: true });
const bySlug = new Map(allEntries.map((entry) => [slugOf(entry), entry]));
const currentSlug = slugOf(currentEntry);
const fondamentauxWanted = [
"archicratie",
"tension",
"arcalite",
"cratialite",
"archicration",
"co-viabilite",
];
const fondamentaux = fondamentauxWanted
.map((slug) => bySlug.get(slug))
.filter(Boolean);
function resolveList(slugs = []) {
return slugs
.map((slug) => bySlug.get(slug))
.filter(Boolean);
}
function uniqueBySlug(entries) {
const seen = new Set();
const out = [];
for (const entry of entries) {
const slug = slugOf(entry);
if (seen.has(slug)) continue;
seen.add(slug);
out.push(entry);
}
return out;
}
const relatedEntries = uniqueBySlug(resolveList(currentEntry.data.related ?? []))
.sort((a, b) => collator.compare(a.data.term, b.data.term));
const opposedEntries = uniqueBySlug(resolveList(currentEntry.data.opposedTo ?? []))
.sort((a, b) => collator.compare(a.data.term, b.data.term));
const seeAlsoEntries = uniqueBySlug(resolveList(currentEntry.data.seeAlso ?? []))
.sort((a, b) => collator.compare(a.data.term, b.data.term));
const paradigmes = [...allEntries]
.filter((e) => e.data.kind === "paradigme" && slugOf(e) !== currentSlug)
.sort((a, b) => collator.compare(a.data.term, b.data.term));
function contextualParadigmsFor(entry) {
const relatedParadigms = (entry.data.related ?? [])
.map((slug) => bySlug.get(slug))
.filter((e) => e && e.data.kind === "paradigme");
const seeAlsoParadigms = (entry.data.seeAlso ?? [])
.map((slug) => bySlug.get(slug))
.filter((e) => e && e.data.kind === "paradigme");
const opposedParadigms = (entry.data.opposedTo ?? [])
.map((slug) => bySlug.get(slug))
.filter((e) => e && e.data.kind === "paradigme");
const merged = uniqueBySlug([
...relatedParadigms,
...seeAlsoParadigms,
...opposedParadigms,
]);
if (merged.length > 0) {
return merged.slice(0, 5);
}
if (entry.data.kind === "paradigme") {
const preferred = [
"gouvernementalite",
"gouvernementalite-algorithmique",
"cybernetique",
"biopolitique",
"domination-legale-rationnelle",
"democratie-deliberative",
"gouvernance-des-communs",
"agencement-machinique",
"pharmacologie-technique",
"preemption-algorithmique",
"dissensus-politique",
"lieu-vide-du-pouvoir",
"habitus-et-violence-symbolique",
"theorie-de-la-resonance",
"conatus-et-multitude",
"configuration-et-interdependance",
"technodiversite-et-cosmotechnie",
"grammatisation-et-proletarisation-cognitive",
];
return uniqueBySlug(
preferred
.filter((slug) => slug !== currentSlug)
.map((slug) => bySlug.get(slug))
.filter(Boolean)
).slice(0, 6);
}
return paradigmes.slice(0, 5);
}
const contextualParadigms = contextualParadigmsFor(currentEntry);
const kindLabels = {
concept: "Concept",
diagnostic: "Diagnostic",
topologie: "Topologie",
verbe: "Verbe",
paradigme: "Paradigme",
doctrine: "Doctrine",
};
const domainLabels = {
transversal: "Transversal",
theorie: "Théorie",
"cas-ia": "Cas IA",
};
const levelLabels = {
fondamental: "Fondamental",
intermediaire: "Intermédiaire",
avance: "Avancé",
};
const metaLabel = [
kindLabels[currentEntry.data.kind] ?? currentEntry.data.kind,
domainLabels[currentEntry.data.domain] ?? currentEntry.data.domain,
levelLabels[currentEntry.data.level] ?? currentEntry.data.level,
].join(" · ");
---
<nav class="glossary-aside" aria-label="Navigation du glossaire">
<div class="glossary-aside__block glossary-aside__block--intro">
<a class="glossary-aside__back" href="/glossaire/">← Retour au glossaire</a>
<div class="glossary-aside__title">Glossaire archicratique</div>
<div class="glossary-aside__meta">{metaLabel}</div>
</div>
<section class="glossary-aside__block">
<h2 class="glossary-aside__heading">Portails</h2>
<ul class="glossary-aside__list">
<li><a href="/glossaire/">Index général</a></li>
<li><a href="/glossaire/paradigmes/">Page paradigmes</a></li>
</ul>
</section>
{fondamentaux.length > 0 && (
<section class="glossary-aside__block">
<h2 class="glossary-aside__heading">Noyau archicratique</h2>
<ul class="glossary-aside__list">
{fondamentaux.map((entry) => {
const active = slugOf(entry) === currentSlug;
return (
<li>
<a
href={hrefOf(entry)}
aria-current={active ? "page" : undefined}
class={active ? "is-active" : undefined}
>
{entry.data.term}
</a>
</li>
);
})}
</ul>
</section>
)}
{(relatedEntries.length > 0 || opposedEntries.length > 0 || seeAlsoEntries.length > 0) && (
<section class="glossary-aside__block">
<h2 class="glossary-aside__heading">Autour de cette fiche</h2>
{relatedEntries.length > 0 && (
<>
<h3 class="glossary-aside__subheading">Liés</h3>
<ul class="glossary-aside__list">
{relatedEntries.map((entry) => (
<li><a href={hrefOf(entry)}>{entry.data.term}</a></li>
))}
</ul>
</>
)}
{opposedEntries.length > 0 && (
<>
<h3 class="glossary-aside__subheading">Opposés</h3>
<ul class="glossary-aside__list">
{opposedEntries.map((entry) => (
<li><a href={hrefOf(entry)}>{entry.data.term}</a></li>
))}
</ul>
</>
)}
{seeAlsoEntries.length > 0 && (
<>
<h3 class="glossary-aside__subheading">Voir aussi</h3>
<ul class="glossary-aside__list">
{seeAlsoEntries.map((entry) => (
<li><a href={hrefOf(entry)}>{entry.data.term}</a></li>
))}
</ul>
</>
)}
</section>
)}
{contextualParadigms.length > 0 && (
<section class="glossary-aside__block">
<h2 class="glossary-aside__heading">
{currentEntry.data.kind === "paradigme" ? "Paradigmes voisins" : "Paradigmes mobilisés"}
</h2>
<ul class="glossary-aside__list">
{contextualParadigms.map((entry) => (
<li><a href={hrefOf(entry)}>{entry.data.term}</a></li>
))}
</ul>
</section>
)}
</nav>
<style>
.glossary-aside{
display: flex;
flex-direction: column;
gap: 14px;
}
.glossary-aside__block{
border: 1px solid rgba(127,127,127,0.22);
border-radius: 16px;
padding: 12px;
background: rgba(127,127,127,0.05);
}
.glossary-aside__block--intro{
padding-top: 11px;
padding-bottom: 11px;
}
.glossary-aside__back{
display: inline-block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 700;
text-decoration: none;
}
.glossary-aside__title{
font-size: 14px;
font-weight: 800;
letter-spacing: .2px;
line-height: 1.25;
}
.glossary-aside__meta{
margin-top: 8px;
font-size: 12px;
line-height: 1.35;
opacity: .78;
}
.glossary-aside__heading{
margin: 0 0 10px;
font-size: 13px;
font-weight: 800;
opacity: .9;
}
.glossary-aside__subheading{
margin: 12px 0 8px;
font-size: 12px;
font-weight: 800;
opacity: .8;
text-transform: uppercase;
letter-spacing: .04em;
}
.glossary-aside__list{
list-style: none;
margin: 0;
padding: 0;
}
.glossary-aside__list li{
margin: 6px 0;
}
.glossary-aside__list a{
text-decoration: none;
font-size: 13px;
line-height: 1.3;
}
.glossary-aside__list a.is-active{
font-weight: 800;
}
@media (prefers-color-scheme: dark){
.glossary-aside__block{
background: rgba(255,255,255,0.04);
}
}
</style>
+114 -21
View File
@@ -1,35 +1,128 @@
---
// src/components/LevelToggle.astro
const { initialLevel = 1 } = Astro.props;
---
<div class="level-toggle" role="group" aria-label="Niveau de lecture">
<button type="button" class="lvl-btn" data-level="1" aria-pressed="true">Niveau 1</button>
<button type="button" class="lvl-btn" data-level="2" aria-pressed="false">Niveau 2</button>
<button type="button" class="lvl-btn" data-level="3" aria-pressed="false">Niveau 3</button>
<div class="level-toggle" role="group" aria-label="Mode d’édition">
<button type="button" class="level-btn" data-level="1">Propos</button>
<button type="button" class="level-btn" data-level="2">Références</button>
<button type="button" class="level-btn" data-level="3">Illustrations</button>
<button type="button" class="level-btn" data-level="4">Commentaires</button>
</div>
<script is:inline>
<script is:inline define:vars={{ initialLevel }}>
(() => {
const KEY = "archicratie.readingLevel";
const buttons = Array.from(document.querySelectorAll(".lvl-btn"));
const BODY = document.body;
function apply(level) {
document.body.setAttribute("data-reading-level", String(level));
buttons.forEach((b) => b.setAttribute("aria-pressed", b.dataset.level === String(level) ? "true" : "false"));
const wrap = document.querySelector(".level-toggle");
if (!wrap) return;
const buttons = Array.from(wrap.querySelectorAll("button[data-level]"));
if (!buttons.length) return;
const KEY = "archicratie:readingLevel";
function clampLevel(n) {
const x = Number.parseInt(String(n), 10);
if (!Number.isFinite(x)) return 1;
return Math.min(4, Math.max(1, x));
}
// Valeur par défaut : si rien n'est stocké, on met 1 (citoyen).
// Si JS est absent/casse, le site reste lisible (tout s'affiche).
const stored = Number(localStorage.getItem(KEY));
const level = (stored === 1 || stored === 2 || stored === 3) ? stored : 1;
function setActiveUI(lvl) {
for (const b of buttons) {
const on = String(b.dataset.level) === String(lvl);
b.classList.toggle("is-active", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
}
}
apply(level);
function captureBeforeLevelSwitch() {
const paraId =
window.__archiCurrentParaId ||
window.__archiLastParaId ||
String(location.hash || "").replace(/^#/, "") ||
"";
buttons.forEach((b) => {
b.addEventListener("click", () => {
const lvl = Number(b.dataset.level);
localStorage.setItem(KEY, String(lvl));
apply(lvl);
});
window.__archiLevelSwitchCtx = {
paraId,
hash: location.hash || "",
scrollY: window.scrollY || 0,
t: Date.now(),
};
}
function applyLevel(lvl, { persist = true } = {}) {
const v = clampLevel(lvl);
if (BODY) BODY.dataset.readingLevel = String(v);
setActiveUI(v);
if (persist) {
try { localStorage.setItem(KEY, String(v)); } catch {}
}
try {
window.dispatchEvent(
new CustomEvent("archicratie:readingLevel", { detail: { level: v } })
);
} catch {}
}
// init : storage > initialLevel
let start = clampLevel(initialLevel);
try {
const stored = localStorage.getItem(KEY);
if (stored) start = clampLevel(stored);
} catch {}
applyLevel(start, { persist: false });
// clicks
wrap.addEventListener("click", (ev) => {
const btn = ev.target?.closest?.("button[data-level]");
if (!btn) return;
ev.preventDefault();
// ✅ crucial : on capture la position AVANT le reflow lié au changement de niveau
captureBeforeLevelSwitch();
applyLevel(btn.dataset.level);
});
})();
</script>
<style>
.level-toggle{
display: inline-flex;
gap: 8px;
align-items: center;
}
.level-btn{
border: 1px solid rgba(127,127,127,0.40);
background: rgba(127,127,127,0.08);
border-radius: 999px;
padding: 6px 10px;
font-size: 13px;
cursor: pointer;
user-select: none;
transition: filter .12s ease, transform .12s ease, background .12s ease, border-color .12s ease;
}
.level-btn:hover{
filter: brightness(1.08);
}
.level-btn.is-active{
border-color: rgba(160,160,255,0.95);
background: rgba(140,140,255,0.18);
font-weight: 900;
}
.level-btn.is-active:hover{
filter: brightness(1.12);
}
.level-btn:active{
transform: translateY(1px);
}
</style>
File diff suppressed because it is too large. Load diff
+16 -10
View File
@@ -1,11 +1,17 @@
<nav class="site-nav" aria-label="Navigation principale">
<a href="/">Accueil</a><span aria-hidden="true"> · </span>
<a href="/editions/">Carte des œuvres</a><span aria-hidden="true"> · </span>
<a href="/methode/">Méthode</a><span aria-hidden="true"> · </span>
<a href="/recherche/">Recherche</a><span aria-hidden="true"> · </span>
<a href="/archicratie/">Essai-thèse</a><span aria-hidden="true"> · </span>
<a href="/traite/">Traité</a><span aria-hidden="true"> · </span>
<a href="/ia/">Cas IA</a><span aria-hidden="true"> · </span>
<a href="/glossaire/">Glossaire</a><span aria-hidden="true"> · </span>
<a href="/atlas/">Atlas</a>
</nav>
<a href="/">Accueil</a>
<span aria-hidden="true"> · </span>
<a href="/archicrat-ia/">Essai-thèse</a>
<span aria-hidden="true"> · </span>
<a href="/cas-ia/">Cas IA</a>
<span aria-hidden="true"> · </span>
<a href="/glossaire/">Glossaire</a>
<span aria-hidden="true"> · </span>
<a href="/recherche/">Recherche</a>
</nav>
+83
View File
@@ -0,0 +1,83 @@
import { defineCollection, z } from "astro:content";
const linkSchema = z.object({
type: z.enum(["definition", "appui", "transposition"]),
target: z.string().min(1),
note: z.string().optional()
});
const baseTextSchema = z.object({
title: z.string().min(1),
level: z.union([z.literal(1), z.literal(2), z.literal(3)]).default(1),
version: z.string().min(1),
concepts: z.array(z.string().min(1)).default([]),
links: z.array(linkSchema).default([]),
order: z.number().int().nonnegative().optional(),
summary: z.string().optional()
});
// Éditions (séparation stricte : edition + status verrouillés par collection)
const casIa = defineCollection({
type: "content",
schema: baseTextSchema.extend({
edition: z.literal("cas-ia"),
status: z.literal("application")
})
});
const commencer = defineCollection({
type: "content",
schema: baseTextSchema.extend({
edition: z.literal("commencer"),
status: z.union([z.literal("presentation"), z.literal("draft")])
})
});
// ✅ NOUVELLE collection : archicrat-ia (Essai-thèse)
// NOTE : on accepte temporairement edition/status "archicratie/modele_sociopolitique"
// si tes MDX n’ont pas encore été normalisés.
// Quand tu voudras "strict", on passera à edition="archicrat-ia" status="essai_these"
// + update frontmatter des 7 fichiers.
const archicratIa = defineCollection({
type: "content",
schema: baseTextSchema.extend({
edition: z.union([z.literal("archicrat-ia"), z.literal("archicratie")]),
status: z.union([z.literal("essai_these"), z.literal("modele_sociopolitique")])
})
});
// Glossaire (référentiel terminologique)
const glossaire = defineCollection({
type: "content",
schema: z.object({
title: z.string().min(1),
term: z.string().min(1),
aliases: z.array(z.string().min(1)).default([]),
urlAliases: z
.array(z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/))
.default([]),
mobilizedAuthors: z.array(z.string().min(1)).default([]),
comparisonTraditions: z.array(z.string().min(1)).default([]),
edition: z.literal("glossaire"),
status: z.literal("referentiel"),
version: z.string().min(1),
definitionShort: z.string().min(1),
concepts: z.array(z.string().min(1)).default([]),
links: z.array(linkSchema).default([]),
kind: z.enum(["concept", "topologie", "diagnostic", "verbe", "paradigme", "doctrine"]),
domain: z.enum(["transversal", "theorie", "cas-ia"]),
level: z.enum(["fondamental", "intermediaire", "avance"]),
related: z.array(z.string().min(1)).default([]),
opposedTo: z.array(z.string().min(1)).default([]),
seeAlso: z.array(z.string().min(1)).default([])
})
});
export const collections = {
commencer,
"archicrat-ia": archicratIa,
"cas-ia": casIa,
glossaire,
};
@@ -1,6 +1,6 @@
---
title: "Chapitre 1 — Fondements épistémologiques et modélisation"
edition: "archicratie"
edition: "archicrat-ia"
status: "modele_sociopolitique"
level: 1
version: "0.1.0"
@@ -14,35 +14,41 @@ source:
---
Si les sciences politiques ont longtemps trouvé leur ancrage et leur légitimité dans l’analyse des institutions formelles du pouvoir – souveraineté, contrat, autorité, représentation – c’est que le politique y était toujours présumé se manifester à travers une scène, un lieu, un sujet, un régime. Le pouvoir y avait des signes, des corps, des textes ; il procédait d’un fondement — Dieu, la volonté générale, la loi, la nation — et d’un opérateur identifié : le prince, le peuple, le juge, l’État. De la théorie de Hobbes à celle de Rawls, en passant par Rousseau, Kant ou Habermas, les paradigmes de légitimation sont fondés sur une ontologie de la centralité, de la scène constituante, et d’un sujet instituant dont l’autonomie garantit la normativité du pouvoir.
Or, ce que nos régimes contemporains de régulation mettent désormais en crise, ce n’est pas tant la scène elle-même, que la possibilité de sa tenue effective. La scène subsiste, peut-être, mais elle est vidée, ritualisée, simulée, remplacée, saturée ou dissoute dans des procédures techniques, des protocoles logistiques, des décisions sans auteurs, des gouvernances algorithmiques. Le pouvoir opère sans se déclarer, régule sans se justifier, agit sans se mettre en débat. Il est devenu ce que nous proposons de nommer *archicratistique* — c’est-à-dire qu’il agit selon un régime de régulation autonome sans scène, sans justification visible, sans délai institué pour la dispute, sans temporalité contradictoire. Non pas qu’il soit tyrannique, autoritaire ou caché au sens classique, mais parce qu’il *ne se donne plus à la critique* selon les formes instituées de l’opposabilité politique.
Or, ce que nos régimes contemporains de régulation mettent désormais en crise, ce n’est pas tant la scène elle-même, que la possibilité de sa tenue effective. La scène subsiste, peut-être, mais elle est vidée, ritualisée, simulée, remplacée, saturée ou dissoute dans des procédures techniques, des protocoles logistiques, des décisions sans auteurs, des gouvernances algorithmiques. Le pouvoir opère comme s’il n’avait plus à se déclarer, régule sur des justifications faiblement exposables, et agit dans des configurations où la mise en débat est neutralisée, comprimée ou vidée de prise. Il est devenu ce que nous proposons de nommer archicratistique — c’est-à-dire qu’il agit selon un régime de régulation autonome où la scène d’épreuve se trouve neutralisée, mimée, relocalisée hors d’atteinte ou rendue pratiquement inopérante ; où les justifications demeurent faiblement exposables ou inopposables pour les affectés ; où le délai de la dispute est comprimé, fictif ou supprimé dans les faits ; et où la temporalité contradictoire n’est plus tenue comme scène praticable de reprise. Non pas qu’il soit tyrannique, autoritaire ou caché au sens classique, mais parce qu’il ne se donne plus à la critique selon les formes instituées de l’opposabilité politique.
L’ambition de ce chapitre est donc de fonder ce paradigme nouveau, ou plus exactement, de nommer et d’outiller une forme ancienne mais non encore pensée du pouvoir — la régulation contradictoire institué. Cette forme, nous la nommons *archicratie*, non pour désigner un régime de plus, mais pour désigner un méta-régime viable de régulation lorsqu’une triade — *arcalité* (fondation déclarable), *cratialité* (opération traçable), *archicration* (épreuve instituée différée) — se tient en tenségrité, c’est-à-dire en tension dynamique métastable l’un à l’autre.
L’ambition de ce chapitre est donc de fonder ce paradigme nouveau, ou plus exactement, de nommer et d’outiller une forme ancienne mais non encore pensée du pouvoir — la régulation contradictoire instituée. Cette forme, nous la nommons *archicratie*, non pour désigner un régime de plus, mais pour désigner un méta-régime viable de régulation lorsqu’une triade — *arcalité* (fondation déclarable), *cratialité* (opération traçable), *archicration* (épreuve instituée différée) — se tient en tenségrité, c’est-à-dire en tension dynamique métastable l’un à l’autre.
À rebours de l’*archicratie*, nous appelons désarchicration la configuration où la scène est dissoute, court-circuitée ou relocalisée hors d’atteinte, tandis que les régulations se poursuivent néanmoins. Il ne s’agit pas de prétendre que tout deviendrait opaque, mais de constater que l’opacité est structurelle aux dispositifs : non comme accident, mais comme condition de fonctionnement (secret industriel, non-auditabilité des modèles, dilution des responsabilités). Les algorithmes de notation d’allocataires, les règles budgétaires qui déclenchent mécaniquement des fermetures, les protocoles d’ajustement fiscal ou de police sanitaire sont légalement institués, techniquement rationalisés, mais politiquement indisponibles à l’opposition procédurale. Cette indisponibilité se mesure : absence de canal de saisine opposable, délai non garanti pour contester, motifs non accessibles ni réutilisables par les contestataires, paramètres non traçables ni réversibles. Ce n’est pas une archicratie (qui suppose la tenue d’une scène), mais son inverse : une désarchicration où la cratialité et l’arcalité se conjuguent sans archicration, produisant des décisions *indiscutables* de fait, parce que non opposables en temps et en raison.
À rebours de l’archicratie, nous appelons désarchicration la configuration où la scène est dissoute, court-circuitée ou relocalisée hors d’atteinte, tandis que les régulations se poursuivent néanmoins. Il ne s’agit pas de prétendre que tout deviendrait opaque, mais de constater que l’opacité est structurelle à certains dispositifs : non comme accident, mais comme condition de fonctionnement. Les algorithmes de notation d’allocataires, les règles budgétaires qui déclenchent mécaniquement des fermetures, les protocoles d’ajustement fiscal ou de police sanitaire sont légalement institués, techniquement rationalisés, mais politiquement indisponibles à l’opposition procédurale. Cette indisponibilité se mesure : absence de canal de saisine opposable, délai non garanti pour contester, motifs non accessibles ni réutilisables par les contestataires, paramètres non traçables ni réversibles. Ce n’est pas une archicratie, qui suppose la tenue d’une scène, mais son inverse : une désarchicration dans laquelle l’arcalité et la cratialité continuent d’opérer tandis que l’archicration se trouve neutralisée, relocalisée hors d’atteinte, ou rendue pratiquement inopérante, produisant des décisions indiscutables de fait parce que non opposables en temps et en raison.
Les paradigmes classiques — qu’ils soient contractualistes, décisionnistes ou délibératifs — échouent désormais à rendre compte de ces formes. Le pouvoir n’est plus localisé dans un lieu, il ne procède plus d’une scène constituante unique, il n’est plus incarné par un sujet identifiable. Il opère à travers une multiplicité de dispositifs hétérogènes, qui se soutiennent les uns les autres sans principe transcendant, selon une logique de capture, de redondance, de saturation ou d’euphémisation. Michel Foucault l’avait annoncé dès la fin des années 1970 avec son analyse de la gouvernementalité : « ce qui se met en place, ce n’est plus le droit de faire mourir ou de laisser vivre, mais le pouvoir de faire vivre et de laisser mourir » (1976). Hannah Arendt, bien avant lui, avait déjà diagnostiqué que « la disparition de la scène publique, c’est la disparition de la politique elle-même » (1958). Claude Lefort, dans *L’Invention démocratique*, avait désigné le pouvoir démocratique comme un lieu vide — précisément parce que sa légitimité dépend de la possibilité constante de remise en scène, de re-questionnement, de re-distribution. Mais que reste-t-il lorsque cette vacance est non plus instituante, mais neutralisée par des automatismes ? Lorsque la scène est formellement prévue mais substantiellement empêchée ? Lorsque les voies de recours sont techniques, absconses, et différées jusqu’à l’oubli ?
Ce que nous vivons, ce que nous subissons parfois, ce que nous habitons souvent sans le voir, c’est la montée silencieuse des régimes archicratiques : dispositifs de régulation où la scène est inaccessible, où les fondements ne sont plus invoqués, où les décisions ne sont plus motivées, où le temps du différé est supprimé. Ce ne sont pas des régimes de domination au sens fort ; ce sont des régimes d’administration sans épreuve, des formes d’ordonnancement sans théâtre. L’archicratie ne se pense ni contre la démocratie, ni comme sa dégénérescence, mais comme sa marginalisation structurelle : elle s’installe dans les interstices, dans les écarts, dans les non-lieux du politique. Elle est l’administration algorithmique des droits, la codification silencieuse des trajectoires, la performativité sans contradictoire des normes automatiques.
Ce que nous vivons, ce que nous subissons parfois, ce que nous habitons souvent sans le voir, ce n’est pas la montée silencieuse de “régimes archicratiques” au sens fort, mais la prolifération de configurations désarchicratiques et archicratistiques, parfois portées jusqu’à la dérive autarchicratique. Il s’agit de dispositifs de régulation où la scène devient difficilement praticable, neutralisée ou relocalisée hors d’atteinte ; où les fondements deviennent faiblement invoqués, euphémisés ou difficilement exposables ; où les décisions sont de moins en moins motivées de manière opposable ; et où le temps du différé se trouve comprimé, vidé de sa prise ou rendu fictif.
Il nous faut alors un paradigme, non pour idéaliser une alternative, mais pour diagnostiquer cette configuration, pour la rendre visible, pensable, opposable. Car c’est bien cela qui définit un paradigme : sa capacité à faire apparaître ce qui était jusque-là neutralisé. Le paradigme archicratique ne vise pas à remplacer les théories classiques du pouvoir, mais à leur adjoindre une grammaire supplémentaire, permettant de nommer ce qui échappait à leurs catégories. Il ne s’agit plus seulement de savoir *qui* gouverne, mais *comment* se tiennent les régulations, *malgré* l’absence de légitimation visible, *malgré* l’effondrement des scènes instituées, *malgré* la disparition des temporalités différées de la décision. Il s’agit de rendre compte des zones grises, des seuils, des bifurcations, des objets métonymiques de l’ordre, des fonctions qui régulent sans jamais statuer.
Il ne s’agit pas nécessairement de régimes de domination au sens classique, mais de formes de régulation administrative dans lesquelles l’épreuve est neutralisée, mimée ou rendue pratiquement inopérante, tandis que le théâtre politique s’affaiblit, se vide de sa prise ou se trouve relocalisé hors d’atteinte. Il ne s’agit donc pas ici de l’archicratie comme méta-régime viable de régulation, mais de son envers pratique : des dispositifs qui continuent d’opérer tout en neutralisant les conditions de leur propre mise en discussion. Se déploient ainsi des chaînes de décision où l’administration algorithmique des droits, la codification silencieuse des trajectoires et la performativité de normes automatiques dont le contradictoire est neutralisé ou vidé de prise tendent à soustraire la régulation à l’épreuve plutôt qu’à l’instituer.
Il nous faut alors un paradigme, non pour idéaliser une alternative, mais pour diagnostiquer cette configuration, pour la rendre visible, pensable, opposable. Car c’est bien cela qui définit un paradigme : sa capacité à faire apparaître ce qui était jusque-là neutralisé. Le paradigme archicratique ne vise pas à remplacer les théories classiques du pouvoir, mais à leur adjoindre une grammaire supplémentaire, permettant de nommer ce qui échappait à leurs catégories. Il ne s’agit plus seulement de savoir qui gouverne, mais comment se tiennent les régulations lorsque les légitimations deviennent faiblement visibles, lorsque les scènes instituées sont neutralisées, vidées ou relocalisées hors d’atteinte, et lorsque les temporalités différées de la décision se trouvent comprimées, fictives ou pratiquement supprimées. Il s’agit de rendre compte des zones grises, des seuils, des bifurcations, des objets métonymiques de l’ordre, des fonctions qui régulent sans jamais statuer.
Dans cette perspective, ce chapitre se donne pour tâche de poser les fondements épistémologiques, conceptuels et politiques du paradigme archicratique. Nous y exposerons son architecture théorique tripolaire — *arcalité* (ce qui fonde), *cratialité* (ce qui opère), *archicration* (ce qui permet la dispute) — ainsi que sa grammaire topologique interne/externe, ses objets de repérage concrets (fonctions, seuils, porteurs, signes, temporalités), et enfin ses critères de validité. Nous chercherons à produire une cartographie intelligible des formes de régulation contemporaine, sans tomber dans le cynisme ni dans l’utopie, mais en nommant avec précision les tensions, les prises, les scènes, les ruptures, les points aveugles.
Car penser l’archicratie, ce n’est pas inventer un concept : c’est l’extraire du réel pour en faire un outil de diagnostic, un test de viabilité des régulations, un instrument de critique effective des dispositifs. C’est interroger ce qui gouverne sans débat, ce qui ordonne sans justification, ce qui affecte sans se montrer. C’est rouvrir la possibilité d’une pensée politique critique du réel.
Car penser l’archicratie, ce n’est pas inventer un concept : c’est l’extraire du réel pour en faire un outil de diagnostic, un test de viabilité des régulations, un instrument de critique effective des dispositifs. C’est interroger ce qui gouverne à débat neutralisé, ce qui ordonne sur des justifications difficilement exposables, ce qui affecte en se soustrayant aux formes ordinaires de visibilité, de contestation et de reprise. C’est rouvrir la possibilité d’une pensée politique critique du réel.
Si la modernité politique a trouvé dans la souveraineté représentative son axe structurant, sa fiction fondatrice et sa promesse régulatrice, cette architecture intellectuelle et institutionnelle semble aujourd’hui en proie à une désynchronisation profonde avec les formes effectives de la régulation contemporaine. La souveraineté, dans sa formulation classique — qu’elle s’incarne dans le peuple, la nation, l’État, le contrat ou la loi — présuppose une centralité décisionnelle, une légitimité visible, une continuité symbolique entre le fondement et l’exercice du pouvoir. Or, ce que nous observons aujourd’hui dans la majorité des dispositifs régulateurs, c’est une crise de cette souveraineté représentative non pas tant dans sa légitimité que dans sa capacité à structurer les prises réelles sur le monde. La scène parlementaire subsiste, mais elle est fréquemment contournée ; les mécanismes électoraux se perpétuent, mais ils échouent à produire une autorité agissante sur les déterminations majeures ; les figures institutionnelles traditionnelles persistent, mais elles ne sont plus les lieux d’où se décident ni les normes, ni les trajectoires. Ce déphasage — structurel, et non conjoncturel — signe l’obsolescence progressive d’un régime de pensée : celui qui identifie le pouvoir à une source souveraine, incarnée, stable, légitimée.
Si la modernité politique a trouvé dans la souveraineté représentative son axe structurant, sa fiction fondatrice et sa promesse régulatrice, cette architecture intellectuelle et institutionnelle semble aujourd’hui en proie à une désynchronisation profonde avec les formes effectives de la régulation contemporaine. La souveraineté, dans sa formulation classique — qu’elle s’incarne dans le peuple, la nation, l’État, le contrat ou la loi — présuppose une centralité décisionnelle, une légitimité visible, une continuité symbolique entre le fondement et l’exercice du pouvoir.
À cette crise de la souveraineté s’ajoute l’épuisement des grilles de lecture fondées sur le sujet autonome, contractuel, rationnel — figure centrale de la philosophie politique moderne. La fiction du citoyen-individu, maître de lui-même, capable d’entrer en délibération avec autrui, producteur d’une volonté générale informée, n’a plus de prise réelle sur les architectures décisionnelles automatisées, sur les normes codifiées par délégation, sur les processus algorithmiques opérant sans concertation. L’idéal contractualiste — au fondement des démocraties libérales occidentales — repose sur une scène d’égalité juridique, d’information partagée, de temporalité différée. Or les régulations actuelles, dans le champ fiscal, sanitaire, technologique, éducatif ou même sécuritaire, se déploient sans que ces conditions soient réunies. Le sujet y est affecté avant même d’avoir été informé ; il y est inclus sans qu’on le consulte ; il y est contraint sans avoir la possibilité d’émettre un désaccord opposable. La subjectivité politique classique — cette figure du citoyen capable de comprendre, d’argumenter, de consentir ou de refuser — est contournée par des formats de décision qui n’appellent plus ni la volonté, ni la délibération, ni même la conscience. L’archicratie commence précisément là où le pouvoir ne passe plus par le sujet.
Or, ce que nous observons aujourd’hui dans la majorité des dispositifs régulateurs, c’est une crise de la souveraineté représentative non pas tant dans sa légitimité déclarée que dans sa capacité à structurer les prises réelles sur le monde. La scène parlementaire subsiste, mais elle est fréquemment contournée ; les mécanismes électoraux se perpétuent, mais ils échouent souvent à peser sur les déterminations majeures ; les figures institutionnelles traditionnelles persistent, mais elles ne sont plus toujours les lieux effectifs de production, de justification et de révision des normes. Ce déphasage — structurel, et non conjoncturel — ne signifie pas la disparition du politique, mais la transformation de ses modes de régulation.
Mais cette crise des catégories héritées n’est pas une vacance : elle est immédiatement occupée par des formes nouvelles de régulation — que nous appelons ici *archicratiques*. Car ce n’est pas l’absence de pouvoir qui domine, mais sa redistribution silencieuse selon des régimes techno-fonctionnels. Nous assistons à une montée en régime de ce que Foucault nommait déjà *les dispositifs de gouvernementalité* : régulations discrètes, réparties, non centralisées, opérant par la norme, le calcul, la procédure, le protocole, l’interface, le flux. Ce sont des décisions sans fondement visible, sans discours justificatif, sans énonciateur identifiable. La décision se donne comme évidence procédurale, comme injonction technique, comme impératif statistique. L’ordre n’est plus commandé, il est implémenté ; il ne se fonde plus dans la loi, mais dans l’algorithme ; il ne se légitime plus par le débat, mais par le chiffre.
À cette crise de la souveraineté s’ajoute l’épuisement des grilles de lecture fondées sur le sujet autonome, contractuel et rationnel, figure centrale de la philosophie politique moderne. La fiction du citoyen-individu, maître de lui-même, capable d’entrer en délibération avec autrui et de participer à une volonté générale informée, n’a plus de prise suffisante sur des architectures décisionnelles automatisées, sur des normes codifiées par délégation, sur des processus algorithmiques opérant à distance ou sans concertation réelle. Le sujet y est souvent affecté avant même d’avoir été informé ; inclus sans être consulté ; contraint sans pouvoir formuler un désaccord effectivement opposable. La subjectivité politique classique se trouve ainsi contournée par des formats de décision qui n’appellent plus nécessairement ni la volonté, ni la délibération, ni même la conscience explicite des sujets affectés.
Cette montée archicratique s’observe dans la prolifération de gouvernances sans contre-pouvoir : agences indépendantes, plateformes numériques, circuits de certification, autorités administratives sans délibération parlementaire. Ce sont des formes de régulation qui ne sont pas illégales, mais qui échappent à toute mise en scène démocratique. Le débat n’est pas supprimé : il est rendu impossible. Les délais ne sont pas raccourcis : ils sont supprimés. Les voies de recours ne sont pas fermées : elles sont devenues impraticables. L’archicratie est ce régime où la décision opère sans justification explicite, où l’ordre normatif n’a plus besoin de fondation symbolique, et où le politique est remplacé par la régulation performative.
Mais cette crise des catégories héritées n’est pas une vacance. Elle signale au contraire l’émergence de formes de régulation dont les prises réelles se redistribuent selon des logiques techno-fonctionnelles, administratives, protocolaires ou computationnelles. Ce ne sont pas des formes qu’il faut appeler archicratiques au sens fort, dès lors qu’elles tendent précisément à soustraire la régulation à la scène d’épreuve ; elles relèvent plutôt de dynamiques de désarchicration, d’archicratistique, ou, lorsqu’elles se referment durablement sur leur propre logique d’exécution, de dérives autarchicratiques.
Dès lors, ce que ce chapitre propose, c’est un paradigme non pas alternatif mais *diagnostic* : il ne s’agit pas d’imaginer une nouvelle utopie du pouvoir juste, mais de produire une grille d’intelligibilité critique des régulations sans scène. Car un paradigme n’est pas une hypothèse théorique isolée ; il est une manière de rendre pensable ce qui échappait jusqu’alors aux catégories existantes. Dans notre cas, il s’agit de rendre visibles, pensables et opposables les formes de régulation qui opèrent *en dehors* des critères classiques du politique. Le paradigme archicratique est un paradigme de la non-scène : il suppose l’effectivité sans justification, la décision sans énonciateur, la procédure sans fondement, la régulation sans contradictoire.
Ce que nous voyons proliférer, ce sont des régulations discrètes, réparties, non centralisées, opérant par la norme, le calcul, la procédure, le protocole, l’interface ou le flux : décisions à fondement peu visible, à justification faible, à énonciateur diffus, qui se donnent comme évidences procédurales, injonctions techniques ou impératifs statistiques. L’ordre n’y est plus principalement commandé ; il est implémenté. Il ne se fonde plus toujours dans une loi explicitement débattue ; il s’inscrit dans des chaînes opératoires, des standards, des métriques, des architectures logicielles ou des agencements institutionnels partiellement soustraits à l’épreuve.
Or, pour être valide, un tel paradigme doit être opposable. C’est-à-dire qu’il doit permettre de formuler un diagnostic vérifiable, contestable, falsifiable, reproductible. Il doit désigner des objets de repérage : scènes absentes, fondements éteints, délais supprimés, procédures automatiques, figures d’intercession techniques. Il doit nommer des configurations typiques : régimes où tout semble fonctionnel, mais où rien ne peut plus être remis en cause. Il doit permettre une lecture différenciée du réel, et non une généralisation rhétorique. C’est à cette condition — à la fois théorique, épistémique et politique — que le paradigme archicratique peut s’imposer comme une nouvelle grammaire critique de la régulation contemporaine.
Cette montée autarchicratique s’observe dans la prolifération de gouvernances sans contre-pouvoir : agences indépendantes, plateformes numériques, circuits de certification, autorités administratives sans délibération parlementaire effective. Ce sont des formes de régulation qui ne sont pas nécessairement illégales, mais qui échappent progressivement à une mise en scène démocratique substantielle. Le débat n’y est pas frontalement supprimé : il est rendu inopérant. Les délais ne sont pas toujours abolis : ils deviennent insuffisants, fictifs ou impraticables. Les voies de recours ne sont pas nécessairement closes en droit : elles cessent d’être effectivement opposables. L’autarchicratie ne désigne donc pas ici un méta-régime symétrique de l’archicratie, mais la dérive par laquelle des architectures régulatrices continuent d’opérer tout en se soustrayant à la scène d’épreuve, en affaiblissant l’exposition de leurs fondements et en comprimant le politique dans l’exécution performative de leurs propres procédures.
La crise sanitaire du COVID-19, à cet égard, a constitué une épreuve paradigmatique : décisions exceptionnelles prises en Conseil de défense, sans publication des débats, sans délais, sans contradictoire ; régulations sociales massives imposées par décret, par indicateurs, par plateformes ; gouvernance par courbe épidémique, sans justification autre que l’anticipation algorithmique. Dans ce moment extrême, le paradigme archicratique a révélé sa capacité à lire ce que les catégories classiques ne permettaient plus d’interpréter. De même, dans la gouvernance algorithmique des aides sociales, dans la notation automatique des élèves ou dans la gestion budgétaire par règles européennes automatiques, nous observons des dispositifs où *la régulation opère sans pouvoir visible, sans scène explicite, sans fondement justifié*. Ce n’est pas la fin du politique, c’est sa mutation silencieuse. Et c’est cette mutation que l’archicratie nous permet de penser, de cartographier, de critiquer — à condition que le paradigme se dote d’une architecture rigoureuse, d’une typologie opératoire, d’une exigence de validation, et d’un langage partagé. Ce chapitre en jette les fondements.
Dès lors, ce que ce chapitre propose, ce n’est pas de rebaptiser “archicratie” la non-scène contemporaine, mais de construire un paradigme capable de la diagnostiquer. Le paradigme archicratique n’est pas un paradigme de la non-scène ; il est une grammaire critique permettant de repérer les conditions sous lesquelles une régulation devient fondée, opérante et révisable — ou, au contraire, dérive vers l’opacité, l’indisputabilité et la fermeture, c’est-à-dire l’autarchicratie. Il ne vise donc pas à célébrer une mutation silencieuse du pouvoir, mais à rendre visibles, pensables et opposables les écarts entre une régulation pleinement archicratique, articulant arcalité, cratialité et archicration, et des dispositifs qui, tout en continuant d’opérer, se soustraient à la scène de leur propre épreuve. C’est à cette condition qu’un tel paradigme peut prétendre à une véritable portée diagnostique, critique et politique.
Or, pour être valide, un tel paradigme doit être opposable. C’est-à-dire qu’il doit permettre de formuler un diagnostic vérifiable, contestable, falsifiable, reproductible. Il doit désigner des objets de repérage : scènes neutralisées, mimées ou relocalisées hors d’atteinte ; fondements opacifiés, euphémisés ou difficilement exposables ; délais comprimés, fictifs ou pratiquement supprimés, procédures automatiques, figures d’intercession techniques. Il doit nommer des configurations typiques : régimes où tout semble fonctionnel, mais où rien ne peut plus être remis en cause. Il doit permettre une lecture différenciée du réel, et non une généralisation rhétorique. C’est à cette condition — à la fois théorique, épistémique et politique — que le paradigme archicratique peut s’imposer comme une nouvelle grammaire critique de la régulation contemporaine.
La crise sanitaire du COVID-19, à cet égard, a constitué une épreuve paradigmatique : décisions exceptionnelles prises en Conseil de défense, sans publication des débats, avec des délais comprimés et un contradictoire fortement réduit ; régulations sociales massives imposées par décret, par indicateurs, par plateformes ; gouvernance par courbe épidémique, avec des justifications largement internalisées dans la logique de l’anticipation sanitaire. Dans ce moment extrême, le paradigme archicratique a révélé sa capacité à lire ce que les catégories classiques ne permettaient plus d’interpréter. De même, dans la gouvernance algorithmique des aides sociales, dans la notation automatique des élèves ou dans la gestion budgétaire par règles européennes automatiques, nous observons des dispositifs où la régulation opère à travers des chaînes de décision faiblement visibles, à scène d’épreuve comprimée ou relocalisée, et à fondements difficilement exposables pour ceux qu’ils affectent. Ce n’est pas la fin du politique, c’est sa mutation silencieuse. Et c’est cette mutation que le paradigme archicratique nous permet de penser, de cartographier et de critiquer — à condition qu’il se dote d’une architecture rigoureuse, d’une typologie opératoire, d’une exigence de validation et d’un langage partagé.
## 1.1 — Hypothèse fondatrice : L’archicratie comme paradigme triadique de régulation
@@ -52,11 +58,11 @@ Le premier pôle est celui de *l’arcalité*. L’arcalité n’est ni une simp
Le second pôle est celui de *la cratialité*. La cratialité désigne la capacité effective d’un dispositif à produire des effets dans le monde : son opérativité, sa puissance d’action, sa capacité à structurer, à contraindre, à transformer. Elle inclut les infrastructures matérielles (bâtiments, réseaux, plateformes), les dispositifs techniques (logiciels, bases de données, interfaces), les procédures (circulaires, décrets, codifications), les corps organisés (administrations, directions, opérateurs), ainsi que les flux (budgétaires, logistiques, cognitifs) qui permettent à la régulation de s’effectuer. La cratialité n’est pas nécessairement visible, mais elle est toujours active. Elle est la force sans laquelle la norme reste lettre morte, la capacité sans laquelle le fondement demeure incantation. C’est la force de transformation, mais aussi, potentiellement, celle de capture, de verrouillage ou d’effacement de la scène politique.
Le troisième pôle, enfin, est celui de *l’archicration*. Ce néologisme désigne la capacité d’un ordre à se rendre *disputable* : c’est-à-dire à instituer une scène, un moment, un dispositif, où l’ordre peut être mis en épreuve, critiqué, reformulé, contesté, amendé. L’archicration est ce qui rend le pouvoir *opposable*, non dans un sens antagonique, mais dans un sens procédural, différé, dialogique. Elle prend la forme d’un contradictoire institué, d’un recours possible, d’un délai interprétatif, d’un droit de parole, d’un espace de reconfiguration. Là où l’arcalité fonde, où la cratialité opère, l’archicration dispute — et en disputant, elle garantit la *viabilité politique* du dispositif. Sans elle, la régulation devient une pure mécanique ; avec elle, elle devient une architecture habitable.
Le troisième pôle, enfin, est celui de *l’archicration*. Ce néologisme désigne la capacité d’un ordre à se rendre *disputable* : c’est-à-dire à instituer une scène, un moment, un dispositif, où l’ordre peut être mis en épreuve, critiqué, reformulé, contesté, amendé. L’archicration est ce qui rend le pouvoir *opposable*, non dans un sens antagonique, mais dans un sens procédural, différé, dialogique. Elle prend la forme d’un contradictoire institué, d’un recours possible, d’un délai interprétatif, d’un droit de parole, d’un espace de reconfiguration. Là où l’arcalité fonde, où la cratialité opère, l’archicration dispute — et en disputant, elle garantit la *viabilité politique* du dispositif.
Mais ce qui fait la force heuristique du paradigme archicratique, c’est que ces trois pôles ne sont pas des catégories séparées, ni des moments successifs, mais des *dimensions co-présentes* de tout dispositif. Chacun peut dominer, se retrancher, se dissoudre, ou se substituer aux autres, mais leur tension constitue le *champ de régulation*. Ce trépied constitue ainsi une véritable *grille différentielle de lecture* des régulations : il permet de repérer ce qui fonde sans agir, ce qui agit sans fonder, ce qui opère sans être disputé — et, symétriquement, ce qui fonde tout en opérant, ce qui opère tout en permettant la contestation, ce qui conteste à partir d’un fondement consistant.
Mais ce qui fait la force heuristique du paradigme archicratique, c’est que ces trois pôles ne sont pas des catégories séparées, ni des moments successifs, mais des *dimensions co-présentes* de tout dispositif. Chacun peut dominer, se retrancher, se dissoudre, ou se substituer aux autres, mais leur tension constitue le *champ de régulation*. Ce trépied constitue ainsi une véritable *grille différentielle de lecture* des régulations : il permet de repérer ce qui fonde sans prise opératoire suffisante, ce qui agit sur fondement peu exposable, ce qui opère sous archicration neutralisée ou rendue pratiquement inopérante — et, symétriquement, ce qui fonde tout en opérant, ce qui opère tout en laissant place à la contestation, ce qui conteste à partir d’un fondement consistant.
Ce paradigme triadique ne repose donc pas sur une essence du politique, mais sur une condition de possibilité de la régulation. Il ne dit pas ce que *doit être* un bon régime, mais ce qui fait qu’un ordre peut être dit *politique*, c’est-à-dire exposé à l’épreuve, fondé dans un sens, et transformateur d’une réalité. Dans un contexte où la scène politique se disloque, où les régulations deviennent silencieuses, où les fondements sont euphémisés et les disputes neutralisées, la force de ce paradigme est de *restituer une cartographie* du fait politique. Il ne suffit plus d’invoquer la légitimité, ni de dénoncer la technicisation : il faut *déplier* chaque dispositif en ses composantes fondamentales — fondation, opération, régulation — et en comprendre les combinaisons, les déséquilibres, les absences, les ruptures.
Ce paradigme triadique ne repose donc pas sur une essence du politique, mais sur une condition de possibilité de la régulation. Il ne dit pas ce que *doit être* un bon régime, mais ce qui fait qu’un ordre peut être dit *politique*, c’est-à-dire exposé à l’épreuve, fondé dans un sens, et transformateur d’une réalité. Dans un contexte où la scène politique se disloque, où les régulations deviennent silencieuses, où les fondements sont euphémisés et les disputes neutralisées, la force de ce paradigme est de *restituer une cartographie* du fait politique. Il ne suffit plus d’invoquer la légitimité, ni de dénoncer la technicisation : il faut *déplier* chaque dispositif en ses composantes fondamentales — fondation, opération, régulation — et en comprendre les combinaisons, les déséquilibres, les mises en latence, les neutralisations et les ruptures d’articulation.
C’est sur cette base que se construiront, dans les sections suivantes, une typologie des arcalités, des cratialités et des archicrations, une cartographie interne/externe de leurs interactions, ainsi qu’une analyse critique de leurs configurations pathologiques. Ce triptyque constitue la structure paradigmatique minimale pour analyser les dispositifs politiques en dehors des grilles souverainistes, contractualistes ou fonctionnalistes. Et c’est précisément en les confrontant à la réalité — textes, objets, fonctions, temporalités, cas d’usage — que nous en testerons la robustesse.
@@ -92,7 +98,7 @@ Le terme de cratialité comme nous l’avons vu en préambule dérive ici du rad
Dans les régimes politiques classiques, on identifie la *cratialité* à l’exécutif : ministère, force publique, budget, bras armé. Mais cette lecture est à la fois trop réductrice et historiquement datée. La *cratialité* contemporaine excède largement les figures classiques du pouvoir d’État. Elle s’incarne dans des *algorithmes*, des *tableaux de bord*, des *logiciels d’évaluation*, des *systèmes de logistique globale*, des *normes ISO*, des *actes administratifs unilatéraux*, des *marchés publics automatisés*, des *protocoles cryptés*, des *formulaires numériques inaccessibles*. Elle peut être discrète, discrétionnaire, distribuée, voire disséminée jusqu’à devenir opaque. Mais elle est toujours active : elle fait advenir un effet de régulation.
C’est pourquoi la *cratialité* ne saurait être conçue comme un simple rouage intermédiaire entre la fondation (*arcalité*) et la révision (*archicration*). Elle n’est ni instrument, ni exécution pure. Elle possède sa logique propre, ses effets d’autonomisation, ses inerties internes, ses formes d’expansion ou de court-circuit. Elle peut fonctionner sans justification — c’est le cas des algorithmes non documentés — et sans épreuve — c’est le cas des procédures automatiques non opposables. C’est en cela qu’elle constitue l’un des points névralgiques du basculement archicratique : là où la *cratialité* devient *indépendante*, *autosuffisante*, *désarrimée*, elle cesse d’être un vecteur de régulation pour devenir un vecteur de domination silencieuse.
C’est pourquoi la *cratialité* ne saurait être conçue comme un simple rouage intermédiaire entre la fondation (*arcalité*) et la révision (*archicration*). Elle n’est ni instrument, ni exécution pure. Elle possède sa logique propre, ses effets d’autonomisation, ses inerties internes, ses formes d’expansion ou de court-circuit. Elle peut fonctionner comme si elle n’avait plus à répondre explicitement de ses justifications — c’est le cas des algorithmes non documentés — et comme si la scène d’épreuve n’avait plus de prise effective sur son cours ordinaire — c’est le cas des procédures automatiques rendues pratiquement non opposables. C’est en cela qu’elle constitue l’un des points névralgiques du basculement archicratique : là où la *cratialité* devient *indépendante*, *autosuffisante*, *désarrimée*, elle cesse d’être un vecteur de régulation pour devenir un vecteur de domination silencieuse.
Or, comme l’*arcalité*, la *cratialité* se donne elle aussi selon une double topologie : interne et externe. Une *cratialité* est dite *interne* lorsqu’elle repose sur les moyens d’action propres au dispositif : administration, personnels, processus décisionnels, procédures codifiées, infrastructures spécifiques. Elle est dite *externe* lorsqu’elle mobilise des ressources extérieures : cabinets de conseil, normes transnationales, plateformes numériques privées, logistiques externalisées, financements conditionnés, technologies exogènes. Ce clivage interne/externe n’est pas secondaire : il est décisif pour comprendre les asymétries de pouvoir, les dépendances structurelles, les logiques de capture. Un hôpital, par exemple, peut être cratialisé de l’intérieur par son système de codage et ses tableaux d’affectation, ou de l’extérieur par des normes budgétaires européennes ou des logiciels achetés à des firmes privées.
@@ -108,11 +114,11 @@ Ce n’est pas la performance qui fait la validité politique d’une *cratialit
#### **L’*archicration* : ce qui dispute, conteste, met en épreuve, arbitre**
L’*archicration* constitue le troisième pôle du paradigme archicratique et, à bien des égards, son point d’incandescence politique : c’est elle qui transforme une régulation en un ordre véritablement *politique*. Là où l’*arcalité* fonde et où la *cratialité* opère, l’*archicration* institue l’épreuve, ouvre la dispute, rend possible la contestation organisée, et par là confère à l’ordre de régulation une dimension dialogique et réflexive. Sans *archicration*, une régulation n’est qu’un flux ; le pouvoir n’est qu’un automatisme ; le politique s’efface derrière la procédure. L’*archicration* est donc la scène instituée du différé, du contradictoire, du droit de regard et de la reprise.
L’archicration constitue le troisième pôle du paradigme archicratique et, à bien des égards, son point d’incandescence politique : c’est elle qui transforme une régulation en un ordre véritablement politique. Là où l’arcalité fonde et où la cratialité opère, l’archicration institue l’épreuve, ouvre la dispute, rend possible la contestation organisée, et confère à l’ordre de régulation une dimension dialogique et réflexive. Lorsqu’elle est neutralisée, mimée, relocalisée hors d’atteinte ou rendue pratiquement inopérante, la régulation tend à devenir une pure mécanique ; lorsqu’elle est tenue comme scène effective de reprise, elle devient une architecture habitable.
Le terme lui-même est construit à partir de *ἀρχὴ* (principe, origine, autorité) et *κρατέω* (être ou devenir le maître). Il désigne donc la capacité d’un dispositif de régulation à se rendre *opposable et amendable* dans un cadre institué : une instance, un rituel, une temporalité qui permet aux acteurs concernés de formuler, argumenter, contester, reconfigurer. C’est l’anti-mimétisme de la scène : non pas faire semblant d’écouter, mais *produire les conditions réelles* de l’écoute, du délai, du recours, de la révision. L’*archicration* n’est pas une option morale, ni un supplément d’âme procédural ; elle est la condition même de l’habitation politique d’un ordre, le fait d’en être maître, d’en devenir autorité.
Historiquement, les sociétés se sont dotées de formes d’*archicration* très variées : tribunaux, parlements, assemblées populaires, chambres de recours, médiations rituelles, procédures de droit coutumier, juridictions supra-étatiques. Ce sont des *scènes instituantes* qui ralentissent l’acte, l’exposent, l’ouvrent à la pluralité, et, ce faisant, lui confèrent une légitimité durable. Mais ces scènes peuvent se fermer, se vider, se fragmenter, ou être capturées. Dans les dispositifs archicratiques contemporains, l’*archicration* est souvent absente, mimée ou neutralisée. On convoque une « consultation publique » dont les résultats sont déjà fixés ; on ouvre une « plateforme participative » dont les algorithmes filtrent les réponses ; on institue des « délais de recours » dont la brièveté ou la complexité les rend impraticables. Il en résulte un effet d’évanescence de la scène : elle est là en apparence, mais elle n’opère plus en réalité.
Historiquement, les sociétés se sont dotées de formes d’*archicration* très variées : tribunaux, parlements, assemblées populaires, chambres de recours, médiations rituelles, procédures de droit coutumier, juridictions supra-étatiques. Ce sont des *scènes instituantes* qui ralentissent l’acte, l’exposent, l’ouvrent à la pluralité, et, ce faisant, lui confèrent une légitimité durable. Mais ces scènes peuvent se fermer, se vider, se fragmenter, ou être capturées. Dans les dispositifs archicratiques contemporains, l’archicration est souvent neutralisée, mimée, relocalisée hors d’atteinte ou rendue pratiquement inopérante. On convoque une « consultation publique » dont les résultats sont déjà fixés ; on ouvre une « plateforme participative » dont les algorithmes filtrent les réponses ; on institue des « délais de recours » dont la brièveté ou la complexité les rend impraticables. Il en résulte un effet d’évanescence de la scène : elle est là en apparence, mais elle n’opère plus en réalité.
C’est pourquoi l’analyse archicratique exige de penser l’*archicration* en tant que *dimension constitutive*, et non comme simple accessoire. Comme pour l’*arcalité* et la *cratialité*, l’*archicration* se déploie selon une double topologie : elle peut être interne — lorsqu’un dispositif institue en son sein des mécanismes de contestation et d’ajustement (conseils délibératifs, commissions de médiation, audits internes, organes de gouvernance participative) — ou externe — lorsqu’il est soumis à des scènes de contrôle ou d’épreuve extérieures (juridictions supranationales, contre-pouvoirs citoyens, presse, ONG, instances internationales). Ces deux modalités sont cruciales : un dispositif peut être très opposable en externe mais totalement fermé en interne, ou inversement. La santé démocratique d’une régulation se mesure à la combinaison des deux.
@@ -122,7 +128,7 @@ L’*archicration* ne se limite pas à ce qui autorise la critique ; elle est ce
Enfin, comme pour l’*arcalité* et la *cratialité*, l’*archicration* doit pouvoir être mesurée, qualifiée, falsifiée. Il ne suffit pas d’affirmer qu’une scène existe ; il faut interroger sa qualité : est-elle accessible, proportionnée, pluraliste, transparente, effective ? Est-elle un véritable lieu de remise en cause ou une simple mise en scène ? Ces questions sont au cœur de l’analyse archicratique. Elles seront systématisées dans la section 1.6, où nous formulerons des critères d’opposabilité et d’authentification, et testées empiriquement dans les études de cas du chapitre.
Ainsi, l’*archicration* complète le triptyque fondateur du paradigme archicratique. Sans elle, la régulation n’est qu’un flux technique ; avec elle, elle redevient un ordre politique. Elle n’est pas un supplément éthique, mais une condition de viabilité. Elle n’est pas un idéal abstrait, mais une variable concrète, observable, localisable, qui signe la différence entre un dispositif gouvernant et un dispositif administrant. Et c’est précisément parce qu’elle est aujourd’hui menacée, neutralisée ou mimée que le paradigme archicratique s’impose : pour rendre visible ce qui se perd, pour diagnostiquer ce qui persiste, et pour imaginer ce qui pourrait s’instituer.
Ainsi, l’*archicration* complète le triptyque fondateur du paradigme archicratique. Privée d’archicration effective, la régulation tend à n’être plus qu’un flux technique ; lorsqu’une scène de reprise demeure praticable, elle redevient un ordre politique. Elle n’est pas un supplément éthique, mais une condition de viabilité. Elle n’est pas un idéal abstrait, mais une variable concrète, observable, localisable, qui signe la différence entre un dispositif gouvernant et un dispositif administrant. Et c’est précisément parce qu’elle est aujourd’hui menacée, neutralisée ou mimée que le paradigme archicratique s’impose : pour rendre visible ce qui se perd, pour diagnostiquer ce qui persiste, et pour imaginer ce qui pourrait s’instituer.
Les trois prises ne se subsument jamais l’une l’autre : l’*arcalité* fonde, la *cratialité* oblige, l’*archicration* met en controverse réglée. Aucune ne vaut sans les deux autres ; toute lecture binaire serait régressive.
@@ -214,9 +220,9 @@ Chaque *objet d’archicration* doit donc être interrogé selon ces critères.
Enfin, il convient d’insister sur la topologie des *objets archicratifs*. Ils peuvent être internes : conseils délibératifs, comités d’éthique, médiateurs, dispositifs de saisine interne, commissions de réexamen. Ils peuvent être externes : recours juridictionnels, mobilisation d’instances indépendantes (Défenseur des droits, Conseil d’État, Autorité de régulation), interventions médiatiques documentées, pétitions publiques structurées, tribunaux populaires. Un système politique robuste combine les deux : il s’auto-dispute en interne, mais se rend aussi disputable de l’extérieur.
Dans des configurations archicratiques, les *objets d’archicration* sont souvent mimés en interne — pour donner l’illusion de la scène — tandis que les dispositifs externes d’opposabilité sont rendus inopérants, dilués ou purement formels. Ce n’est pas qu’un régime serait ouvertement archicratique ; c’est que certaines fonctions, certaines séquences ou certains vecteurs de régulation se déploient de manière archicratique : par neutralisation de l’épreuve, occultation des fondements et saturation opératoire. L’*archicratie* ne désigne donc pas une oligarchie instituée, mais une modalité silencieuse de la régulation qui peut traverser tous les régimes, tous les secteurs, et dont certains groupes sociaux ou fonctions deviennent les porteurs structurels — sans en revendiquer pour autant le nom. Nous les appelons ici : les *archicrates*.
Dans des configurations autarchicratiques, les objets d’archicration sont souvent mimés en interne — pour donner l’illusion de la scène — tandis que les dispositifs externes d’opposabilité sont rendus inopérants, dilués ou purement formels. Ce n’est pas qu’un régime serait ouvertement autarchicratique ; c’est que certaines fonctions, certaines séquences ou certains vecteurs de régulation se déploient selon une logique autarchicratique : par neutralisation de l’épreuve, occultation des fondements et saturation opératoire. L’autarchicratie ne désigne donc pas une oligarchie instituée, mais une dérive silencieuse de la régulation qui peut traverser tous les régimes, tous les secteurs, et dont certains groupes sociaux ou certaines fonctions deviennent les porteurs structurels — sans en revendiquer pour autant le nom. Nous les appellerons ici, si l’on veut les nommer rigoureusement : des porteurs de fermeture autarchique.
Car c’est bien cela qui est en jeu : non pas l’existence théorique d’une contestation, mais sa possibilité instituée, régulée, documentée. Là où l’archicration est absente, le pouvoir devient indiscutable — non par tyrannie explicite, mais par dissolution des conditions de la scène. Là où elle est simulée, le politique devient un théâtre d’ombres. Là où elle est instituée, explicite, traçable, elle redonne au pouvoir sa dimension dialogique, sa dimension temporelle, sa dimension politique.
Car c’est bien cela qui est en jeu : non pas l’existence théorique d’une contestation, mais sa possibilité instituée, régulée, documentée. Là où l’archicration est neutralisée, mimée, relocalisée hors d’atteinte ou rendue pratiquement inopérante, le pouvoir tend à devenir indiscutable — non par tyrannie explicite, mais par dissolution des conditions effectives de la scène. Là où elle est simulée, le politique devient un théâtre d’ombres. Là où elle est instituée, explicite, traçable et opposable, elle redonne au pouvoir sa dimension dialogique, sa dimension temporelle, sa dimension politique.
Les *objets archicratifs* ne sont donc ni accessoires ni résiduels : ils sont la clef de voûte du politique comme épreuve. Ce sont eux qui assurent qu’une norme n’est pas un destin, qu’une régulation peut être reconfigurée, qu’un ordre n’est pas clos. Ce sont eux qui, au sein de la structure archicratique, permettent que le pouvoir soit tenu pour comptable, et donc transformable.
@@ -226,7 +232,7 @@ Il est nécessaire mais non suffisant d’identifier des principes abstraits —
Un badge d’accès, un formulaire de recours, un tableau de bord algorithmique, une charte d’éthique, une convocation à une commission, un registre de délibération, un logiciel de pilotage budgétaire ou un protocole de saisine sont autant d’objets d’apparence neutre — mais qui, situés dans un écosystème de régulation, expriment une position archicratique : ils sont les métonymies de la structure, au sens où chaque objet, bien qu’apparemment partiel, renvoie à l’ensemble du régime dont il procède.
Un badge sans nom, sans fonction lisible, sans statut accessible ne signale pas seulement un problème d’accès : il objective une dissociation entre *cratialité* et *archicration*, en empêchant la formulation de toute contestation localisée. Une charte d’entreprise affichée dans un hall, non opposable juridiquement, manifeste une *arcalité* de façade, sans ancrage normatif. Un algorithme qui produit des décisions sans en publier les règles institue une *cratialité* opaque, sans archicration possible. Ce sont là des objets discrets — mais qui configurent le pouvoir, organisent l’action, orientent la reconnaissance, verrouillent les possibilités de mise en cause.
Un badge sans nom, sans fonction lisible, sans statut accessible ne signale pas seulement un problème d’accès : il objective une dissociation entre *cratialité* et *archicration*, en empêchant la formulation de toute contestation localisée. Une charte d’entreprise affichée dans un hall, non opposable juridiquement, manifeste une *arcalité* de façade, sans ancrage normatif. Un algorithme qui produit des décisions sans en publier les règles institue une *cratialité* opaque, sans archicration effective possible. Ce sont là des objets discrets — mais qui configurent le pouvoir, organisent l’action, orientent la reconnaissance, verrouillent les possibilités de mise en cause.
Lire une régulation politiquement, c’est donc lire les objets qui la rendent possible, et interroger la grammaire que ces objets rendent visible ou invisible. Or, cette grammaire ne peut être saisie qu’en articulant trois dimensions :
@@ -244,11 +250,11 @@ Mais cette lecture métonymique ne s’oppose pas à une lecture systémique : a
Si le paradigme archicratique repose sur cette tripartition fondamentale, il serait erroné d’en faire une grille statique ou une typologie figée. Ce qui fait sa valeur heuristique ne se résume pas en la distinction de ces trois pôles, mais dans la mise en tension dynamique qui les relie, les contraint, les déséquilibre ou les ajuste dans toute situation de régulation. Loin d’être des sphères séparées, ces trois dimensions n’existent que dans leur interaction mutuelle, dans une dialectique structurante où chacune affecte la viabilité des deux autres. C’est cette logique d’interdépendance — parfois harmonieuse, souvent tendue, parfois rompue — qui constitue la véritable mécanique différentielle de l’analyse archicratique.
L’*arcalité* peut exister sans *cratialité* : c’est le cas des idéaux solennels sans effectuation, des textes fondateurs devenus vestiges, des récits politiques non suivis d’action. La *cratialité* peut se déployer sans *arcalité* : c’est le régime du pur instrument, de la machine sans justification, du dispositif opératoire sans horizon. L’*archicration* peut être instituée sans prise réelle : c’est la critique sans effet, le recours sans impact, la procédure désarmée. Chacun de ces désajustements produit des formes pathologiques de régulation, non par la faute d’un défaut total, mais par la déconnexion des pôles entre eux. L’*archicratie* ne devient intelligible qu’en mesurant le degré d’articulation effective — et non déclarative — entre ces trois dimensions.
L’arcalité peut subsister à l’état de vestige, de référence peu opérante ou de fondement faiblement mobilisé ; la cratialité peut s’autonomiser sur fond d’arcalité peu exposable ou faiblement assumée ; l’archicration peut être formellement instituée tout en restant sans prise réelle sur la trajectoire décisionnelle. Chacun de ces désajustements produit des formes pathologiques de régulation, non par la faute d’un défaut absolu, mais par la déconnexion ou la dégradation des pôles entre eux. L’archicratie ne devient intelligible qu’en mesurant le degré d’articulation effective — et non simplement déclarative — entre ces trois dimensions.
Pour cette raison, il est nécessaire de substituer à la figure du triangle une figure tensorielle, où les forces de chaque pôle exercent une poussée, un rappel ou un affaissement sur les deux autres. Toute régulation produit ainsi un champ de forces, qui peut être tendu, équilibré, instable ou disjoint. L’*arcalité* oriente le sens ; la *cratialité* donne prise au réel ; l’*archicration* introduit le différé, le possible, l’inédit. Leur co-présence est la condition minimale d’un ordre politique habitable. Leur absence ou leur domination univoque constitue, à l’inverse, le signe d’un régime appauvri, voire fermé et autoritaire.
Pour cette raison, il est nécessaire de substituer à la figure du triangle une figure tensorielle, où les forces de chaque pôle exercent une poussée, un rappel ou un affaissement sur les deux autres. Toute régulation produit ainsi un champ de forces, qui peut être tendu, équilibré, instable ou disjoint. L’*arcalité* oriente le sens ; la *cratialité* donne prise au réel ; l’*archicration* introduit le différé, le possible, l’inédit. Leur co-présence est la condition minimale d’un ordre politique habitable. Leur neutralisation, leur mise en latence ou leur domination univoque constitue, à l’inverse, le signe d’un régime appauvri, voire fermé et autoritaire.
Ce n’est donc pas la présence de chacun des trois pôles, pris isolément, qui garantit la qualité politique d’une régulation, mais la manière dont ils se répondent, se nourrissent, se corrigent ou s’affrontent. Une *arcalité* active, mais non révisable, produit du dogmatisme. Une *cratialité* hyper-efficiente sans fondement engendre de la brutalité technicienne. Une *archicration* procédurale sans fondation partagée tourne à vide sans adhésion dans le pur arbitraire. L’analyse archicratique repose alors sur un principe de consistance dynamique : *toute régulation politiquement viable est un régime où la légitimité fondatrice, la puissance opératoire et la scène contradictoire s’équilibrent par ajustements réciproques.*
Ce n’est donc pas la présence de chacun des trois pôles, pris isolément, qui garantit la qualité politique d’une régulation, mais la manière dont ils se répondent, se nourrissent, se corrigent ou s’affrontent. Une *arcalité* active, mais non révisable, produit du dogmatisme. Une *cratialité* hyper-efficiente à fondement peu exposable ou désarrimé engendre de la brutalité technicienne. Une *archicration* procédurale sans fondation partagée tourne à vide sans adhésion dans le pur arbitraire. L’analyse archicratique repose alors sur un principe de consistance dynamique : *toute régulation politiquement viable est un régime où la légitimité fondatrice, la puissance opératoire et la scène contradictoire s’équilibrent par ajustements réciproques.*
Cette dynamique se lit dans la manière dont une règle renvoie à une doctrine, dont un instrument laisse place à une critique, dont une norme peut évoluer par révision. Prenons un exemple : un protocole sanitaire. Il peut être fondé (*arcalité* scientifique ou juridique), appliqué (*cratialité* technique et administrative), mais si aucune révision n’est possible en cas de controverse, l’*archicration* fait défaut — et le dispositif devient pure contrainte et imposition. À l’inverse, une procédure ouverte à la contestation qui ne serait pas adossée à un fondement clair produirait du désordre, du flottement ou de l’épuisement normatif.
@@ -262,11 +268,11 @@ Ainsi, loin de reposer sur une addition de fonctions ou un empilement de princip
Encore faut-il que notre construction tripartite permette de repérer les régulations qui échouent, non par absence de dispositifs, mais par déséquilibre interne entre les trois pôles constitutifs du paradigme. C’est précisément dans ces cas limites — ces régimes pathologiques où un seul pôle écrase ou court-circuite les autres — que la robustesse analytique du paradigme archicratique peut se vérifier. Ce ne sont pas des anomalies anecdotiques, mais des figures structurantes de la régulation contemporaine, des formes typiques de désarticulation qui signent l’entrée dans une zone grise du politique : ni complètement illégitime, ni ouvertement dictatoriale, mais oblitérant l’opposabilité.
La première configuration critique est celle de la *cratialité* orpheline, où la puissance d’agir est autonome, sans fondement clair, ni scène d’épreuve possible. On y observe des chaînes d’exécution hautement performantes — plateformes logistiques, tableaux de pilotage budgétaire, algorithmes décisionnels, procédures automatisées — qui fonctionnent sans invocation fondatrice (*arcalité*) et sans recours institué (*archicration*). La décision devient impersonnelle, inattaquable, et souvent non négociable. La puissance technique se substitue à la justification et à l’épreuve.
La première configuration critique est celle de la cratialité orpheline, où la puissance d’agir tend à s’autonomiser sur fond d’arcalité faiblement exposable et d’archicration neutralisée, comprimée ou rendue pratiquement inopérante. On y observe des chaînes d’exécution hautement performantes — plateformes logistiques, tableaux de pilotage budgétaire, algorithmes décisionnels, procédures automatisées — qui fonctionnent avec des fondements peu explicités pour les affectés et avec des voies de reprise, de contestation ou de recours soit relocalisées hors d’atteinte, soit fictives, soit matériellement impraticables. La décision y devient impersonnelle, difficilement attaquable, et souvent non négociable dans les faits. Ce qui caractérise cette configuration n’est donc pas l’absence absolue de fondement ou de scène, mais leur dégradation opératoire : l’arcalité subsiste sous forme implicite, technique ou déléguée ; l’archicration subsiste comme possibilité affaiblie, mimée ou neutralisée ; tandis que la cratialité, elle, concentre l’effectivité.
La seconde figure limite est celle de l’*arcalité* désincarnée, où les récits fondateurs subsistent — parfois en majesté — mais ne produisent plus d’effet régulateur. Constitutions vénérées, serments solennels, principes éthiques, déclarations universelles sont mobilisés dans les discours, affichés dans les chartes, mais ne traversent plus les dispositifs opératoires. Il en résulte un régime de performativité déconnectée : les institutions prétendent être fondées en droit ou en humanisme, mais leur fonctionnement réel s’émancipe totalement de ce socle. L’*arcalité* devient alors décorative, voire cyniquement instrumentalisée : c’est le cas des appels républicains dans des politiques autoritaires, des valeurs affichées dans des dispositifs inaccessibles, ou des codes d’éthique utilisés pour justifier des dispositifs de surveillance. Ce désajustement génère une forme de désorientation politique profonde, où la norme déclarée et la norme opératoire ne se rejoignent plus.
La troisième pathologie est celle de l’*archicration* fictive, où des scènes de dispute sont instaurées, mais sans effectivité réelle. Le dispositif donne l’apparence d’un contradictoire — consultation publique, boîte à idées, forum participatif, convention citoyenne, droit de recours — mais en réalité, aucune des conditions de l’opposabilité n’est remplie : délais trop courts, opacité des motifs, non-publication des réponses, filtrage des revendications, absence de prise sur la décision finale. Cette forme de mise en scène procédurale produit une illusion de démocratie régulée, où la scène d’épreuve est ritualisée sans effet. L’*archicration* existe ici comme spectacle, non comme épreuve, et contribue paradoxalement à renforcer l’irréversibilité des décisions prises. C’est le cas par exemple de certaines concertations environnementales menées en urgence, où les contributions citoyennes sont enregistrées, mais sans être prises en compte ni motivées dans les décisions finales.
La troisième pathologie est celle de l’*archicration* fictive, où des scènes de dispute sont instaurées, mais sans effectivité palpable. Le dispositif donne l’apparence d’un contradictoire — consultation publique, boîte à idées, forum participatif, convention citoyenne, droit de recours — mais en réalité, aucune des conditions de l’opposabilité n’est remplie : délais trop courts, opacité des motifs, non-publication des réponses, filtrage des revendications, absence de prise sur la décision finale. Cette forme de mise en scène procédurale produit une illusion de démocratie régulée, où la scène d’épreuve est ritualisée sans effet. L’*archicration* existe ici comme spectacle, non comme épreuve, et contribue paradoxalement à renforcer l’irréversibilité des décisions prises. C’est le cas par exemple de certaines concertations environnementales menées en urgence, où les contributions citoyennes sont enregistrées, mais sans être prises en compte ni motivées dans les décisions finales.
Mais il existe aussi des cas où un seul pôle détourne ou capture les deux autres. On parlera alors de court-circuit paradigmatique, ou d’effet de surcodage. Ainsi, une cratialité surpuissante peut simuler un fondement (en produisant des justifications pseudo-techniques ou tautologiques : « c’est la procédure », « l’algorithme l’a déterminé »), tout en neutralisant la scène de l’épreuve (« ce n’est pas contestable, car c’est automatisé »). Nous nommons ce type de dérive : *régime hypercratial*.
@@ -274,9 +280,9 @@ Mais il existe aussi des cas où un seul pôle détourne ou capture les deux aut
Enfin, une *archicration* *hypertrophiée* — une société saturée de recours, d’instances, de médiations — peut empêcher toute effectuation : c’est la régulation paralysée par la sur-opposabilité, la scène d’épreuve transformée en labyrinthe.
Ces cas limites ne sont pas simplement pathologiques : ils sont heuristiquement structurants. Ils montrent comment l’équilibre entre les pôles ne relève ni d’une essence ni d’une norme, mais d’un jeu d’ajustement toujours instable, toujours à documenter. C’est dans leur désajustement que se révèlent les conditions minimales de viabilité politique d’un dispositif. Car un ordre peut survivre longtemps sans fondement (arcalité), ou sans épreuve (archicration), ou même sans action (cratialité), mais il cesse alors d’être un ordre politique habitable : il devient un régime bloqué de contrainte, de vacance ou d’impuissance.
Ces cas limites ne sont pas simplement pathologiques : ils sont heuristiquement structurants. Ils montrent comment l’équilibre entre les pôles ne relève ni d’une essence ni d’une norme, mais d’un jeu d’ajustement toujours instable, toujours à documenter. C’est dans leur désajustement que se révèlent les conditions minimales de viabilité politique d’un dispositif. Car une formation collective peut se maintenir durablement sous déficit d’arcalité, sous atrophie de l’archicration, ou sous affaissement de la cratialité ; mais elle cesse alors d’être un ordre politique pleinement habitable. Elle persiste au prix d’une dégradation de sa co-viabilité, et tend à devenir un régime bloqué de contrainte, de vacance ou d’impuissance.
Enfin, ces déséquilibres ne doivent pas être lus comme des erreurs de conception ou des dysfonctionnements ponctuels. Ils signalent souvent des régimes intentionnels de régulation, où l’absence de fondement, la fermeture de la dispute ou la saturation de l’opération sont stratégiquement construits pour éviter l’épreuve politique. C’est ici que le paradigme archicratique déploie sa force critique : en rendant visibles ces dispositifs qui, tout en étant fonctionnels, se soustraient à la critique, à la scène, à l’épreuve.
Enfin, ces déséquilibres ne doivent pas être lus comme des erreurs de conception ou des dysfonctionnements ponctuels. Ils signalent souvent des régimes intentionnels de régulation, où le défaut d’exposition du fondement, la neutralisation de la dispute ou la saturation de l’opération sont stratégiquement construits pour éviter l’épreuve politique. C’est ici que le paradigme archicratique déploie sa force critique : en rendant visibles ces dispositifs qui, tout en étant fonctionnels, se soustraient à la critique, à la scène, à l’épreuve.
Mais d’ores et déjà, cette exploration des désajustements initiaux nous permet de poser une hypothèse structurante : une régulation devient politiquement problématique non lorsqu’un pôle manque absolument, mais lorsque ses interactions avec les autres sont rompues, perverties, neutralisées. Ce sont ces configurations, ces bifurcations, ces effets de bascule que l’analyse archicratique doit pouvoir capter, nommer, décrire et critiquer.
@@ -302,7 +308,7 @@ La forme, en revanche, relève d’un niveau intermédiaire. Elle désigne les m
Mais en-dessous de ces formes, il y a la structure régulatrice : un plan plus profond, plus transversal, plus déterminant, qui ne désigne ni les institutions ni les pratiques, mais les conditions même de possibilité de la régulation politique. Cette structure est ce qui permet — ou non — qu’un ordre se rende visible, controversable, habitable. Elle ne renvoie pas à une architecture constitutionnelle, mais à une configuration d’agencement entre ce qui fonde, ce qui opère, et ce qui permet d’en discuter. C’est là que se situe l’hypothèse archicratique : non au niveau des régimes ou des formes, mais au niveau de la structure régulatrice profonde, ce que l’on pourrait appeler, en empruntant à Lévi-Strauss ou à Althusser, un *inconscient politique de la régulation*.
L’*archicratie* n’est donc pas un régime de plus. Elle n’est pas la cousine néolibérale de la démocratie, ni une dérive technocratique de l’État-nation. Elle est le nom donné à une structure de régulation sans scène instituée, sans fondement invocable, sans épreuve contradictoire formelle. Et à ce titre, elle peut traverser différents régimes, s’y superposer, y introduire des failles ou des inerties, s’y agglomérer comme un parasitage silencieux. Un régime peut être démocratique en apparence, mais archicratique dans sa structure opératoire réelle. Inversement, certaines formes autoritaires peuvent ménager des archicrations ponctuelles, permettant une conflictualité politique minimale. C’est ce qui rend l’archicratie si difficile à identifier, et si nécessaire à penser : elle ne se lit pas dans les institutions, mais dans la manière dont un ordre rend — ou non — la régulation opposable, différée, arbitrable.
L’autarchicratie n’est donc pas un régime politique de plus. Elle n’est ni la cousine néolibérale de la démocratie, ni une simple dérive technocratique de l’État-nation. Elle désigne la dérive par laquelle des architectures régulatrices continuent d’opérer tout en se soustrayant progressivement à la scène d’épreuve : les fondements ne disparaissent pas nécessairement, mais deviennent de moins en moins susceptibles d’exposition ; les procédures contradictoires peuvent subsister, mais se vident, se simulent ou deviennent inopérantes ; et la régulation tend à se refermer sur sa propre logique d’exécution. À ce titre, l’autarchicratie peut traverser différents régimes politiques, s’y superposer, y introduire des inerties, des fermetures ou des parasitages silencieux. Un régime peut ainsi être démocratique en apparence, tout en dérivant vers une structure opératoire autarchicratique. Inversement, certaines formes autoritaires peuvent encore ménager des archicrations ponctuelles, permettant le maintien d’une conflictualité politique minimale. C’est ce qui rend l’autarchicratie difficile à identifier, et pourtant nécessaire à penser : elle ne se lit pas seulement dans les institutions, mais dans la manière dont un ordre rend — ou non — sa régulation exposable, opposable, différable et révisable.
Cette distinction entre régime, forme, et structure n’est pas simplement académique. Elle est politiquement décisive. Car c’est elle qui permet d’éviter deux écueils fréquents : le fétichisme institutionnel (croire qu’un régime démocratique suffit à garantir une régulation juste), et le scepticisme cynique (penser que toute régulation est toujours déjà close, dominée, irréformable). L’*archicratie* introduit un plan d’analyse transversal, qui oblige à questionner non ce que les institutions prétendent, mais ce qu’elles permettent réellement en termes de fondement, d’opération, et de dispute. Elle ouvre une lecture différentielle du politique, fondée non sur l’étiquette (démocratie ou non), mais sur la qualité régulatrice de l’ordre.
@@ -320,7 +326,7 @@ Dit autrement, un *méta-régime* n’est pas une forme politique particulière,
Cette grille permet par exemple de comparer un dispositif d’allocation d’aides sociales dans une démocratie représentative, un protocole sanitaire dans une dictature *soft*, ou un mécanisme d’ajustement budgétaire dans une structure supra-étatique. Ce qui compte, ce n’est pas l’étiquette politique du régime, mais la qualité de sa régulation au regard des trois critères fondamentaux : le fondement mobilisé (*arcalité*), les moyens d’effectuation (*cratialité*), et les possibilités instituées de contestation (*archicration*). L’*archicratie* devient ainsi un analyseur paradigmatique, c’est-à-dire une manière de lire des dispositifs en détectant ce qui s’y fonde, ce qui y opère, et ce qui s’y dispute — ou non.
Cette approche n’est pas purement descriptive. Elle est diagnostique, critique, opposable. Elle permet de dire : « ici, l’ordre régulateur semble robuste, mais il est archicratique en ce sens qu’il est devenu indisputable » ; ou bien : « ce dispositif mobilise un fondement fort, mais il est cratialisé sans contrôle, et sans scène de recours ». En ce sens, l’*archicratie* n’est pas un concept mollement critique : elle est une épreuve conceptuelle pour les régimes existants, un test de viabilité démocratique ou politique, une mise à nu des écarts entre l’invocation des principes et la structure réelle de la régulation.
Cette approche n’est pas purement descriptive. Elle est diagnostique, critique, opposable. Elle permet de dire : « ici, l’ordre régulateur semble robuste, mais il fonctionne désormais sur un mode autarchicratique, en ce qu’il devient indisputable » ; ou bien : « ce dispositif mobilise un fondement fort, mais sa cratialité se déploie avec un contrôle affaibli et une scène de recours neutralisée, comprimée ou rendue pratiquement inopérante ». En ce sens, l’archicratie n’est pas un concept mollement critique : elle est une épreuve conceptuelle pour les régimes existants, un test de viabilité politique, une mise à nu des écarts entre l’invocation des principes et la structure réelle de la régulation.
Mais surtout, cette conceptualisation du *méta-régime archicratique* permet de dépasser deux impasses majeures des théories politiques classiques. La première, *normative*, qui suppose qu’un régime est légitime parce qu’il respecte formellement certaines règles (élections, séparation des pouvoirs, État de droit). Or, ces règles peuvent subsister tandis que la régulation réelle devient opaque, automatique ou imperméable à la contestation.
@@ -353,7 +359,7 @@ Il devient ainsi possible, grâce à cette grille tripolaire, de qualifier des r
Mais encore faut-il que le paradigme soit explicatif, c’est-à-dire capable de rapprocher des phénomènes épars, de construire des séries intelligibles, de réduire la contingence apparente à des configurations discernables. Là encore, le paradigme archicratique s’impose par sa puissance intégratrice : il permet d’embrasser dans une même grille d’analyse des réalités qui, jusque-là, semblaient hétérogènes — parce que dispersées dans des domaines sectoriels (fiscalité, santé, climat, numérique, sécurité), ou parce qu’enveloppées dans des langages spécialisés (juridique, économique, algorithmique).
C’est précisément cette capacité à unifier sans uniformiser, à articuler sans réduire, qui signe la force d’un paradigme. En *archicratie*, des phénomènes aussi variés que — la gouvernance sanitaire par indicateurs ; la régulation budgétaire par règles automatiques ; la gestion algorithmique des droits sociaux ; l’application de standards techniques globaux sans débat local ; ou encore la prolifération des plateformes numériques de notification unilatérale — peuvent être reliés, compris, comparés à l’aune d’une même question : *cette régulation est-elle fondée, opérante et opposable ?* Si l’un de ces trois pôles fait défaut, la régulation devient fragile, injustifiable ou autoritaire. Ainsi, ce que le paradigme archicratique rend possible, c’est une lecture transversale, critique, intersectorielle du politique contemporain — non plus à partir des formes visibles de pouvoir, mais à partir de ses pratiques effectives de régulation, de ses vecteurs techniques, et de ses seuils de dispute.
C’est précisément cette capacité à unifier sans uniformiser, à articuler sans réduire, qui signe la force d’un paradigme. En *archicratie*, des phénomènes aussi variés que — la gouvernance sanitaire par indicateurs ; la régulation budgétaire par règles automatiques ; la gestion algorithmique des droits sociaux ; l’application de standards techniques globaux sans discussion locale ; ou encore la prolifération des plateformes numériques de notification unilatérale — peuvent être reliés, compris, comparés à l’aune d’une même question : *cette régulation est-elle fondée, opérante et opposable ?* Si l’un de ces trois pôles fait défaut, la régulation devient fragile, injustifiable ou autoritaire. Ainsi, ce que le paradigme archicratique rend possible, c’est une lecture transversale, critique, intersectorielle du politique contemporain — non plus à partir des formes visibles de pouvoir, mais à partir de ses pratiques effectives de régulation, de ses vecteurs techniques, et de ses seuils de dispute.
#### Une opposabilité empirique : falsifiabilité, indicateurs et critères de lecture
@@ -369,13 +375,13 @@ Ces critères sont au nombre de quatre, que nous avons déjà évoqués en filig
- La *possibilité de recours effectif* : une norme est politique si elle peut être contestée dans un délai raisonnable, devant une instance compétente, selon une procédure intelligible et des modalités non excluantes. Le formalisme du recours ne suffit pas ; c’est sa praticabilité qui importe.
Ces quatre indicateurs peuvent être mesurés, étayés et comparés. Ils permettent d’objectiver le diagnostic archicratique, d’en délivrer une cartographie différenciée, et d’en circonscrire les seuils. Ils rendent la critique opératoire et la théorie falsifiable. Car il est tout à fait possible qu’un dispositif tienne sans scène, sans délai, sans recours, sans fondement clair — dans ce cas, notre paradigme devra s’avouer inopérant. C’est précisément cette possibilité qui fonde sa valeur scientifique.
Ces quatre indicateurs peuvent être mesurés, étayés et comparés. Ils permettent d’objectiver le diagnostic archicratique, d’en délivrer une cartographie différenciée, et d’en circonscrire les seuils. Ils rendent la critique opératoire et la théorie falsifiable. Car il est tout à fait possible qu’un dispositif tienne avec une scène neutralisée ou relocalisée hors d’atteinte, avec des délais fictifs ou comprimés, avec des recours formellement ouverts mais pratiquement impraticables, avec des fondements difficiles à exposer ou à opposer. Dans ce cas, notre paradigme n’a pas à s’avouer inopérant : il doit précisément décrire, qualifier et mesurer cette neutralisation, cette compression, cette inopérance ou cette opacification. C’est précisément cette possibilité qui fonde sa valeur scientifique.
Par ailleurs, ce paradigme pourra être confronté à des contre-exemples méthodiques — ce que nous ferons dans la section 1.10 — afin de tester la puissance de sa réfutabilité. Il ne s’agit pas de tout faire entrer dans le moule archicratique, mais de tester la pertinence du moule face à des réalités rétives, résistantes, voire incompatibles. C’est cela, l’esprit de la critique : ne pas chercher la confirmation, mais la disjonction révélatrice.
En définitive, le paradigme archicratique ne vaut que par sa capacité à rendre visible ce qui opère sans justification, à déplier ce qui agit sans être discuté, à donner langue à ce qui neutralise le contradictoire. Il ne se présente pas comme une nouvelle théorie normative, ni comme un surplomb idéologique, mais comme une grille de détection des régulations silencieuses — celles qui se passent de scène, de délai, de motifs, de recours, et qui, de ce fait, court-circuitent la possibilité même du politique.
En définitive, le paradigme archicratique ne vaut que par sa capacité à rendre visible ce qui opère à travers des justifications faiblement exposables, à déplier ce qui agit à travers des scènes comprimées, mimées ou relocalisées, à donner langue à ce qui neutralise le contradictoire sans jamais l’abolir purement et simplement. Il ne se présente pas comme une nouvelle théorie normative, ni comme un surplomb idéologique, mais comme une grille de détection des régulations silencieuses — celles où la scène se vide, où les délais deviennent fictifs, où les motifs deviennent inopposables, où les recours cessent d’être pratiquement saisissables, et qui, de ce fait, court-circuitent la possibilité même du politique.
En ce sens, il est un paradigme à la fois critique et méthodique : critique, car il met à nu les absences, les évasions, les courts-circuits ; méthodique, puisqu’il les mesure, les nomme, les cartographie, et les expose à l’épreuve. Ce n’est qu’à cette double condition — fécondité analytique et opposabilité empirique — que le paradigme archicratique pourra prétendre à une légitimité scientifique, académique et politique. Et c’est sur cette base, et cette base seulement, qu’il pourra accompagner l’effort critique contemporain pour penser ce qui régule sans qu’on le voie, ce qui gouverne sans qu’on le dise, ce qui décide sans qu’on puisse le conteste.
En ce sens, il est un paradigme à la fois critique et méthodique : critique, car il met à nu les neutralisations, les évasions, les courts-circuits ; méthodique, puisqu’il les mesure, les nomme, les cartographie, et les expose à l’épreuve. Ce n’est qu’à cette double condition — fécondité analytique et opposabilité empirique — que le paradigme archicratique pourra prétendre à une légitimité scientifique, académique et politique.
## **1.3 —** Les axiomes régulateurs du paradigme archicratique
@@ -401,7 +407,9 @@ Ce premier axiome constitue le socle ontologique minimal du paradigme archicrati
Poser cet axiome, c’est affirmer que ces trois dimensions — même lorsqu’elles sont invisibles, réduites, capturées ou en dormance — sont toujours présentes, repérables, détectables, au moins de façon latente ou implicite. Autrement dit, on ne peut pas penser une régulation sans ce tripode, même si celui-ci est déséquilibré, distordu ou saboté. Un dispositif qui n’aurait ni *arcalité* (aucun principe fondateur ou récit de justification), ni *cratialité* (aucune opération effective, aucun vecteur d’action), ni *archicration* (aucun lieu ou possibilité de contestation ou de révision), ne serait pas un dispositif régulateur : ce serait un chaos, une contingence brute, un pur hasard ou un agrégat sans structure.
Ce que cet axiome implique immédiatement, c’est un changement de regard épistémique. Il ne suffit pas de repérer les structures visibles du pouvoir (institutions, lois, autorités) : il faut traquer les trois prises constitutives dans tout agencement régulateur — y compris ceux qui prétendent ne pas être politiques, comme par exemple, les régulations algorithmiques, les logiques d’attribution budgétaire, les procédures hospitalières, les normes de conformité technique. Même là, il y a fondement, opération, et possibilité de contestation — et, au pire, leur mise en invisibilité stratégique. De sorte que ce n’est pas parce que l’*arcalité* est devenue silencieuse (par exemple : une légitimation implicite par la science ou par la nécessité technique), ni parce que l’*archicration* est éteinte (absence de scène de recours), que ces dimensions sont absentes : elles sont alors refoulées, verrouillées, court-circuitées — mais leur absence relative est elle-même un indice paradigmatique.
Ce que cet axiome implique immédiatement, c’est un changement de regard épistémique. Il ne suffit pas de repérer les structures visibles du pouvoir (institutions, lois, autorités) : il faut traquer les trois prises constitutives dans tout agencement régulateur — y compris ceux qui prétendent ne pas être politiques, comme par exemple, les régulations algorithmiques, les logiques d’attribution budgétaire, les procédures hospitalières, les normes de conformité technique. Même là, il y a fondement, opération, et possibilité de contestation — et, au pire, leur mise en invisibilité stratégique. De sorte que ce n’est pas parce que l’*arcalité* est devenue silencieuse (par exemple : une légitimation implicite par la science ou par la nécessité technique), ni parce que l’*archicration* est neutralisée, verrouillée, relocalisée hors d’atteinte ou rendue pratiquement inopérante (absence de scène de recours), que ces dimensions sont absentes : elles sont alors refoulées, verrouillées, court-circuitées — mais leur absence relative est elle-même un indice paradigmatique.
Toute régulation effective comporte, en acte ou en puissance, une arcalité, une cratialité et une archicration ; les pathologies contemporaines n’abolissent pas cette tripolarité, mais en opacifient les fondements, en autonomisent les opérations et en neutralisent, miment ou relocalisent la scène d’épreuve, jusqu’à la dérive autarchicratique.
L’*axiome de coprésence* a donc une valeur heuristique fondamentale. Il permet de penser les situations où une régulation semble purement technique ou exclusivement administrative (*cratialité*) comme étant toujours aussi fondée dans un imaginaire (*arcalité*) et exposée ou soustraite à une épreuve (*archicration*). Il impose ainsi une lecture structurale de tout dispositif, qu’il soit juridique, logistique, algorithmique, sanitaire, éducatif, écologique ou financier. C’est une invitation à lire dans la matière du monde les tripodes de la régulation.
@@ -487,9 +495,9 @@ Autre réduction, lorsque tout est renvoyé à la dispute, à la consultation,
L’histoire politique moderne offre de nombreux exemples de ces régimes déséquilibrés : républiques fondées sur des textes constitutionnels irréprochables, mais pratiquement inopérantes ; dictatures fonctionnelles et performantes bien que sans légitimité ni controverse possible ; démocraties dites « délibératives » paralysées par l’inflation des consultations sans pouvoir réel. Chaque fois qu’un pôle tend à se substituer aux deux autres, la régulation se dégrade — soit en simulacre, soit en oppression, soit en chaos.
Ce que l’a*xiome de disjonction fonctionnelle* impose, c’est une lecture différentielle, exigeante, des configurations régulatrices. Il oblige à penser non seulement la présence des trois pôles, mais surtout leur non-confusion. Il n’est pas rare, en effet, que des dispositifs affichent un fondement alors qu’il ne s’agit que d’un effet de langage cratial ; qu’ils prétendent à la dispute alors qu’ils ne permettent aucun recours réel ; qu’ils opèrent à vide sans principe ni scène. L’*archicratie* elle-même, dans ses formes les plus avancées, mime les trois pôles tout en les court-circuitant — comme le montrent certaines plateformes de participation citoyenne entièrement encadrées par des algorithmes de tri et d’euphémisation, dites de modération.
Ce que l’axiome de disjonction fonctionnelle impose, c’est une lecture différentielle, exigeante, des configurations régulatrices. Il oblige à penser non seulement la présence des trois pôles, mais surtout leur non-confusion. Il n’est pas rare, en effet, que des dispositifs affichent un fondement alors qu’il ne s’agit que d’un effet de langage cratial ; qu’ils prétendent à la dispute alors qu’ils ne permettent aucun recours réel ; qu’ils opèrent à vide sans principe exposable ni scène praticable. Ce ne sont pas là des formes accomplies de l’archicratie, mais des configurations autarchicratiques ou archicratistiques dans lesquelles les trois pôles sont mimés, exhibés ou stylisés tout en étant partiellement court-circuités — comme le montrent certaines plateformes de participation citoyenne entièrement encadrées par des algorithmes de tri, de hiérarchisation ou d’euphémisation.
Autrement dit, ce que l’on croit parfois être une régulation équilibrée peut en réalité relever d’un effondrement fonctionnel déguisé, d’une substitution abusive, d’une délégation simulée. L’*axiome de disjonction fonctionnelle* agit alors comme outil de discernement critique : il permet de tester si un dispositif tient ensemble les trois fonctions, ou s’il glisse vers un monolithe politique — source de désajustement systémique.
Autrement dit, ce que l’on croit parfois être une régulation équilibrée peut en réalité relever d’un effondrement fonctionnel déguisé, d’une substitution abusive, d’une délégation simulée. L’axiome de disjonction fonctionnelle (1.3.4) vient ensuite avertir que l’effacement, la neutralisation ou l’hypertrophie d’un seul pôle conduit nécessairement à une régulation pathologique. De sorte qu’un ordre privé de fondement exposable, de scène d’épreuve praticable ou de capacité opératoire effective n’est ni pleinement viable, ni légitime, ni habitable.
Mais cet axiome a aussi une portée épistémologique et méthodologique. Il oblige l’analyste à disjoindre ce que la régulation tend à confondre. Il impose de ne pas se satisfaire d’un indicateur unique ou d’une déclaration de principe. Il exige de chercher, pour chaque dispositif : *Quelle est la source réelle de l’arcalité invoquée ? Quelle est la structure effective de la cratialité mobilisée ? Quelle est la scène concrète d’archicration instituée ?*
@@ -551,7 +559,7 @@ Cet axiome fonde également la dimension anti-dogmatique du paradigme archicrati
**Tout paradigme qui prétend décrire les régulations politiques doit rendre possible sa propre mise à l’épreuve. Il doit produire des instruments de falsifiabilité, de critique interne et de différenciation analytique. Un paradigme non falsifiable est un dogme ; un cadre non différenciateur est un slogan.**
Une régulation n’est réputée archicratique qu’à la condition d’instituer une scène contradictoire où les prétentions arcale et cratiale peuvent être opposées, arbitrées et révisées selon des procédures publiques et des indicateurs déclarés.
Une régulation n’est réputée pleinement archicratique qu’à la condition de rendre possible une scène d’épreuve où les prétentions arcale et cratiale puissent être opposées, arbitrées et révisées selon des formes de contestation effectives, lesquelles peuvent être publiques, instituées, rituelles, situées ou faiblement codifiées, pourvu qu’elles soient praticables, opposables et capables d’infléchir la régulation.
Il est une exigence trop oubliée des sciences sociales : un paradigme critique n’est pas une grille qui se plaque partout, mais une matrice qui se teste, qui s’affine, qui peut échouer. Toute proposition théorique sur le pouvoir, la régulation, l’opposabilité doit se soumettre à une exigence que Karl Popper formulait dès 1934 : la condition de scientificité minimale d’un énoncé est sa capacité à être réfuté. Autrement dit, un paradigme qui s’applique partout, tout le temps, sans jamais rencontrer de cas-limite, ne serait pas robuste : il serait vide.
@@ -614,7 +622,7 @@ L’*axiome de coprésence* (1.3.1) rappelle que tout dispositif de régulation,
L’*axiome de détectabilité* (1.3.3) pose que chaque pôle laisse des traces situées : visibles ou occultées, explicites ou latentes, mais toujours repérables dans des objets, des pratiques, des structures ou des langages. C’est cet ancrage dans des matérialités documentaires qui permet de différencier l’analyse archicratique de toute spéculation sans prise sur le réel.
L’*axiome de disjonction fonctionnelle* (1.3.4) vient ensuite avertir que l’absence ou l’hypertrophie d’un seul pôle conduit nécessairement à une régulation pathologique. De sorte qu’un ordre sans fondement, sans régulation ou sans opération n’est ni viable, ni légitime, ni habitable.
L’axiome de disjonction fonctionnelle (1.3.4) vient ensuite avertir que la neutralisation, la mise en latence ou l’hypertrophie d’un seul pôle conduit nécessairement à une régulation pathologique. De sorte qu’un ordre privé de fondement exposable, de scène d’épreuve praticable ou de capacité opératoire effective n’est ni viable, ni légitime, ni habitable.
Les axiomes suivants assurent la robustesse transversale du paradigme. L’*axiome de variabilité différentielle* (1.3.5) affirme que les formes prises par les trois pôles sont historiquement, culturellement, politiquement, technologiquement variables — sans que cela invalide leur structure minimale. L’*axiome d’incomplétude systémique* (1.3.6), quant à lui, soutient qu’aucune régulation n’est jamais entièrement close sur elle-même : il y a toujours un reste, un vide, un point de fuite, une indétermination — condition de la critique comme de l’invention.
@@ -642,7 +650,7 @@ Pour comprendre cette architecture mouvante, nous devons prendre appui sur trois
À partir de cette triple assise théorique, nous pouvons affirmer que la grammaire topologique constitue le pivot méthodologique du paradigme archicratique. Elle permet d’analyser chaque pôle de la régulation — *arcalité, cratialité, archicration* — comme une structure positionnelle, où l’interne et l’externe ne sont pas des essences, mais des effets de configuration, des prises, des agencements différenciés. Une *arcalité* peut être interne lorsqu’elle émane du récit propre d’une communauté politique, mais elle peut devenir externe si elle est dictée par des normes globales imposées. Une *cratialité* peut être locale et maîtrisée, mais aussi captée par des infrastructures techniques venues d’ailleurs. Une *archicration* peut se déployer dans une scène instituée par les acteurs concernés, ou leur être imposée depuis une juridiction externe, un tribunal arbitral, une plateforme d’arbitrage privée.
C’est cette plasticité topologique — cette capacité à circuler, à migrer, à se dissimuler — qui fait à la fois la richesse et la dangerosité des régimes archicratiques. Car une régulation peut fonctionner en mimant l’interne tout en étant totalement dépendante d’un externe invisible. Elle peut se présenter comme autonome, participative, située, alors qu’elle est en réalité structurée par des standards qui échappent à toute contestation locale. Le risque est alors celui de la dépolitisation radicale, d’une perte de repères critiques, d’une désorientation normative. Ce que la grammaire topologique rend possible, c’est précisément de restaurer cette lisibilité perdue : de retrouver les fils, de remonter les chaînes, de cartographier les prises.
C’est cette plasticité topologique — cette capacité à circuler, à migrer, à se dissimuler — qui fait à la fois la richesse analytique du paradigme et la dangerosité de certaines configurations désarchicratiques ou autarchicratiques. Car une régulation peut fonctionner en mimant l’interne tout en étant totalement dépendante d’un externe invisible. Elle peut se présenter comme autonome, participative, située, alors qu’elle est en réalité structurée par des standards qui échappent à toute contestation locale. Le risque est alors celui de la dépolitisation radicale, d’une perte de repères critiques, d’une désorientation normative. Ce que la grammaire topologique rend possible, c’est précisément de restaurer cette lisibilité perdue : retrouver les fils, remonter les chaînes, cartographier les prises.
C’est pourquoi, dans les sections qui suivent, nous déploierons une analyse détaillée de chacun des trois pôles — *arcalité, cratialité, archicration* — en les examinant à travers leurs configurations internes et externes. Nous montrerons comment ces modalités se combinent, se transforment, se superposent. Nous analyserons les effets de ces agencements sur la viabilité, la lisibilité et l’opposabilité des dispositifs. Et nous préparerons ainsi le terrain pour une typologie opératoire des régimes de régulation — une typologie qui ne se contente pas de décrire, mais qui permet de diagnostiquer, de contester, de transformer.
@@ -716,15 +724,15 @@ En ce sens, l’analyse archicratique exige une vigilance accrue sur les conditi
Ainsi, l’inertie d’une *arcalité externe* n’est pas seulement un échec technique ou politique : c’est le symptôme d’une absence de portage humain effectif. À l’inverse, sa puissance régulatrice dépend de sa capacité à être habitée, interprétée, mobilisée, par des professionnels, des agents publics, des collectifs intermédiaires ou des figures institutionnelles. Il y a une condition anthropologique de l’*arcalité* : sans corps vivant, pas de fondement durable.
Lorsque ces fondements exogènes sont naturalisés, dépolitisés, non contestables, ils deviennent des vecteurs privilégiés du régime archicratique : leur autorité prétendue neutre masque l’impossibilité de les discuter, de les traduire, de les opposer. Ce n’est pas leur contenu qui pose problème, mais leur mode d’énonciation : sans scène de justification, sans délai, sans dispositif de médiation, ces *arcalités* deviennent des injonctions performatives opaques. Elles gouvernent sans débat, fondent sans s’exposer, et neutralisent la discorde.
Lorsque ces fondements exogènes sont naturalisés, dépolitisés ou rendus difficilement contestables, ils deviennent des vecteurs privilégiés de dérives autarchicratiques ou archicratistiques : leur autorité prétendue neutre masque l’impossibilité pratique de les discuter, de les traduire ou de les opposer. Ce n’est pas leur contenu qui pose problème, mais leur mode d’énonciation : lorsque la scène de justification est neutralisée, lorsque les délais de médiation sont comprimés ou rendus fictifs, lorsque les dispositifs de traduction deviennent inopérants, ces arcalités tendent à devenir des injonctions performatives opaques.
C’est pourquoi le paradigme archicratique impose une exigence méthodologique claire : documenter le statut, le portage, l’activation et la contestation des *arcalités externes*. *Qui les invoque ? Qui les traduit ? Qui les fait vivre ? Sont-elles révisables ? Opposables ? Intégrées dans une scène ou dissoutes dans le dogme ?* La critique archicratique ne vise pas à disqualifier l’externe, mais à rendre visibles ses conditions d’incarnation.
Une *arcalité externe* bien internalisée, portée par une communauté compétente, disputée dans ses effets, rendue visible dans ses limites, peut renforcer un ordre démocratique. Mais une *arcalité* exogène désincarnée, mimée, figée, peut devenir le vecteur sourd d’un pouvoir sans scène. Le facteur humain, ici encore, est la clef de l’*arcalité vivante*.
Une *arcalité externe* bien internalisée, portée par une communauté compétente, disputée dans ses effets, rendue visible dans ses limites, peut renforcer un ordre démocratique. Mais une *arcalité* exogène désincarnée, mimée, figée, peut devenir le vecteur sourd d’un pouvoir à scène neutralisée, relocalisée hors d’atteinte ou rendue pratiquement inopérante. Le facteur humain, ici encore, est la clef de l’*arcalité vivante*.
### 1.4.3 — *Cratialités internes* : opérativité endogène, chaînes d’exécution incarnées et pouvoir discret sans extériorité
La *cratialité interne* constitue l’un des points névralgiques du paradigme archicratique. Elle ne se résume ni à l’action technique, ni à la simple application de normes venues d’ailleurs. Elle désigne la capacité d’un dispositif régulateur à produire lui-même, depuis l’intérieur, sa propre puissance opératoire, sans recourir explicitement à une légitimation transcendante (*arcalité*) ni à une scène d’épreuve (*archicration*). En ce sens, l’interne n’est pas un dedans organique, mais une position topologique de clôture opératoire : là où la régulation se suffit à elle-même, là où l’ordre d’exécution s’impose par la seule inertie du dispositif, là où le pouvoir se fait sans être dit.
La *cratialité interne* constitue l’un des points névralgiques du paradigme archicratique. Elle ne se résume ni à l’action technique, ni à la simple application de normes venues d’ailleurs. Elle désigne la capacité d’un dispositif régulateur à produire lui-même, depuis l’intérieur, sa propre puissance opératoire, sans mobiliser explicitement une légitimation transcendante, et avec une scène d’épreuve devenue marginale, reléguée à l’arrière-plan ou pratiquement inopérante dans le cours ordinaire de l’exécution. En ce sens, l’interne n’est pas un dedans organique, mais une position topologique de clôture opératoire : là où la régulation tend à se suffire à elle-même, là où l’ordre d’exécution s’impose par l’inertie du dispositif, et là où le pouvoir agit comme s’il n’avait plus à se dire, à se justifier ni à se laisser reprendre.
Ce qui distingue fondamentalement la *cratialité interne*, c’est son auto-suffisance apparente. Elle repose sur la densité cumulative des routines, des instruments, des protocoles, des langages, des procédures et — ce qui est décisif — des humains qui les activent, les maintiennent, les modulent. Car la *cratialité interne* n’est jamais une abstraction systémique désincarnée. Elle est portée par des agents, des métiers, des savoir-faire, qui donnent forme, corps et continuité à l’action régulatrice. Elle est faite de mains, de gestes, de réflexes, de scripts mentaux, de hiérarchies tacites, de cultures professionnelles. Elle n’est pas simplement un logiciel ou un organigramme : elle est le produit vivant de celles et ceux qui opèrent.
@@ -744,7 +752,7 @@ Il convient alors de distinguer plusieurs formes typologiques de *cratialité in
- *Langagières et discursives* : ici, le pouvoir tient au langage : aux jargons techniques, aux syntaxes normalisées, aux grilles d’expression. Il est détenu par ceux qui maîtrisent le langage de l’institution — rédacteurs administratifs, formateurs, communicants, traducteurs techniques, qui donnent forme intelligible à la régulation.
Ce qui fait la puissance silencieuse de cette *cratialité interne*, c’est qu’elle ne demande pas à être légitimée. Elle ne requiert ni fondement transcendant (*arcalité*), ni mise en épreuve contradictoire (*archicration*). Elle opère par inertie, par habitude, par flux. Tout y est déjà cadré, validé, formaté. La régulation se fait d’elle-même — non parce qu’elle est juste, mais parce qu’elle est possible, rapide, fluide.
Ce qui fait la puissance silencieuse de cette cratialité interne, c’est qu’elle peut fonctionner comme si elle n’avait plus à répondre explicitement de ses justifications, et comme si la scène d’épreuve n’avait plus de prise effective sur son cours ordinaire ; c’est le cas des algorithmes non documentés et des procédures automatiques rendues pratiquement inopposables. Tout y est déjà cadré, validé, formaté. La régulation se fait d’elle-même — non parce qu’elle serait absolument sans arcalité ni sans archicration, mais parce que celles-ci deviennent peu exposables, reléguées à l’arrière-plan, ou pratiquement inopérantes dans le cours ordinaire de l’exécution.
Mais c’est précisément cette fluidité sans extériorité qui peut poser problème. Car elle rend toute remise en cause difficile. Il n’y a pas de scène où s’adresser, pas de personne clairement responsable, pas de temporalité pour l’amendement. Ce n’est pas l’illégalité qui menace ici, mais la fermeture autoréférentielle : le dispositif fonctionne, donc il est. Le pouvoir s’exécute, donc il n’a pas besoin de s’expliquer.
@@ -836,7 +844,7 @@ C’est ici que le paradigme archicratique prend toute sa valeur : il rend lisib
Autrement dit, l’*archicration interne* est ce qui maintient le pouvoir habitable, parce qu’elle le rend réversible, explicable, explicite et amendable. Elle ne garantit pas la vérité, ni la justice — elle garantit la possibilité d’un autrement.
Mais lorsqu’elle se dissout dans l’inaction, lorsqu’elle est mise en scène sans effets, ou déportée vers des figures incapables d’assumer l’épreuve de la contestation, elle devient un artefact démocratique, un simulacre de dispute, une mise en forme de l’illusion délibérative. C’est là que l’*archicratie* s’installe : non comme domination explicite, mais comme court-circuit silencieux de la possibilité de reprise.
Mais lorsqu’elle se dissout dans l’inaction, lorsqu’elle est mise en scène sans effets, ou déportée vers des figures incapables d’assumer l’épreuve de la contestation, elle devient un artefact démocratique, un simulacre de dispute, une mise en forme de l’illusion délibérative. C’est là que s’installe non l’archicratie, mais son envers : une désarchicration susceptible de dériver vers l’autarchicratie, c’est-à-dire un court-circuit silencieux de la possibilité de reprise.
### 1.4.6 — *Archicrations externes* : scènes surplombantes, interpellations dissidentes et figures de contre-institution
@@ -898,7 +906,7 @@ C’est aussi, plus tragiquement, le lieu où se manifeste l’effondrement des
Si l’on a distingué, jusqu’ici, les prises internes et externes pour chacun des trois pôles du paradigme archicratique — *arcalité, cratialité, archicration* —, cette cartographie ne saurait être figée. Car les dispositifs régulateurs réels ne sont pas des blocs isolés ; ce sont des ensembles dynamiques, traversés par des circulations, des transferts, des reconfigurations. Autrement dit, l’interne et l’externe sont des positions politiques et stratégiques, dont les objets, les fonctions, les signes et les effets peuvent migrer, se dissimuler ou se renverser.
C’est précisément dans cette dynamique migratoire que se déploie toute la plasticité — mais aussi toute l’ambiguïté — des régulations contemporaines. L’*archicratie* ne se limite pas à une absence de scène ou à une saturation cratiale ; elle procède souvent par reconfiguration des prises : ce qui était externe devient interne (capture), ce qui était interne devient externe (délestage), et ce qui devrait être visible est rendu opaque par changement de topologie. Ainsi, la logique archicratique se manifeste autant par ce qui est dit que par l’endroit d’où cela est dit, tout autant par ce qui est fait que par l’endroit d’où cela est imposé.
C’est précisément dans cette dynamique migratoire que se déploie toute la plasticité — mais aussi toute l’ambiguïté — des régulations contemporaines. Les dérives autarchicratiques ne se réduisent pas à une simple absence de scène ou à une saturation cratiale ; elles procèdent souvent par reconfiguration des prises : ce qui était externe devient interne (capture), ce qui était interne devient externe (délestage), et ce qui devrait être visible est rendu opaque par changement de topologie. Ainsi, la dérive autarchicratique se manifeste autant par ce qui est dit que par l’endroit d’où cela est dit, tout autant par ce qui est fait que par l’endroit d’où cela est imposé.
#### ***Migrations arcales* : du mythe incorporé au fondement importé**
@@ -954,7 +962,7 @@ L’excès d’internalité produit des régulations opaques, fermées, surcodé
Mais c’est précisément entre ces deux pôles extrêmes que se noue la possibilité d’un dispositif politiquement viable : c’est-à-dire ni totalement clos, ni intégralement hétéronome, mais articulé dans une dialectique régulée entre interne et externe. Cette *co-viabilité* repose sur la capacité du dispositif à *maintenir une tension active entre ses composantes*, à autoriser la critique venue de l’extérieur sans se dissoudre dans une dépendance pure, et à mobiliser des justifications internes sans basculer dans l’auto-légitimation close.
Cette tension est particulièrement cruciale dans les formes de régulation dites « silencieuses » — c’est-à-dire sans théâtre, sans acteurs identifiables, sans justification publique explicite. Là où l’interne l’emporte, le risque est celui d’une saturation normative : plus rien ne peut venir troubler le cycle opératoire du dispositif. Là où l’externe l’emporte, c’est l’évaporation du sens qui guette : plus rien ne fonde localement la régulation, plus rien ne la rend habitable.
Cette tension est particulièrement cruciale dans les formes de régulation dites « silencieuses » — c’est-à-dire à théâtre affaibli ou neutralisé, à acteurs difficilement identifiables, et à justification publique faiblement exposable. Là où l’interne l’emporte, le risque est celui d’une saturation normative : plus rien ne peut venir troubler le cycle opératoire du dispositif. Là où l’externe l’emporte, c’est l’évaporation du sens qui guette : plus rien ne fonde localement la régulation, plus rien ne la rend habitable.
Le paradigme archicratique n’a pas pour vocation de désigner un juste milieu abstrait entre ces deux extrêmes, mais de fournir les instruments critiques pour repérer les points de bascule, les seuils de rupture, les zones où la régulation cesse d’être lieu de confrontation parce que ses prises deviennent illisibles. Ces seuils de *co-viabilité* doivent être pensés à partir de critères politiques précis : *existe-t-il un différé ? Un recours ? Une scène ? Une instance d’interpellation ? Une capacité d’amendement ?* Ces questions ne sont pas secondaires : elles sont les conditions minimales de soutenabilité d’un ordre régulateur dans une société qui se dit démocratique.
@@ -1084,15 +1092,15 @@ En cela, les *formes hypertopiques* nous enseignent ce que devient une régulati
Il est des régulations dont la fragilité réside dans le défaut même d’articulation, dans la carence silencieuse d’un ou plusieurs pôles, non pas dominance mais par manquements, effacements, indéterminations. Ces régimes ne sont pas saturés — ils sont désarrimés, désaffiliés, incomplètement ancrés. Ce sont les *formes hypotopiques* : configurations où la triade archicratique ne parvient pas à s’incarner pleinement, soit par inachèvement historique, soit par délitement structurel, soit par marginalisation sociale. Non point des régimes de déséquilibre manifeste — mais de décrochements latents.
Dans ces situations, ce n’est pas la force excessive d’un pôle qui domine, mais l’effacement progressif ou brutal de l’un de ses vecteurs constitutifs. Cela peut être l’arcalité qui fait défaut, lorsqu’une régulation se poursuit sans fondement explicite, sans justification reconnue, sans principe commun. Cela peut être la *cratialité* qui s’effondre, lorsque les moyens d’opération sont absents, disjoints ou dysfonctionnels. Cela peut être l’*archicration* qui se dissout, lorsque plus aucune scène ne permet l’expression du différend, la mise en tension, la confrontation réglée.
Dans ces situations, ce n’est pas la force excessive d’un pôle qui domine, mais l’effacement progressif ou brutal de l’un de ses vecteurs constitutifs. Cela peut être l’arcalité qui fait défaut, lorsqu’une régulation se poursuit sous des fondements implicites, peu exposables ou dépourvus de justification reconnue. Cela peut être la cratialité qui s’effondre, lorsque les moyens d’opération sont absents, disjoints ou dysfonctionnels. Cela peut être l’archicration qui se dissout, lorsqu’aucune scène effective ne permet plus l’expression du différend, la mise en tension ou la confrontation réglée.
Un exemple manifeste d’*hypoarcalité* se retrouve dans les dispositifs provisoires ou de crise, où l’action publique se déploie sans fondement explicite, dans une zone grise entre droit et exception. L’état d’urgence sanitaire prolongé, la gestion dérogatoire des flux migratoires ou encore les expérimentations territoriales non encadrées sont autant de cas où la *cratialité* et parfois l’*archicration* existent, mais où l’*arcalité* est suspendu, implicite ou réduit à un vague impératif de « nécessité ». Dans ces cas, la légitimité régulatrice s’efface, non pas par volonté autoritaire, mais par inachèvement ou déni de sa propre condition normative. Le pouvoir continue d’agir, mais sans adossement symbolique, éthique ou juridique pleinement assumé.
Un exemple manifeste d’*hypoarcalité* se retrouve dans les dispositifs provisoires ou de crise, où l’action publique se déploie sur fondement implicite, suspendu ou faiblement exposable, dans une zone grise entre droit et exception. L’état d’urgence sanitaire prolongé, la gestion dérogatoire des flux migratoires ou encore les expérimentations territoriales non encadrées sont autant de cas où la *cratialité* et parfois l’*archicration* existent, mais où l’*arcalité* est suspendu, implicite ou réduit à un vague impératif de « nécessité ». Dans ces cas, la légitimité régulatrice s’efface, non pas par volonté autoritaire, mais par inachèvement ou déni de sa propre condition normative. Le pouvoir continue d’agir, mais sur un adossement symbolique, éthique ou juridique faiblement assumé, peu explicité ou difficilement opposable.
À l’inverse, certaines configurations présentent une *hypocratialité* : les intentions sont fortes, les textes abondants, les dispositifs délibératifs multiples — mais l’opérativité est absente ou déficiente. Ce sont les dispositifs de « papier », sur-institutionnalisés et sous-dotés, où les *arcalités* sont proclamées, les *archicrations* ouvertes, mais rien ne se passe. Les plans de transition écologique sans moyens, les comités citoyens sans budget, les programmes d’inclusion numérique sans matériel sont des formes classiques de cette dérive. Le geste politique est performé, mais peu outillé. L’action régulatrice meurt d’épuisement, faute d’infrastructure ou de continuité d’exécution. Le pôle cratial se délite, et avec lui, la consistance de tout l’édifice.
Il existe enfin des *régimes hypoarchicratifs*, dans lesquels les tensions sont étouffées, non par répression ou simulation, mais par désertion. Les scènes de dispute s’éteignent non faute d’autorisation, mais faute de participants, faute de prise, faute d’appel possible. La démocratie locale peut en être le symptôme : dans certains conseils municipaux ruraux, toutes les délibérations sont publiques, les procès-verbaux accessibles, les voies de recours en théorie ouvertes — mais personne n’y vient, personne ne les utilise. L’espace de la critique devient désert, non pas parce qu’on l’empêche, mais parce qu’il n’a plus d’effet. On ne conteste plus ce que l’on ne croit plus modifiable. L’usure de la dispute devient une forme d’abstention persistante.
Ces *régulations hypotopiques* sont redoutables précisément parce qu’elles ne suscitent pas d’alarme immédiate. Elles n’ont ni l’éclat tyrannique des régimes autoritaires, ni le chaos visible des formes délibératives défaillantes. Elles perdurent, parfois longtemps, dans une grande stabilité apparente, faute de tension, de friction, de dispute. Mais c’est une stabilité vide, une paix sans enjeu, un ordre sans scène.
Ces *régulations hypotopiques* sont redoutables précisément parce qu’elles ne suscitent pas d’alarme immédiate. Elles n’ont ni l’éclat tyrannique des régimes autoritaires, ni le chaos visible des formes délibératives défaillantes. Elles perdurent, parfois longtemps, dans une grande stabilité apparente, faute de tension, de friction, de dispute. Mais c’est une stabilité vide, une paix sans enjeu, un ordre à scène désertée, neutralisée ou pratiquement inopérante.
Le paradigme archicratique nous invite ici à une vigilance particulière : il ne suffit pas de vérifier la présence formelle des trois pôles. Il faut en interroger l’effectivité, la consistance, la vitalité, dans la durée et dans la conflictualité. Une *arcalité* proclamée mais jamais invoquée est un décor. Une *cratialité* active mais sans prise humaine est un automatisme. Une *archicration* ouverte mais inopérante est une illusion procédurale. Ce qui fait régulation n’est pas l’énumération des fonctions, mais leur co-présence dynamique, différenciée, habitée.
@@ -1100,9 +1108,11 @@ Les *formes hypotopiques* sont les signes d’une dérégulation par effacement,
### 1.5.4 — *Formes atopiques* : déréalisations, vacuités et simulacres
Il est des dispositifs de régulation où les formes sont présentes, les fonctions identifiables, les terminologies stabilisées — mais où la consistance topologique fait défaut. Ni déséquilibre (hypertopie), ni carence (hypotopie), ni tension co-viable (synchrotopie), ces configurations appartiennent à un tout autre régime : celui de l’*irréalité régulatrice*. La structure semble intacte, parfois même sophistiquée, mais aucun des pôles archicratiques n’est substantiellement ancré, ni dans la scène, ni dans les corps, ni dans les pratiques. Ce sont des *formes atopiques* — c’est-à-dire des régulations sans lieu, sans effectivité, sans consistance — qui simulent l’existence d’un ordre tout en ne le rendant ni habitable, ni contestable, ni opératoire.
Il est des dispositifs de régulation où les formes sont présentes, les fonctions identifiables, les terminologies stabilisées — mais où la consistance topologique fait défaut. Ni déséquilibre (hypertopie), ni carence (hypotopie), ni tension co-viable (synchrotopie), ces configurations appartiennent à un autre régime : celui de l’irréalité régulatrice. La structure semble intacte, parfois même sophistiquée, mais les pôles archicratiques n’y trouvent plus qu’un ancrage vidé, mimé, stylisé ou rendu artefact, sans consistance vivante ni effectivité praticable. L’arcalité y est mimée plutôt qu’exposée, la cratialité y est stylisée plutôt que véritablement rendue opérante, l’archicration y est figurée plutôt qu’instituée comme scène de reprise. Nous n’avons plus affaire à une régulation tenue, mais à une scénographie vide de la régulation.
L’*atopie* n’est pas l’absence pure et simple. Elle est une présence vide, une figuration institutionnelle déconnectée de tout processus régulateur vivant. Elle repose souvent sur des artefacts de légitimation, des outils de pilotage automatisés, des consultations protocolaires — mais dont l’impact sur le réel est nul ou illisible. Tout y est là, en apparence : des fondements (arcalité), des instruments (cratialité), des scènes de discussion (archicration) — mais sans prise, sans contrepartie, sans transformation. L’infrastructure normative est creuse, le pilotage est aveugle, la dispute est factice. On y maintient les gestes, les rites, les discours — mais sans monde.
L’*atopie* n’est pas l’absence pure et simple. Elle est une présence vide, une figuration institutionnelle déconnectée de tout processus régulateur vivant. Elle repose souvent sur des artefacts de légitimation, des outils de pilotage automatisés, des consultations protocolaires — mais dont l’impact sur le réel est nul ou illisible. Tout y est — en apparence : des fondements, des instruments, des scènes de discussion — mais sous des formes vidées de prise, privées de contrepartie effective et incapables de transformation réelle. L’infrastructure normative est creuse, le pilotage est aveugle, la dispute est factice. On y maintient les gestes, les rites, les discours — mais sans prise vivante sur le monde qu’ils prétendent réguler.
L’atopie ne décrit donc pas une régulation effective amputée de ses pôles, mais un simulacre régulatif dans lequel les prises ne subsistent plus qu’à l’état mimé, vidé ou rendu artefact.
Cette atopie peut se manifester selon plusieurs figures typiques.
@@ -1124,7 +1134,7 @@ Enfin, l’atopie peut prendre la forme d’un usage symbolique, rhétorique ou
Ce que révèle l’analyse archicratique, c’est que la vacuité ne s’oppose pas frontalement au pouvoir : elle peut en être la forme la plus stable. Le simulacre est parfois plus durable que l’autoritarisme, car moins repérable, moins conflictuel, plus fluide. Une démocratie peut périr dans l’atopie sans jamais suspendre le droit de vote. Une administration peut devenir autistique sans jamais violer la procédure. Un dispositif de participation peut être déserté par saturation symbolique, non par coercition.
L’*atopie* est donc le point aveugle de la critique classique. Elle ne se manifeste pas par l’excès de pouvoir, ni par son absence, mais par sa *simulation creuse*. Elle est ce moment où les fonctions sont remplies, mais où aucune scène n’est réellement investie, aucun lien n’est réellement actif, aucun différé n’est institué.
L’*atopie* est donc le point aveugle de la critique classique. Elle ne se manifeste pas par l’excès de pouvoir, ni par son absence, mais par sa *simulation creuse*. Elle est ce moment où les fonctions sont remplies, mais où les scènes ne sont plus réellement investies, où les liens deviennent inactifs ou purement formels, et où le différé n’est plus institué comme prise effective de reprise, mais seulement figuré ou neutralisé.
### Cartographier la tenue régulatrice : vers une pragmatique des formes archicratiques
@@ -1134,9 +1144,9 @@ Nous avons désigné par *forme synchrotopique* la configuration rare bien que d
Les *formes hypertopiques*, quant à elles, rendent visible la plasticité parfois toxique du paradigme : lorsque l’un ou deux des pôles submergent les autres, soit par hégémonie fondatrice (*hyperarcalité*), soit par captation instrumentale (*hypercratialité*), soit par saturation participative (archicration sans opérativité), la régulation se bloque, s’étiole ou se pervertit. Les dérives y sont patentes, mais leur diagnostic exige plus que des jugements de valeur : il réclame une lecture structurelle, patiente, stratifiée.
Avec les *formes hypotopiques*, le paradigme archicratique affronte une tout autre pathologie : celle de l’effacement, du désarrimage, de la déprise. Il ne s’agit plus ici d’excès, mais de manque : manque de justification, d’effectuation, de dispute. Ce n’est plus la sur-présence d’un pôle qui menace, mais l’absence réelle ou feinte de tout principe actif de régulation. Ce sont des régulations en dormance, en rétraction, ou en dissociation.
Avec les *formes hypotopiques*, le paradigme archicratique affronte une tout autre pathologie : celle de l’effacement, du désarrimage, de la déprise. Il ne s’agit plus ici d’excès, mais de manque : manque de justification, d’effectuation, de dispute. Ce n’est plus la sur-présence d’un pôle qui menace, mais l’effacement, la mise en dormance ou la désactivation apparente des principes actifs de régulation.
Enfin, les *formes atopiques* révèlent la possibilité la plus troublante, et sans doute la plus contemporaine : celle de régulations sans réalité, où les pôles sont mimés, stylisés, formalisés — mais sans consistance, sans scène, sans prise. Ce ne sont plus des déséquilibres, mais des simulacres. Non plus des régimes problématiques, mais des dispositifs qui tiennent sans fonder, sans opérer, sans disputer. Ce sont les formes vides du pouvoir sans régulation véritable — celles qui appellent une reconstitution critique urgente du politique.
Enfin, les formes atopiques révèlent la possibilité la plus troublante, et sans doute la plus contemporaine : celle de configurations où les pôles sont stylisés et formalisés, mais avec des prises mimées, vidées, à ancrage vivant introuvable, à scène devenue impraticable, et à effectivité régulatrice non soutenable. Ils ne “tiennent” pas au sens fort d’une régulation viable ; ils se maintiennent comme artefacts, comme figures creuses, comme apparences d’ordre. Ce sont les formes vides du pouvoir sans régulation habitable — celles qui appellent une reconstitution critique urgente du politique.
Ce qui se donne à voir, à travers cette cartographie dynamique des tenues régulatrices, c’est donc une topologie active du paradigme archicratique : un modèle théorique qui permet de penser non seulement les composants, mais leur manière d’apparaître, de s’ordonner, de se désordonner, de se figer, de se dissoudre. Il s’agit d’un dispositif critique, qui articule diagnostic, description et problématisation, en donnant au lecteur — et plus encore, au chercheur, au praticien, au citoyen — des outils de repérage, de discernement, d’action.
@@ -1148,8 +1158,8 @@ Cette section 1.5 constitue ainsi un tournant dans notre essai-thèse. Car elle
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| **Synchrotopique** | Trois pôles présents, différenciés, articulés | Équilibre différé et co-tendu | Régulation habitée, amendable, soutenable | — (forme idéale) | Maximal : habitable, critique, stable | Capacité de ralentir, d’ajuster, de contester |
| **Hypertopique** | Un ou deux pôles hégémoniques (ex. arcalité absolue ou cratialité saturante) | Tension écrasée, figée ou court-circuitée | Régulation asymétrique, autoréférente ou verrouillée | Sur-arcalisation, technogestion, théâtralisation participative | Moyen à faible, selon la plasticité restante | Blocage d’un pôle, exclusion d’un autre, saturation opératoire |
| **Hypotopique** | Un ou plusieurs pôles effacés, désaffiliés ou en veille | Tension suspendue, inopérante | Régulation dévitalisée, bancale ou inerte | Disparition des fondements, perte d’efficacité, vide contestataire | Faible, sauf réactivation coordonnée | Absence de scènes, silence des agents, désancrage narratif |
| **Atopique** | Trois pôles présents mais sans ancrage réel | Tension fantomatique ou mimée | Régulation spectrale, simulée ou décorative | Déréalisation, fétichisation, vacuité procédurale | Nul à toxique : simulacre paralysant | Aucun effet de scène, aucune reprise, vacuité masquée par la forme |
| **Hypotopique** | Un ou plusieurs pôles effacés, désaffiliés ou en veille | Tension suspendue, inopérante | Régulation dévitalisée, bancale ou inerte | Effacement, mise en latence ou faible exposition des fondements ; perte d’efficacité ; dévitalisation contestataire | Faible, sauf réactivation coordonnée | Scènes désertées, neutralisées ou pratiquement inopérantes ; silence des agents ; désancrage narratif |
| **Atopique** | Trois pôles mimés, stylisés ou artefactuels, sans ancrage vivant soutenable | Tension fantomatique ou mimée | Régulation spectrale, simulée ou décorative | Déréalisation, fétichisation, vacuité procédurale | Nul à toxique : simulacre paralysant | Scène figurée sans prise, reprise neutralisée, vacuité masquée par la forme |
Chaque ligne de ce tableau constitue un cadre analytique complet : il peut être utilisé comme outil de diagnostic dans les chapitres suivants, pour évaluer des dispositifs empiriques sectoriels, repérer des évolutions topologiques, ou cartographier des formes de capture et de libération régulatrice.
@@ -1201,7 +1211,7 @@ En somme, la détectabilité est ici un geste critique et herméneutique : recon
L’*archicratie*, destinée à saisir autrement les modalités différenciées, évolutives et souvent opaques de la régulation dans les sociétés humaines, n’entend pas livrer ici un instrument de mesure, mais proposer une grammaire de discernement — ancrée dans la triade *arcalité–cratialité–archicration* — pour éclairer de réelles situations situées. L’ambition est heuristique : offrir des points d’ancrage et de repérage capables de décrire, de discriminer et de problématiser, sans sacrifier la pluralité des expériences ni la conflictualité inhérente à toute *co-viabilité*.
Nous vivons un moment où l’« opérativité » est souvent dévoyée par la prolifération de mesures pseudo-scientifiques érigées en dispositifs de vérité sans scène : tableaux de bord, indicateurs de performance, grilles d’évaluation, algorithmes prédictifs et plateformes de pilotage normant l’action en amont et rétrécissant les marges du possible en aval. Sous prétexte de rationalisation, s’étend une régulation sans scène : sans explicitation des sources, sans exposition des mécanismes, sans différé d’interprétation. La mesure devient sa propre justification, neutralisant le conflit constitutif du social et sapant les conditions d’une *archicration* effective.
Nous vivons un moment où l’opérativité est fréquemment dévoyée par la prolifération de mesures pseudo-scientifiques élevées au rang de dispositifs de vérité à scène neutralisée, comprimée ou relocalisée hors d’atteinte. Tableaux de bord, indicateurs de performance, grilles d’évaluation, algorithmes prédictifs et plateformes de pilotage étendent ainsi une régulation dont la scène d’épreuve se trouve neutralisée : les sources demeurent insuffisamment explicitées, les mécanismes ne sont pas pratiquement exposables, et le différé interprétatif devient fictif ou inopérant. Sous prétexte de rationalisation, ces dispositifs normaient l’action en amont et resserrent en aval les marges du possible. La mesure tend alors à se faire sa propre justification, neutralisant le conflit constitutif du social et minant les conditions mêmes d’une archicration effective.
Face à cette saturation instrumentale, la tâche du paradigme archicratique est plus modeste et plus exigeante : non pas proposer un outil de pilotage alternatif, ni concurrencer les instruments existants sur leur terrain, mais offrir des repères heuristiques pour identifier des formes différenciées de légitimation, d’opération et de contestation dans des contextes socio-politiques, historiques et institutionnels situés. Ces repères ne visent ni universalité formelle ni exclusivité, mais pertinence contextualisée. Ils se proposent comme scène d’interprétation ouverte, susceptible d’être discutée et ajustée par les acteurs. Ils forment une matrice de discernement, non un schéma d’application.
Nota bene : la conversion de ces repères en protocoles d’évaluation relève d’un programme ultérieur, non traité dans l’ouvrage.
@@ -1220,7 +1230,7 @@ Prenons un service public numérisé où les usagers passent par un portail en l
Enfin, l’*archicration* constitue le point névralgique de la lecture archicratique. Elle ne se réduit ni au droit au recours formel ni à une contradiction abstraite : elle désigne la scène instituée, différée, située, où le pouvoir se laisse interroger, où la décision devient amendable, où le fondement redevient discours. Instance juridictionnelle, médiation, espace de débat, conseil pluraliste, rituel, forum numérique, procédure contradictoire, délai suspensif : la forme importe moins que la possibilité effective pour les agents affectés de faire entendre leur voix, demander des comptes, suspendre, raconter une autre version, transformer le dispositif en scène contradictoire.
Une régulation sans *archicration* est une régulation sans visage. Une décision sans différé est une décision sans interlocuteur. Là où le contradictoire disparaît, vacille non seulement la démocratie, mais la capacité d’une société à se représenter comme traversée de conflits et de désaccords potentiellement féconds. La lecture archicratique consiste à repérer, dans chaque dispositif, les conditions minimales d’une telle scène, à diagnostiquer les formes de son effacement et à préfigurer les moyens de sa réapparition. L’*archicration* n’est pas un luxe : c’est la condition de possibilité de toute régulation viable.
Une régulation privée d’archicration effective est une régulation sans visage. Une décision sans différé praticable est une décision sans interlocuteur. Là où le contradictoire disparaît ou devient inopérant, vacille non seulement la démocratie, mais la capacité d’une société à se représenter comme traversée de conflits et de désaccords potentiellement féconds. La lecture archicratique consiste à repérer, dans chaque dispositif, les conditions minimales d’une telle scène, à diagnostiquer les formes de son effacement et à préfigurer les moyens de sa réapparition. L’archicration n’est pas un luxe : elle est la condition de possibilité de toute régulation viable.
Une telle lecture ne peut être univoque ni standardisée. Elle suppose une approche située, transversale, dialogique, mobilisant des savoirs institutionnels, juridiques, techniques, mais aussi ethnographiques, narratifs, subjectifs. Elle engage des compétences et lectures croisées, et exige que les sujets soient parties prenantes de l’interprétation. C’est dans les récits, résistances, bifurcations, plaintes et silences que la régulation — ou son absence — devient visible. C’est dans les marges, interstices et incidents que l’*archicratie* trouve sa matière vive.
@@ -1276,7 +1286,7 @@ Car ce qui fait valeur dans un paradigme critique, ce n’est pas sa seule élé
Ce travail de morphologie opérante repose sur trois exigences conjuguées. D’abord, une *exigence* *référentielle* : comprendre à partir de quels énoncés, objets, récits, modèles, figures ou principes un ordre prétend se légitimer. Ensuite, une *exigence opératoire* : analyser les mécanismes, supports, outils, interfaces, organigrammes, temporalités et frictions par lesquels une règle produit ses effets. Enfin, une *exigence expérientielle* : restituer la manière dont ces formes sont vécues, comprises, redoutées, contournées ou investies par celles et ceux qui y sont exposés. C’est dans cette articulation — du sémantique, du matériel et du sensible — que réside la puissance diagnostique du paradigme.
Mais il nous faut aller plus loin encore, car cette typologie stratifiée ne peut se limiter à un inventaire. Elle constitue davantage le socle critique de l’épreuve archicratique. En effet, une *arcalité* peut exister sans être compréhensible ni contestable ; une *cratialité* peut opérer sans être localisable ni amendable ; une *archicration* peut être instituée sans jamais devenir praticable. Le paradigme archicratique n’exige pas que ces prises soient parfaites — il exige qu’elles soient rendues lisibles dans leur état, qu’elles soient exposées dans leur morphologie effective, et surtout qu’elles soient analysées pour ce qu’elles permettent ou empêchent en termes d’adresse, de comparution et de remédiation.
Mais il nous faut aller plus loin encore, car cette typologie stratifiée ne peut se limiter à un inventaire. Elle constitue davantage le socle critique de l’épreuve archicratique. En effet, une arcalité peut subsister en demeurant difficilement compréhensible ou contestable ; une cratialité peut opérer tout en restant peu localisable ou faiblement amendable ; une archicration peut être instituée tout en devenant pratiquement impraticable. Le paradigme archicratique n’exige pas que ces prises soient parfaites — il exige qu’elles soient rendues lisibles dans leur état, qu’elles soient exposées dans leur morphologie effective, et surtout qu’elles soient analysées pour ce qu’elles permettent ou empêchent en termes d’adresse, de comparution et de remédiation.
Il s’agit ici d’un pas décisif vers l’observation située, vers la formalisation graduée des régimes, vers une grammaire analytique capable de faire apparaître les formes ténues ou massives, explicites ou latentes, ouvertes ou fermées, des prises archicratiques. C’est un appel à regarder autrement ce qui fait régulation. Et cet appel passe par la reconnaissance des formes — même mutilées, même silencieuses — que prennent nos fondements, nos opérations et nos scènes.
@@ -1342,9 +1352,9 @@ Elle peut se déployer dans des *régimes centralisés* — un portail numériqu
Il faut ici récuser toute vision gestionnaire ou techniciste. La *cratialité* ne se résume pas à un processus d’optimisation. Elle n’a pas pour finalité d’assurer l’efficacité d’un dispositif, mais de rendre intelligible les formes d’effectuation concrète d’un ordre. Une chaîne de traitement peut être rapide, fluide, bien documentée — et pourtant rigide, incompréhensible, impitoyable. À l’inverse, une *cratialité* peut être lente, manuelle, fragmentée — mais ménager des seuils d’ajustement, des lieux d’appel, des moments d’interruption qui la rendent habitable. Ce n’est pas le style de l’opération qui compte, mais sa capacité à articuler des effets dans une configuration régulatrice spécifique.
Cette articulation engage la matérialité des infrastructures comme la plasticité des rôles. Un *geste cratial* est conditionné par une forme, un outil, une procédure, un délai, un seuil, un langage. Lorsqu’un distributeur automatique refuse un retrait parce que le compte est bloqué, il ne fait qu’opérer un effet déjà inscrit ailleurs dans une chaîne bancaire : encodage d’un score de risque, clôture discrète d’un accès, préemption sur des soldes en litige. Le geste, ici, est purement machinique, mais sa *cratialité* est pleine : il transforme une trajectoire, conditionne une action, produit un effet de contrainte — sans justification visible et hors de toute scène explicite.
Cette articulation engage la matérialité des infrastructures comme la plasticité des rôles. Un *geste cratial* est conditionné par une forme, un outil, une procédure, un délai, un seuil, un langage. Lorsqu’un distributeur automatique refuse un retrait parce que le compte est bloqué, il ne fait qu’opérer un effet déjà inscrit ailleurs dans une chaîne bancaire : encodage d’un score de risque, clôture discrète d’un accès, préemption sur des soldes en litige. Le geste, ici, est purement machinique, mais sa *cratialité* est pleine : il transforme une trajectoire, conditionne une action, produit un effet de contrainte — sur une justification faiblement visible et dans une scène d’épreuve neutralisée, reléguée ou pratiquement hors de portée.
C’est précisément cette possibilité d’absence de scène qui rend la *cratialité* décisive dans notre paradigme. Car elle travaille en amont de toute possible contestation et en aval de tout fondement. Elle agit avant la réclamation et après le principe. Elle ne parle pas, mais elle configure les possibilités d’action : ce que l’on peut faire, ce que l’on doit fournir, où l’on peut passer, avec qui l’on peut parler, à quel moment une procédure ou une démarche est susceptible de relance ou s’avère réellement close. En cela, ce sont les chaînes opératoires — non pas les discours — qui rendent compte du régime véritable d’un ordre. En quelque sorte, elle en révèle *sa conduite* et *son conduit*.
C’est précisément cette possibilité de neutralisation, de relocalisation ou d’inopérance de la scène qui rend la cratialité décisive dans notre paradigme. Car elle travaille en amont de toute possible contestation et en aval de tout fondement. Elle agit avant la réclamation et après le principe. Elle ne parle pas, mais elle configure les possibilités d’action : ce que l’on peut faire, ce que l’on doit fournir, où l’on peut passer, avec qui l’on peut parler, à quel moment une procédure ou une démarche est susceptible de relance ou s’avère réellement close. En cela, ce sont les chaînes opératoires — non pas les discours — qui rendent compte du régime véritable d’un ordre. En quelque sorte, elle en révèle *sa conduite* et *son conduit*.
Dans les pratiques sociales ordinaires, la *cratialité* se manifeste sans protocole formel. Lorsqu’un étudiant s’adresse à un secrétariat, lorsqu’un réfugié tente d’accéder à un guichet saturé, lorsqu’un malade cherche un rendez-vous sur Doctolib, ce ne sont pas les normes écrites qui produisent la régulation, mais les séquences d’opérations réelles : les créneaux disponibles, la tolérance de l’agent, la posture du guichet, l’existence ou non d’un canal alternatif, la résilience du dispositif face à la surcharge. Une *cratialité* se reconnaît au fait que le sujet modifie son agir en fonction de la chaîne d’effectuation perçue ou anticipée — même s’il n’en connaît ni la structure ni les règles exactes.
@@ -1352,7 +1362,7 @@ On ne saurait confiner la *cratialité* à l’espace institutionnel. Dans les
Dans les logiques environnementales, la *cratialité* peut prendre la forme de barrières physiques (clôtures, canaux), d’interdictions de prélèvement, de modulations de quota, de déclencheurs de dispositifs d’alerte. De sorte qu’un niveau de pollution franchi peut suspendre le trafic, rediriger des flux, interdire l’usage d’une zone — par l’effet combiné d’instruments de mesure, de seuils préprogrammés et de dispositifs de verrouillage. Cette régulation ne suppose ni acteur visible ni débat public. Elle agit par chaîne. Et c’est cette chaîne — non sa justification — qui par exemple constitue le *régime cratial* de la gestion écologique — dans les faits.
On retrouve ce même mécanisme dans les mécanismes migratoires : un dispositif de visa, un fichier de police, une fermeture administrative, une alerte biométrique, un fichier d’exclusion partagé de ressortissants entre États. Chacun de ces gestes — souvent silencieux, parfois automatisé — transforme l’état d’un sujet sans jamais apparaître comme une décision. Il ne s’agit pas d’arbitraire, mais de *cratialité brute et opaque* : une régulation agissant par action différée, sans scène, sans fondement explicite.
On retrouve ce même mécanisme dans les mécanismes migratoires : un dispositif de visa, un fichier de police, une fermeture administrative, une alerte biométrique, un fichier d’exclusion partagé de ressortissants entre États. Chacun de ces gestes — souvent silencieux, parfois automatisé — transforme l’état d’un sujet sans jamais apparaître comme une décision. Il ne s’agit pas d’arbitraire, mais de *cratialité brute et opaque* : une régulation agissant par action différée, à scène neutralisée ou relocalisée, et à fondement implicite, euphémisé ou difficilement exposable.
Le problème fondamental n’est donc pas à nos yeux de savoir si une régulation est automatisée ou humaine, numérique ou analogique. Ce qui importe, c’est la morphologie de la chaîne d’effectuation : *peut-on en reconstituer les étapes ? Qui ou qu’est-ce qui a agi durant celles-ci ? Au nom de quoi ? Quelles ont été concrètement les actions conduites ? Quels en ont été leurs effets ? Au profit ou au détriment de qui ? Existe-t-il des seuils de suspension ? des marges de correction ? des points d’interruption ?* Voilà autant de questions qui constituent la tenue d’une enquête sur la *cratialité*. C’est ce à quoi le paradigme archicratique s’attèle, et ce qu’elle entend *in fine* saisir : une topologie factuelle des régimes opératoires prise dans des situations effectives.
@@ -1418,11 +1428,11 @@ Il y a ensuite les *scènes retournées*, plus pernicieuses encore. Celles-ci ne
Autre figure : la *scène saturée*. Ici, le dysfonctionnement ne vient pas de l’absence, mais de l’excès non médié. Trop d’accès, pas de filtrage, pas de traduction. Trop de voix, pas d’écoute structurée. Trop d’injonctions, pas de temporalité. La scène devient invivable : le volume d’interpellation y déborde toute capacité de traitement, la comparution s’y dilue dans le chaos, la réversibilité devient illisible. Ce sont les grandes plateformes numériques d’évaluation, les forums de “libre expression”, ou encore certaines réunions communautaires où tout le monde parle sans qu’aucune écoute mutuelle ne tienne. La scène donne lieu au vacarme. Le seuil d’entrée s’avère ouvert, mais l’agencement archicratique se voit détruit. La critique y meurt de sa propre profusion non canalisée.
Plus troublante encore est la *scène spectrale*. Là, il ne s’agit plus d’un simulacre d’écoute ni d’un excès d’expression mais d’un ajournement indéfini. Le sujet n’est pas exclu ; il demeure suspendu. On lui dit : “bientôt”, “encore un peu de patience”, “votre demande est en cours de traitement”. Mais celui-ci ne vient jamais. La scène est invoquée, toujours repoussée, jamais tenue. C’est le règne de la régulation par la latence, où le temps devient instrument d’usure et d’effacement sans brutalité. Dans les politiques d’asile, dans les systèmes de demande de logement, dans les chaînes d’approbation administratives, on trouve ce type de *scènes spectrales* : le dossier est bien présent, mais ne trouve jamais place dans la scène. Ce qui est produit alors est une suspension sans mémoire, une attente sans comparution, une adresse sans réponse. Le différé se mue dès lors en forme de pouvoir.
Plus troublante encore est la *scène spectrale*. Là, il ne s’agit plus d’un simulacre d’écoute ni d’un excès d’expression mais d’un ajournement indéfini. Le sujet n’est pas exclu ; il demeure suspendu. On lui dit : “bientôt”, “encore un peu de patience”, “votre demande est en cours de traitement”. Mais celui-ci ne vient jamais. La scène est invoquée, toujours repoussée, jamais tenue. C’est le règne de la régulation par la latence, où le temps devient instrument d’usure et d’effacement sans brutalité. Dans les politiques d’asile, dans les systèmes de demande de logement, dans les chaînes d’approbation administratives, on trouve ce type de *scènes spectrales* : le dossier est bien présent, mais ne trouve jamais place dans la scène. Ce qui est produit alors est une suspension sans mémoire, une attente à comparution différée jusqu’à l’inopérance, une adresse privée de réponse effectivement opposable. Le différé se mue dès lors en forme de pouvoir.
Face à cela, il existe des régulations qui s’ajustent sans jamais organiser de nouvelles scènes. Ce sont les *formes post-interpellatives*. L’ordre y intègre la mémoire de scènes passées — plaintes, mobilisations, controverses — pour reconfigurer silencieusement ses opérations. On peut compter dans cette catégorie : un algorithme modifié sans annonce, un seuil déplacé sans justification, une interface ajustée après une vague de critiques. Il n’y a pas de comparution, mais une mémoire agissante. Ce n’est pas une archicration au sens strict, mais une archicratialisation par effet de sédimentation critique. Ce régime peut être fécond, mais aussi inquiétant, car il soustrait à la vue des concernés les litiges et les dérives établis : *qui contrôle que l’ajustement a bien eu lieu ?, qui mémorise la scène absente ?, qui peut rejouer l’interpellation si elle reste sans forme ?* La critique y est intégrée, donne lieu à évolution d’arcalités et de cratialités, mais la polémique est désamorcée.
Par *archicratialisation*, nous désignons ce régime post-interpellatif par lequel une régulation intègre la mémoire d’épreuves passées (plaintes, controverses, mobilisations) sans pour autant rouvrir de scène : les critiques sédimentent et se transmutent silencieusement en ajustements d’*arcalité* (fondements, critères, axiomes) et de *cratialité* (procédures, seuils, algorithmes, interfaces), sans comparution, sans publicité, sans délai contradictoire nouvellement institué. L’*archicratialisation* n’est donc pas l’*archicration* (qui expose et oppose), mais son après-coup opératoire : une auto-réforme qui peut corriger, parfois avec finesse, tout en déplaçant hors scène le litige qui l’a rendue nécessaire ; d’où son ambivalence. Elle peut être féconde si elle demeure traçable et ré-ouvrable, mais devient inquiétante si elle neutralise la polémique en retirant aux concernés la possibilité d’une réinterpellation opposable.
Par *archicratialisation*, nous désignons ce régime post-interpellatif par lequel une régulation intègre la mémoire d’épreuves passées (plaintes, controverses, mobilisations) sans pour autant rouvrir de scène effective : les critiques sédimentent et se transmutent silencieusement en ajustements d’*arcalité* (fondements, critères, axiomes) et de *cratialité* (procédures, seuils, algorithmes, interfaces), sans comparution véritablement praticable, sans publicité opposable, sans délai contradictoire nouvellement institué et opérant. L’*archicratialisation* n’est donc pas l’*archicration* (qui expose et oppose), mais son après-coup opératoire : une auto-réforme qui peut corriger, parfois avec finesse, tout en déplaçant hors scène le litige qui l’a rendue nécessaire ; d’où son ambivalence. Elle peut être féconde si elle demeure traçable et ré-ouvrable, mais devient inquiétante si elle neutralise la polémique en retirant aux concernés la possibilité d’une réinterpellation opposable.
Enfin, il existe ce que nous nommons les *contre-scènes auto-organisées*, ou *pré-archicrations*. Là, ce n’est plus l’ordre institué qui ouvre une scène, mais les sujets eux-mêmes qui la fabriquent, à même leurs expériences. Ce sont des collectifs d’écoute mutuelle, des cercles d’entraide communautaire, des réunions de colocataires qui instituent des modalités de parole, de suspension, de reconnaissance, de réparation. Ces scènes ne contestent pas toujours l’ordre en bloc : elles déploient un à-côté, un écart, une grammaire marginale de la régulation. Elles tissent leurs propres conditions de comparution, de mémoire, de reprise — et parfois, dans un second temps, elles viennent frapper à la porte de l’institution pour l’obliger à s’ajuster.
@@ -1518,8 +1528,8 @@ Dans cette optique, le chapitre 2 constituera l’épreuve originelle. En retra
Ce qui s’ouvre alors est un laboratoire de différenciation régulatrice. Le paradigme archicratique, tel qu’il a été posé ici dans sa topologie formelle, trouvera dans le chapitre 2 les premières scènes d’épreuve de sa pertinence différentielle : il y montrera qu’il est capable de rendre intelligible ce qui varie réellement d’un régime à l’autre — selon la manière dont il fonde, opère, ou se laisse interroger. Loin d’imposer ses catégories, il s’y laissera *moduler* par les formes historiques, dans une dynamique de transduction critique : non pour valider son propre appareil conceptuel, mais pour en éprouver les seuils, les écarts, les tensions fécondes.
Car si l’*arcalité* ne précède pas toujours la *cratialité*, si la scène d’*archicration* n’est pas partout instituée, si certaines *co-viabilités* se tiennent sans fondement explicite, sans suspension déclarée, sans mémoire stabilisée, alors il faut reconnaître que le paradigme ne détermine pas les régimes qu’il analyse : il s’expose à leur complexité. Il n’est pas un moule, mais un geste de lecture — une manière de faire apparaître les points de tension, de redistribution et de transformation. Le chapitre 2 montrera alors comment des configurations archaïques ou proto-politiques, souvent considérées comme « pré-étatiques », témoignent en réalité de formes complexes de régulation différée, de justification rituelle, d’adresse symbolique, de seuils narratifs ou d’épreuves collectives. Il révélera que l’*archicration* ne coïncide pas nécessairement avec la démocratie formelle, pas plus que la *cratialité* avec la souveraineté étatique, ni l’*arcalité* avec la seule Loi écrite : les régimes de viabilité se sont déployés à travers des architectures plus hétérogènes, plus fragiles, plus inventives qu’on ne le suppose.
Car si l’arcalité ne précède pas toujours la cratialité, si la scène d’archicration n’est pas partout formellement instituée, si certaines co-viabilités se soutiennent à travers des fondements implicites, des suspensions peu codifiées ou des mémoires faiblement stabilisées, alors il faut reconnaître que le paradigme ne détermine pas les régimes qu’il analyse : il s’expose à leur complexité. Il n’est pas un moule, mais un geste de lecture — une manière de faire apparaître les points de tension, de redistribution et de transformation. Le chapitre 2 montrera alors comment des configurations archaïques ou proto-politiques, souvent considérées comme « pré-étatiques », témoignent en réalité de formes complexes de régulation différée, de justification rituelle, d’adresse symbolique, de seuils narratifs ou d’épreuves collectives. Il révélera que l’archicration ne coïncide pas nécessairement avec la démocratie formelle, pas plus que la cratialité avec la souveraineté étatique, ni l’arcalité avec la seule Loi écrite : les régimes de viabilité se sont déployés à travers des architectures plus hétérogènes, plus fragiles, plus inventives qu’on ne le suppose.
C’est donc une archéologie morphologique qui s’ouvrira. Non pas pour remonter à un commencement, mais pour différencier les formes premières de régulation tenue, et ainsi éprouver en retour — par contraste, par confrontation, par amplification — la validité opératoire du paradigme archicratique. Car ce n’est qu’en le confrontant à ces régimes situés qui n’avaient pas encore nos institutions, nos catégories, ni nos grammaires politiques, que nous pourrons établir qu’il s’agit bien d’un paradigme de lecture différenciateur, et non d’un système normatif reconduit à rebours.
Ainsi se clôt ce chapitre 1 : non dans la certitude d’un modèle à dérouler, mais dans l’ouverture d’un champ d’épreuve critique. Car si l’*archicration*, comme nous l’avons montré, constitue bien la condition d’habitabilité d’un ordre, alors c’est aussi elle qui nous oblige à interroger chaque régulation depuis ses effets, ses manques, ses devenirs, et non depuis nos catégories héritées. Ce que le chapitre 2 vient éprouver, c’est donc moins l’origine du paradigme que sa capacité à rendre compte, sans réduction, des formes historiques de la viabilité collective, autrement dit, sa *co-viabilité*.
Ainsi se clôt ce chapitre 1 : non dans la certitude d’un modèle à dérouler, mais dans l’ouverture d’un champ d’épreuve critique. Car si l’archicration constitue bien la condition d’habitabilité d’un ordre, encore faut-il reconnaître qu’elle ne se donne pas partout sous la forme d’une scène pleinement formalisée, stable ou juridiquement instituée. Elle peut être diffuse, rituelle, intermittente, faiblement codifiée ; mais partout où elle est durablement neutralisée, vidée de sa prise, relocalisée hors d’atteinte ou rendue pratiquement inopérante, la régulation se ferme, devient indisponible à ses affectés et compromet sa propre co-viabilité. C’est pourquoi chaque ordre doit être interrogé depuis les formes concrètes par lesquelles il rend ses décisions, ses justifications et ses tensions exposables, discutables et révisables, plutôt que depuis nos seules catégories héritées. Ce que le chapitre 2 vient alors éprouver, ce n’est pas l’origine pure du paradigme, mais sa capacité à rendre compte, sans réduction, de la diversité historique des formes de viabilité collective — autrement dit, de leur co-viabilité.
@@ -14,7 +14,7 @@ source:
---
Ce chapitre se tient à un point nodal de notre essai-thèse : il ouvre un espace d’exploration systématique des formes conceptuelles et philosophiques à travers lesquelles le pouvoir se configure comme régime de régulation. Il ne s’agit pas ici de revenir une nouvelle fois sur les fondements de l’autorité, ni d’interroger la légitimité politique au sens classique du terme, ni même d’enquêter sur la genèse des institutions. L’ambition est autre, structurelle, transversale, morphologique, elle tentera d’arpenter, à même les dispositifs, les pensées, les théorisations et les expériences, les modalités différentiées par lesquelles s’instaurent, s’éprouvent et se disputent les formes de régulation du vivre-ensemble.
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point d’origine, ne prétend restituer aucune ontologie stable du politique. Ce qu’il donne à lire, c’est une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières.
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point d’origine, ne prétend restituer aucune ontologie stable du politique. Ce qu’il donne à lire, c’est une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières, et souvent complémentaires.
Ainsi, loin d’être une galerie illustrative de théories politiques juxtaposées, le chapitre s’agence comme une topologie critique, une plongée stratigraphique dans les scènes où s’articule la régulation — entendue ici non comme stabilisation externe ou ajustement technico-fonctionnel, mais comme dispositif instituant, tension structurante, scène traversée de conflictualité et d’exigence normative. Car à nos yeux, la régulation n’est pas ce qui vient après le pouvoir, elle en est la forme même constitutive — son architecture, son rythme, son épaisseur. Elle est ce par quoi le pouvoir ne se contente pas d’être exercé, mais s’institue, se justifie, se dispute, se recompose.
Loaded 100 of 199 files, more files were not shown because too many files have changed in this diff. Show more