Compare commits
308 Commits
chore/url-
...
chore/docx
| Author | SHA1 | Date | |
|---|---|---|---|
| ec8e29a313 | |||
| 1dc9a60580 | |||
| ee18b26d03 | |||
| 5f4a0f74db | |||
| 6b17df7320 | |||
| 0c33495342 | |||
| d8a09b1def | |||
| 39af501ea0 | |||
| 4c821d9e83 | |||
| deb4a91348 | |||
| 5b36b8e54e | |||
| eda5a877ef | |||
| 5b615a6999 | |||
| 99cf0947da | |||
| dbd1e14e4e | |||
| 7033354011 | |||
| 7345730e3c | |||
| cea94c56db | |||
| c1e24736e3 | |||
| 24bbfbc17f | |||
| a11e2f1d18 | |||
| 630b146d02 | |||
| 551360db83 | |||
| a96c282780 | |||
| d2e0f147c2 | |||
| ad95364021 | |||
| e48e322363 | |||
| a9f2a5bbd4 | |||
| 0cba8f868e | |||
| f8e3ee4cca | |||
| 92e0ad01c6 | |||
| e6c18d6b16 | |||
| a3092f5d5b | |||
| 7187b69935 | |||
| 4ba4453661 | |||
| ee42e391e3 | |||
| f7756be59e | |||
| 4abe70e10e | |||
| b2b4ec35c0 | |||
| b255436958 | |||
| ad06b34a85 | |||
| a38f585f3d | |||
| bf0dc125d1 | |||
| f61dc15b47 | |||
| 1ac3d91a19 | |||
| 100ba10409 | |||
| 5f14785abb | |||
| c7043ae9d5 | |||
| bd1235f8c3 | |||
| 7ae7b4dca3 | |||
| f088db57d4 | |||
| 311e94ed91 | |||
| e078f3f9ab | |||
| 7c4bb5a2cf | |||
| 214e174635 | |||
| f1b2f4605f | |||
| 87955adf5d | |||
| e39a0c547d | |||
| c89ddf7237 | |||
| 615effe8bf | |||
| e952b344a0 | |||
| bb0572cc1a | |||
| f6a2347278 | |||
| d902c2bf98 | |||
| baa2082f51 | |||
| 2f249b420f | |||
| d6b4eb82f4 | |||
| bfa44fecda | |||
| e329235aa9 | |||
| 8cbaa5117c | |||
| 3086f333ed | |||
| c1c3c19d13 | |||
| ddcd0acd4d | |||
| 9bc4eeb3e7 | |||
| 7a9a5319ac | |||
| 7d75de5c9f | |||
| 69c91cb661 | |||
| a1bfbf4405 | |||
|
|
be26b425d8 | ||
|
|
abf88e7037 | ||
| 04fee32fdb | |||
|
|
fbddf5c3fc | ||
|
|
bad748df3a | ||
| 0066cf8601 | |||
| 5d3473d66c | |||
| f9d34110e4 | |||
|
|
84e9c3ead4 | ||
|
|
72e59175fc | ||
| 81b69ac6d5 | |||
| 513ae72e85 | |||
| 4c4dd1c515 | |||
| 46b15ed6ab | |||
| a015e72f7c | |||
|
|
d5df7d77a0 | ||
| ec3ceee862 | |||
| 867475c3ff | |||
| b024c5557c | |||
| 93306f360d | |||
| 52847d999d | |||
| b9629b43ff | |||
| 06482a9f8d | |||
| f2e4ae5ac2 | |||
| 71baf0f6da | |||
| d02b6fc347 | |||
| 431f1e347b | |||
| ab6f45ed5c | |||
| 02c060d239 | |||
| be2029de82 | |||
| e148eaeaf3 | |||
| c63a1e6ce4 | |||
| b3a73a7781 | |||
| 1968585d0f | |||
| b33c758411 | |||
| afa543125c | |||
|
|
0d0252cac0 | ||
|
|
a8bd9aeed5 | ||
| d277c61afd | |||
|
|
86479952d1 | ||
| c94024a8ae | |||
| 70611d16f8 | |||
| 354db231b8 | |||
| 9d8d60d00f | |||
| f5d25abbec | |||
| 8e9f7314f5 | |||
| 03b88b944d | |||
| 385c36f660 | |||
| cfa092cd38 | |||
| 1a762f8f54 | |||
| fbdaf72775 | |||
| 67128a9ca1 | |||
| 898759db3d | |||
| 4f009a9557 | |||
| 378d0981f0 | |||
| 8f3702f803 | |||
| cfd303fc85 | |||
| 0fc0976f8a | |||
| e247ea8ead | |||
| 0c57c4bc6d | |||
| 9b7998e1c3 | |||
| 8997a00413 | |||
| a2e6f6185f | |||
| c2715b01d7 | |||
| 6f09dfcd12 | |||
| bb9f55a3b5 | |||
| 298ee7492c | |||
| 37cb836246 | |||
| 19e3318125 | |||
| 683b02f4a0 | |||
| 20aecc30b1 | |||
| daf57aa152 | |||
| bfd693de92 | |||
| ea2ad0017b | |||
| 82e7473cac | |||
| 315523e80f | |||
| 569b6de154 | |||
| 95f8159554 | |||
|
|
5698c494f1 | ||
| e640e66b8d | |||
|
|
9be7d170c6 | ||
| c2c98c516b | |||
| 32554f5998 | |||
| 308f4f92bc | |||
| 4dfd3b026b | |||
| c93f274f41 | |||
| dfa311fb5b | |||
| 3ef1dc2801 | |||
| 435e41ed4d | |||
| 8825932159 | |||
| b55decbea4 | |||
| 414a848db3 | |||
| cbd4f3a57f | |||
| 49f8d6a95e | |||
| 5afa5cbfda | |||
| a1b1df38ba | |||
| d3f7d74da7 | |||
| 6919190107 | |||
| 021ef5abd7 | |||
| 76cdc85f9c | |||
| f2f6df2127 | |||
| dfe13757f7 | |||
| 148ac997df | |||
| 84492d2741 | |||
| 81baadd57f | |||
| 63d0ffc5fc | |||
| 24143fc2c4 | |||
| 55370b704f | |||
| b8a3ce1337 | |||
| 7f9baedf41 | |||
| 1adbe1c7a3 | |||
| 107a26352f | |||
| 1c2b9ddbb6 | |||
| be99460d4d | |||
| 9e1b704aa6 | |||
| 941fbf5845 | |||
| 0b4a31a432 | |||
| c617dc3979 | |||
| 1b95161de0 | |||
| ebd976bd46 | |||
| f8d57d8fe0 | |||
| 09a4d2c472 | |||
| 1f6dc874d0 | |||
| 4dd63945ee | |||
| ba64b0694b | |||
| 58e5ceda59 | |||
| 08f826ee01 | |||
| 3358d280ec | |||
| 9cb0d5e416 | |||
| a46f058917 | |||
| 604b2199da | |||
| d153f71be6 | |||
| 8f64e4b098 | |||
| 459bf195d8 | |||
| 0c46b0d19b | |||
| bfbdc7b688 | |||
| 8fd53dd4d2 | |||
|
|
c8bbee4f74 | ||
| 04cdf54eb7 | |||
|
|
d6bf645ae9 | ||
| 1ca6bcbd81 | |||
| dec5f8eba7 | |||
| 716c887045 | |||
| 9b1789a164 | |||
| 17fa39c7ff | |||
| 8132e315f4 | |||
| 8d993915d7 | |||
| 497bddd05d | |||
| 7c8e49c1a9 | |||
| 901d28b89b | |||
| 43e2862c89 | |||
| 73fb38c4d1 | |||
| a81d206aba | |||
| 9801ea3cea | |||
| c11189fe11 | |||
| b47edb24cf | |||
| be191b09a0 | |||
| e06587478d | |||
| 402ffb04cd | |||
| 1cbfc02670 | |||
| 28d2fbbd2f | |||
| 225368a952 | |||
| 3574695041 | |||
| ea68025a1d | |||
| 3a08698003 | |||
| 3d583608c2 | |||
|
|
01ae95ab43 | ||
|
|
0d5821c640 | ||
|
|
2bcea39558 | ||
| af85970d4a | |||
| 210f621487 | |||
| 8ad960dc69 | |||
| d45a8b285f | |||
| b6e04a9138 | |||
| dcf1fc2d0b | |||
| 41b0517c6c | |||
| 6b43eb199d | |||
| d40f24e92d | |||
| 480a61b071 | |||
| a5d68d6a7e | |||
| 390f2c33e5 | |||
| 16485dc4a9 | |||
| a43ce5f188 | |||
| 0519ae2dd0 | |||
| 0d5b790e52 | |||
| 342e21b9ea | |||
| 4dec9e182b | |||
| c7ae883c6a | |||
| 9b4584f70a | |||
| 7b64fb7401 | |||
|
|
57cb23ce8b | ||
| 708b87ff35 | |||
| 577cfd08e8 | |||
| de9edbe532 | |||
| 5e95dc9898 | |||
| 006fec7efd | |||
| 2b612214bb | |||
| 29a6c349aa | |||
|
|
33a227c401 | ||
| 396ad4df7c | |||
|
|
0b39427090 | ||
| 8fcb18cb46 | |||
| d03fc519de | |||
| 97dd3797d6 | |||
| 6c7b7ab6a0 | |||
| 105dfe1b5b | |||
| 82f6453538 | |||
| fe862102d3 | |||
| 6ef538a0c4 | |||
| 689612ff7f | |||
| 7b135a4707 | |||
| 0cb8a54195 | |||
| a7a333397d | |||
| eb1d444776 | |||
| 68c3416594 | |||
| ae809e0152 | |||
| 7444eeb532 | |||
| 9bbebf5886 | |||
| fe7810671d | |||
| 53562025ac | |||
| 2b35315466 | |||
| 1b7f23d0a6 | |||
| 3d1d4d7952 | |||
| 3320563e1b | |||
| 798b2ddd0b | |||
| 31d4896f5d | |||
| 3fda37491d | |||
| 488c02b8b5 | |||
| f9ea3760e2 | |||
| 00e1a1d4b0 |
@@ -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
.gitea/workflows/anno-apply-pr.yml
Normal file
450
.gitea/workflows/anno-apply-pr.yml
Normal 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
.gitea/workflows/anno-reject.yml
Normal file
181
.gitea/workflows/anno-reject.yml
Normal 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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
613
.gitea/workflows/deploy-staging-live.yml
Normal file
613
.gitea/workflows/deploy-staging-live.yml
Normal file
@@ -0,0 +1,613 @@
|
||||
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
|
||||
|
||||
BUILD_TIME_RAW="$(TZ=Europe/Paris date '+%Y-%m-%dT%H:%M:%S%z')"
|
||||
BUILD_TIME="${BUILD_TIME_RAW:0:${#BUILD_TIME_RAW}-2}:${BUILD_TIME_RAW:${#BUILD_TIME_RAW}-2}"
|
||||
|
||||
PUBLIC_OPS_ENV=staging \
|
||||
PUBLIC_OPS_UPSTREAM=web_blue \
|
||||
PUBLIC_BUILD_SHA="${AFTER}" \
|
||||
PUBLIC_BUILD_TIME="${BUILD_TIME}" \
|
||||
node scripts/write-ops-health.mjs
|
||||
|
||||
test -f public/__ops/health.json
|
||||
echo "=== public/__ops/health.json (blue/staging) ==="
|
||||
cat public/__ops/health.json
|
||||
|
||||
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"
|
||||
|
||||
wait_url "http://127.0.0.1:8081/__ops/health.json" "blue ops health"
|
||||
|
||||
curl -fsS --max-time 6 "http://127.0.0.1:8081/__ops/health.json" \
|
||||
| python3 -c 'import sys, json; j=json.load(sys.stdin); print("env=", j.get("env")); print("upstream=", j.get("upstream")); print("buildSha=", j.get("buildSha")); print("builtAt=", j.get("builtAt"))'
|
||||
|
||||
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_TIME_RAW="$(TZ=Europe/Paris date '+%Y-%m-%dT%H:%M:%S%z')"
|
||||
BUILD_TIME="${BUILD_TIME_RAW:0:${#BUILD_TIME_RAW}-2}:${BUILD_TIME_RAW:${#BUILD_TIME_RAW}-2}"
|
||||
|
||||
PUBLIC_OPS_ENV=prod \
|
||||
PUBLIC_OPS_UPSTREAM=web_green \
|
||||
PUBLIC_BUILD_SHA="${AFTER}" \
|
||||
PUBLIC_BUILD_TIME="${BUILD_TIME}" \
|
||||
node scripts/write-ops-health.mjs
|
||||
|
||||
test -f public/__ops/health.json
|
||||
echo "=== public/__ops/health.json (green/prod) ==="
|
||||
cat public/__ops/health.json
|
||||
|
||||
# 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
|
||||
|
||||
if ! wait_url "http://127.0.0.1:8082/__ops/health.json" "green ops health"; then rollback; exit 4; fi
|
||||
|
||||
curl -fsS --max-time 6 "http://127.0.0.1:8082/__ops/health.json" \
|
||||
| python3 -c 'import sys, json; j=json.load(sys.stdin); print("env=", j.get("env")); print("upstream=", j.get("upstream")); print("buildSha=", j.get("buildSha")); print("builtAt=", j.get("builtAt"))'
|
||||
|
||||
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
|
||||
788
.gitea/workflows/proposer-apply-pr.yml
Normal file
788
.gitea/workflows/proposer-apply-pr.yml
Normal file
@@ -0,0 +1,788 @@
|
||||
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 (tolerant on empty issue label payload)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
|
||||
echo "event=$EVENT_NAME label=${LABEL_NAME:-<empty>}"
|
||||
|
||||
if [[ "$EVENT_NAME" == "issues" ]]; then
|
||||
if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/approved" ]]; then
|
||||
echo "issues/labeled with explicit non-approved label=$LABEL_NAME -> skip"
|
||||
echo 'SKIP=1' >> /tmp/proposer.env
|
||||
echo 'SKIP_REASON="label_not_state_approved_event"' >> /tmp/proposer.env
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Proceed to API-based selection/gating"
|
||||
|
||||
- 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: Derive deterministic batch identity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
|
||||
|
||||
export TARGET_ISSUES TARGET_CHEMIN
|
||||
|
||||
node --input-type=module - <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const issues = String(process.env.TARGET_ISSUES || "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => Number(a) - Number(b));
|
||||
|
||||
const chemin = String(process.env.TARGET_CHEMIN || "").trim();
|
||||
const keySource = `${chemin}::${issues.join(",")}`;
|
||||
const hash = crypto.createHash("sha1").update(keySource).digest("hex").slice(0, 12);
|
||||
const primary = issues[0] || "0";
|
||||
const batchBranch = `bot/proposer-${primary}-${hash}`;
|
||||
|
||||
fs.appendFileSync(
|
||||
"/tmp/proposer.env",
|
||||
[
|
||||
`BATCH_KEY=${JSON.stringify(keySource)}`,
|
||||
`BATCH_HASH=${JSON.stringify(hash)}`,
|
||||
`BATCH_BRANCH=${JSON.stringify(batchBranch)}`
|
||||
].join("\n") + "\n"
|
||||
);
|
||||
NODE
|
||||
|
||||
echo "Batch identity:"
|
||||
grep -E '^(BATCH_KEY|BATCH_HASH|BATCH_BRANCH)=' /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:-}"
|
||||
export BATCH_BRANCH="${BATCH_BRANCH:-}"
|
||||
export BATCH_KEY="${BATCH_KEY:-}"
|
||||
|
||||
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 batchBranch = String(process.env.BATCH_BRANCH || "");
|
||||
const batchKey = String(process.env.BATCH_KEY || "");
|
||||
|
||||
const proposerOpen = Array.isArray(pulls)
|
||||
? pulls.filter((pr) => String(pr?.head?.ref || "").startsWith("bot/proposer-"))
|
||||
: [];
|
||||
|
||||
const sameBatch = proposerOpen.find((pr) => {
|
||||
const ref = String(pr?.head?.ref || "");
|
||||
const title = String(pr?.title || "");
|
||||
const body = String(pr?.body || "");
|
||||
|
||||
if (batchBranch && ref === batchBranch) return true;
|
||||
if (batchKey && body.includes(`Batch-Key: ${batchKey}`)) return true;
|
||||
|
||||
return issues.some((n) =>
|
||||
ref.startsWith(`bot/proposer-${n}-`) ||
|
||||
title.includes(`#${n}`) ||
|
||||
body.includes(`#${n}`) ||
|
||||
body.includes(`ticket #${n}`)
|
||||
);
|
||||
});
|
||||
|
||||
const out = [];
|
||||
|
||||
if (sameBatch) {
|
||||
out.push("SKIP=1");
|
||||
out.push(`SKIP_REASON=${JSON.stringify("issue_already_has_open_pr")}`);
|
||||
out.push(`OPEN_PR_URL=${JSON.stringify(String(sameBatch.html_url || sameBatch.url || ""))}`);
|
||||
out.push(`OPEN_PR_BRANCH=${JSON.stringify(String(sameBatch?.head?.ref || ""))}`);
|
||||
} 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: Guard on remote batch branch before heavy work
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
|
||||
|
||||
if git ls-remote --exit-code --heads origin "$BATCH_BRANCH" >/dev/null 2>&1; then
|
||||
echo 'SKIP=1' >> /tmp/proposer.env
|
||||
echo 'SKIP_REASON="batch_branch_exists_without_pr"' >> /tmp/proposer.env
|
||||
echo "OPEN_PR_BRANCH=${BATCH_BRANCH}" >> /tmp/proposer.env
|
||||
echo "Remote batch branch already exists -> skip duplicate materialization"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Remote batch branch is free"
|
||||
|
||||
- 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
|
||||
|
||||
if [[ "${SKIP_REASON:-}" == "label_not_state_approved_event" || "${SKIP_REASON:-}" == "label_not_state_approved" ]]; then
|
||||
echo "Skip reason=${SKIP_REASON} -> no comment"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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 batch already has an open proposer PR: ${OPEN_PR_URL:-"(URL unavailable)"}"
|
||||
;;
|
||||
batch_branch_exists_without_pr)
|
||||
MSG="This batch already has a remote batch branch (${OPEN_PR_BRANCH:-"(unknown branch)"}). Manual inspection is required before any new proposer PR is created."
|
||||
;;
|
||||
batch_branch_already_materialized)
|
||||
MSG="This batch was already materialized by another run on branch ${OPEN_PR_BRANCH:-"(unknown branch)"}. No duplicate PR was created."
|
||||
;;
|
||||
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
|
||||
|
||||
export MSG
|
||||
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)"
|
||||
BR="$BATCH_BRANCH"
|
||||
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: Late guard against duplicate batch materialization
|
||||
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
|
||||
|
||||
REMOTE_SHA="$(git ls-remote --heads origin "$BATCH_BRANCH" | awk 'NR==1 {print $1}')"
|
||||
|
||||
if [[ -n "${REMOTE_SHA:-}" && "${REMOTE_SHA}" != "${END_SHA:-}" ]]; then
|
||||
echo 'SKIP=1' >> /tmp/proposer.env
|
||||
echo 'SKIP_REASON="batch_branch_already_materialized"' >> /tmp/proposer.env
|
||||
echo "OPEN_PR_BRANCH=${BATCH_BRANCH}" >> /tmp/proposer.env
|
||||
echo "Remote batch branch already exists at $REMOTE_SHA -> skip duplicate push/PR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Late guard OK"
|
||||
|
||||
- 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; }
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "Missing FORGE_TOKEN"; exit 1; }
|
||||
|
||||
OPEN_PRS_JSON="$(curl -fsS \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls?state=open&limit=100")"
|
||||
|
||||
export OPEN_PRS_JSON BATCH_BRANCH BATCH_KEY
|
||||
|
||||
EXISTING_PR_URL="$(node --input-type=module -e '
|
||||
const pulls = JSON.parse(process.env.OPEN_PRS_JSON || "[]");
|
||||
const branch = String(process.env.BATCH_BRANCH || "");
|
||||
const key = String(process.env.BATCH_KEY || "");
|
||||
const current = Array.isArray(pulls)
|
||||
? pulls.find((pr) => {
|
||||
const ref = String(pr?.head?.ref || "");
|
||||
const body = String(pr?.body || "");
|
||||
return (branch && ref === branch) || (key && body.includes(`Batch-Key: ${key}`));
|
||||
})
|
||||
: null;
|
||||
process.stdout.write(current ? String(current.html_url || current.url || "") : "");
|
||||
')"
|
||||
|
||||
if [[ -n "${EXISTING_PR_URL:-}" ]]; then
|
||||
echo "PR already exists for this batch: $EXISTING_PR_URL"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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 PR_TITLE TARGET_CHEMIN TARGET_ISSUES BRANCH END_SHA DEFAULT_BRANCH OWNER BATCH_KEY
|
||||
|
||||
node --input-type=module -e '
|
||||
import fs from "node:fs";
|
||||
|
||||
const issues = String(process.env.TARGET_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.TARGET_CHEMIN || "(inconnu)"}`,
|
||||
"- Tickets:",
|
||||
...issues.map((n) => ` - #${n}`),
|
||||
`- Branche: ${process.env.BRANCH || ""}`,
|
||||
`- Commit: ${process.env.END_SHA || "unknown"}`,
|
||||
`- Batch-Key: ${process.env.BATCH_KEY || ""}`,
|
||||
"",
|
||||
"Merge si CI OK."
|
||||
].join("\n");
|
||||
|
||||
fs.writeFileSync(
|
||||
"/tmp/proposer.pr.json",
|
||||
JSON.stringify({
|
||||
title: process.env.PR_TITLE || "proposer: apply tickets",
|
||||
body,
|
||||
base: process.env.DEFAULT_BRANCH || "main",
|
||||
head: `${process.env.OWNER}:${process.env.BRANCH}`,
|
||||
allow_maintainer_edit: true
|
||||
})
|
||||
);
|
||||
'
|
||||
|
||||
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 -e '
|
||||
import fs from "node:fs";
|
||||
|
||||
const issue = process.env.ISSUE || "";
|
||||
const url = process.env.PR_URL || "";
|
||||
const msg =
|
||||
`PR proposer creee pour le ticket #${issue} : ${url}\n\n` +
|
||||
`Le ticket est cloture automatiquement ; la discussion peut se poursuivre dans la PR.`;
|
||||
|
||||
fs.writeFileSync(
|
||||
"/tmp/proposer.issue.close.comment.json",
|
||||
JSON.stringify({ body: msg })
|
||||
);
|
||||
'
|
||||
|
||||
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"}'
|
||||
|
||||
ISSUE_STATE="$(curl -fsS \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" | \
|
||||
node --input-type=module -e 'let s=""; process.stdin.on("data", d => s += d); process.stdin.on("end", () => { const j = JSON.parse(s || "{}"); process.stdout.write(String(j.state || "")); });')"
|
||||
|
||||
[[ "$ISSUE_STATE" == "closed" ]] || {
|
||||
echo "Issue #$ISSUE is still not closed after PATCH"
|
||||
exit 1
|
||||
}
|
||||
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"
|
||||
@@ -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"
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -3,6 +3,10 @@
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# dev-only
|
||||
public/_auth/whoami
|
||||
public/_auth/whoami/*
|
||||
|
||||
# --- local backups ---
|
||||
*.bak
|
||||
*.bak.*
|
||||
@@ -21,3 +25,10 @@ dist/
|
||||
# local backups
|
||||
Dockerfile.bak.*
|
||||
public/favicon_io.zip
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# local temp workspace
|
||||
.tmp/
|
||||
public/__ops/health.json
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -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
|
||||
@@ -86,6 +86,10 @@ function rehypeDedupeIds() {
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
legacy: {
|
||||
collectionsBackwardsCompat: true,
|
||||
},
|
||||
|
||||
output: "static",
|
||||
trailingSlash: "always",
|
||||
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
|
||||
|
||||
5
config/anchor-churn-allowlist.json
Normal file
5
config/anchor-churn-allowlist.json
Normal 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,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
docs/EDITORIAL-ANNOTATIONS-SPEC.md
Normal file
327
docs/EDITORIAL-ANNOTATIONS-SPEC.md
Normal 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
|
||||
@@ -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.
|
||||
|
||||
1393
docs/OPS-LOCALHOST-AUTO-SYNC.md
Normal file
1393
docs/OPS-LOCALHOST-AUTO-SYNC.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
683
docs/START-HERE.md
Normal file
683
docs/START-HERE.md
Normal file
@@ -0,0 +1,683 @@
|
||||
# START-HERE — Archicratie / Édition Web (v3)
|
||||
> Onboarding + exploitation “nickel chrome” (DEV → Gitea → CI → Release → Blue/Green → Edge/SSO → localhost auto-sync)
|
||||
|
||||
## 0) TL;DR (la règle d’or)
|
||||
|
||||
- **Gitea = source canonique**.
|
||||
- **`main` est protégée** : toute modification passe par **branche → PR → CI → merge**.
|
||||
- **Le NAS n’est pas la source** : si un hotfix est fait sur NAS, il doit être **backporté immédiatement** via PR.
|
||||
- **Le site est statique Astro** : la prod sert du HTML via nginx ; l’accès est contrôlé au niveau reverse-proxy (Traefik + Authelia).
|
||||
- **Le localhost automatique n’est pas le repo de dev** : il tourne depuis un **worktree dédié**, synchronisé sur `origin/main`.
|
||||
|
||||
---
|
||||
|
||||
## 1) Architecture mentale (ultra simple)
|
||||
|
||||
- **DEV canonique (Mac Studio)** : édition, dev, tests, commits, pushes
|
||||
- **Gitea** : dépôt canonique, PR, CI, workflows éditoriaux
|
||||
- **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)** : routage des hosts
|
||||
- `staging.archicratie...` → 8081
|
||||
- `archicratie...` → 8082
|
||||
- **Authelia** devant, via middleware `chain-auth@file`
|
||||
- **Localhost auto-sync**
|
||||
- un **repo canonique de développement**
|
||||
- un **worktree localhost miroir de `origin/main`**
|
||||
- un **agent de sync**
|
||||
- un **agent Astro**
|
||||
|
||||
---
|
||||
|
||||
## 2) Répertoires & conventions (repo)
|
||||
|
||||
### 2.1 Contenu canon (édition)
|
||||
|
||||
- `src/content/**` : contenu MD / MDX canon
|
||||
- `src/pages/**` : routes Astro
|
||||
- `src/components/**` : composants UI
|
||||
- `src/layouts/**` : layouts
|
||||
- `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` : retrait IDs dupliqués
|
||||
- `scripts/build-para-index.mjs` : index paragraphes
|
||||
- `scripts/build-annotations-index.mjs` : index annotations
|
||||
- `scripts/check-anchors.mjs` : contrat stabilité d’ancres
|
||||
- `scripts/check-annotations*.mjs` : sanity YAML + médias
|
||||
|
||||
> Important : ces scripts ne sont pas accessoires.
|
||||
> Ils font partie du contrat de stabilité éditoriale.
|
||||
|
||||
---
|
||||
|
||||
## 3) Les trois espaces à ne jamais confondre
|
||||
|
||||
### 3.1 Repo canonique de développement
|
||||
|
||||
```text
|
||||
/Volumes/FunIA/dev/archicratie-edition/site
|
||||
```
|
||||
|
||||
Usage :
|
||||
|
||||
- développement normal
|
||||
- branches de travail
|
||||
- nouvelles fonctionnalités
|
||||
- corrections manuelles
|
||||
- commits
|
||||
- pushes
|
||||
- PR
|
||||
|
||||
### 3.2 Worktree localhost miroir de `main`
|
||||
|
||||
```text
|
||||
/Users/s-funia/ops-local/archicratie/localhost-worktree
|
||||
```
|
||||
|
||||
Branche attendue :
|
||||
|
||||
```text
|
||||
localhost-sync
|
||||
```
|
||||
|
||||
Usage :
|
||||
|
||||
- exécuter le localhost automatique
|
||||
- refléter `origin/main`
|
||||
- ne jamais servir d’espace de développement
|
||||
|
||||
### 3.3 Ops local hors repo
|
||||
|
||||
```text
|
||||
/Users/s-funia/ops-local/archicratie
|
||||
```
|
||||
|
||||
Usage :
|
||||
|
||||
- scripts d’exploitation
|
||||
- état
|
||||
- logs
|
||||
- automatisation `launchd`
|
||||
|
||||
---
|
||||
|
||||
## 4) Pourquoi cette séparation existe
|
||||
|
||||
Il ne faut pas utiliser le repo canonique de développement comme serveur localhost permanent.
|
||||
|
||||
Sinon on mélange :
|
||||
|
||||
- travail en cours
|
||||
- commits non poussés
|
||||
- essais temporaires
|
||||
- état réellement publié sur `main`
|
||||
|
||||
Le résultat devient ambigu.
|
||||
|
||||
La séparation retenue est donc :
|
||||
|
||||
- **repo canonique** = espace de développement
|
||||
- **worktree localhost** = miroir exécutable de `origin/main`
|
||||
- **ops local** = scripts et automatisation
|
||||
|
||||
C’est cette séparation qui rend le système lisible, robuste et opérable.
|
||||
|
||||
---
|
||||
|
||||
## 5) Workflow Git “pro” (main protégée)
|
||||
|
||||
### 5.1 Cycle standard (toute modif)
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
### 5.2 PR vers `main`
|
||||
|
||||
- ouvrir une PR dans Gitea
|
||||
- attendre une CI verte
|
||||
- merger
|
||||
- laisser les workflows faire le reste
|
||||
|
||||
### 5.3 Cas spécial : hotfix prod (NAS)
|
||||
|
||||
On peut faire un hotfix d’urgence côté NAS si nécessaire.
|
||||
|
||||
Mais l’état final doit toujours revenir dans Gitea :
|
||||
|
||||
- branche
|
||||
- PR
|
||||
- CI
|
||||
- merge
|
||||
|
||||
---
|
||||
|
||||
## 6) Déploiement (NAS) — principe
|
||||
|
||||
### 6.1 Release pack
|
||||
|
||||
On génère un pack reproductible, puis on déploie.
|
||||
|
||||
### 6.2 Blue/Green
|
||||
|
||||
- `web_blue` = staging (`8081`)
|
||||
- `web_green` = live (`8082`)
|
||||
|
||||
Le reverse-proxy choisit l’upstream selon le host demandé.
|
||||
|
||||
---
|
||||
|
||||
## 7) Happy path complet
|
||||
|
||||
### 7.1 DEV (Mac)
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
### 7.2 Push + PR
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: my change"
|
||||
git push -u origin chore/my-change-YYYYMMDD
|
||||
```
|
||||
|
||||
Puis ouvrir la PR dans Gitea.
|
||||
|
||||
### 7.3 Déploiement NAS
|
||||
|
||||
Voir :
|
||||
|
||||
```text
|
||||
docs/runbooks/DEPLOY-BLUE-GREEN.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8) Localhost auto-sync — ce qu’il faut retenir
|
||||
|
||||
Le localhost automatique sert à voir **la vérité de `main`**, pas à développer du neuf.
|
||||
|
||||
### 8.1 Scripts principaux
|
||||
|
||||
#### Script de sync
|
||||
|
||||
```text
|
||||
~/ops-local/archicratie/auto-sync-localhost.sh
|
||||
```
|
||||
|
||||
Rôle :
|
||||
|
||||
- fetch `origin/main`
|
||||
- réaligner le worktree localhost
|
||||
- lancer `npm ci` si besoin
|
||||
- redéclencher l’agent Astro si nécessaire
|
||||
|
||||
#### Script Astro
|
||||
|
||||
```text
|
||||
~/ops-local/archicratie/run-astro-localhost.sh
|
||||
```
|
||||
|
||||
Rôle :
|
||||
|
||||
- lancer `astro dev`
|
||||
- depuis le bon worktree
|
||||
- avec le bon runtime Node
|
||||
- sur `127.0.0.1:4321`
|
||||
|
||||
> Oui : ce script est nécessaire.
|
||||
> Il isole proprement le lancement du serveur Astro dans un contexte `launchd` stable.
|
||||
|
||||
### 8.2 LaunchAgents
|
||||
|
||||
#### Agent sync
|
||||
|
||||
```text
|
||||
~/Library/LaunchAgents/me.archicratie.localhost-sync.plist
|
||||
```
|
||||
|
||||
#### Agent Astro
|
||||
|
||||
```text
|
||||
~/Library/LaunchAgents/me.archicratie.localhost-astro.plist
|
||||
```
|
||||
|
||||
### 8.3 Document de référence
|
||||
|
||||
Pour tout le détail d’exploitation du localhost automatique, lire :
|
||||
|
||||
```text
|
||||
docs/OPS-LOCALHOST-AUTO-SYNC.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9) Règle d’or : il y a deux usages locaux distincts
|
||||
|
||||
### 9.1 Voir ce qui est réellement sur `main`
|
||||
|
||||
Utiliser :
|
||||
|
||||
```text
|
||||
http://127.0.0.1:4321
|
||||
```
|
||||
|
||||
Ce localhost doit être considéré comme :
|
||||
|
||||
**un miroir local exécutable de `origin/main`**
|
||||
|
||||
### 9.2 Développer / tester une nouvelle fonctionnalité
|
||||
|
||||
Utiliser le repo canonique :
|
||||
|
||||
```bash
|
||||
cd /Volumes/FunIA/dev/archicratie-edition/site
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Donc :
|
||||
|
||||
- **localhost auto-sync** = vérité de `main`
|
||||
- **localhost de dev manuel** = expérimentation en cours
|
||||
|
||||
Il ne faut pas les confondre.
|
||||
|
||||
---
|
||||
|
||||
## 10) Ce qu’il ne faut pas faire
|
||||
|
||||
### 10.1 Ne pas développer dans le worktree localhost
|
||||
|
||||
Le worktree localhost est piloté automatiquement.
|
||||
|
||||
Il peut être :
|
||||
|
||||
- réaligné
|
||||
- nettoyé
|
||||
- redémarré
|
||||
|
||||
Donc :
|
||||
|
||||
- pas de commits dedans
|
||||
- pas de dev feature dedans
|
||||
- pas d’expérimentation de fond dedans
|
||||
|
||||
### 10.2 Ne pas utiliser le repo canonique comme miroir auto-sync
|
||||
|
||||
Sinon on mélange :
|
||||
|
||||
- espace de dev
|
||||
- état publié
|
||||
- serveur local permanent
|
||||
|
||||
### 10.3 Ne pas remettre les scripts ops sur un volume externe
|
||||
|
||||
Les scripts d’ops doivent rester sous `HOME`.
|
||||
|
||||
Le fait de les mettre sous `/Volumes/...` a déjà provoqué des erreurs du type :
|
||||
|
||||
```text
|
||||
Operation not permitted
|
||||
```
|
||||
|
||||
### 10.4 Ne pas supprimer `run-astro-localhost.sh`
|
||||
|
||||
Ce script fait partie de l’architecture actuelle.
|
||||
Le supprimer reviendrait à réintroduire le flou entre sync Git et exécution d’Astro.
|
||||
|
||||
---
|
||||
|
||||
## 11) Commandes de contrôle essentielles
|
||||
|
||||
### 11.1 État global
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/doctor-localhost.sh
|
||||
```
|
||||
|
||||
### 11.2 État Git
|
||||
|
||||
```bash
|
||||
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
|
||||
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
|
||||
git -C ~/ops-local/archicratie/localhost-worktree branch --show-current
|
||||
```
|
||||
|
||||
### 11.3 État LaunchAgents
|
||||
|
||||
```bash
|
||||
launchctl print "gui/$(id -u)/me.archicratie.localhost-sync" | sed -n '1,160p'
|
||||
launchctl print "gui/$(id -u)/me.archicratie.localhost-astro" | sed -n '1,160p'
|
||||
```
|
||||
|
||||
### 11.4 État logs
|
||||
|
||||
```bash
|
||||
tail -n 120 ~/ops-local/archicratie/logs/auto-sync-localhost.log
|
||||
tail -n 120 ~/ops-local/archicratie/logs/astro-localhost.log
|
||||
tail -n 80 ~/Library/Logs/archicratie-localhost-sync.err.log
|
||||
tail -n 80 ~/Library/Logs/archicratie-localhost-astro.err.log
|
||||
```
|
||||
|
||||
### 11.5 État serveur
|
||||
|
||||
```bash
|
||||
lsof -nP -iTCP:4321 -sTCP:LISTEN
|
||||
PID="$(lsof -tiTCP:4321 -sTCP:LISTEN | head -n 1)"
|
||||
ps -p "$PID" -o pid=,command=
|
||||
lsof -a -p "$PID" -d cwd
|
||||
```
|
||||
|
||||
### 11.6 Vérification contenu
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:4321/archicrat-ia/prologue/ | grep -n "taxe Zucman"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12) Problèmes classiques + diagnostic
|
||||
|
||||
### 12.1 “Le staging ne ressemble pas au local”
|
||||
|
||||
Comparer les upstream directs :
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:8081/ | head -n 2
|
||||
curl -sS http://127.0.0.1:8082/ | head -n 2
|
||||
```
|
||||
|
||||
Vérifier le routeur edge :
|
||||
|
||||
```bash
|
||||
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
|
||||
| grep -iE 'HTTP/|location:|x-archi-router'
|
||||
```
|
||||
|
||||
Voir :
|
||||
|
||||
```text
|
||||
docs/runbooks/EDGE-TRAEFIK.md
|
||||
```
|
||||
|
||||
### 12.2 Canonical incorrect
|
||||
|
||||
Cause probable : `PUBLIC_SITE` mal injecté au build.
|
||||
|
||||
Test :
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -1
|
||||
```
|
||||
|
||||
Voir :
|
||||
|
||||
```text
|
||||
docs/runbooks/ENV-PUBLIC_SITE.md
|
||||
```
|
||||
|
||||
### 12.3 Contrat anchors en échec après migration d’URL
|
||||
|
||||
Procédure safe :
|
||||
|
||||
```bash
|
||||
cp -a tests/anchors-baseline.json /tmp/anchors-baseline.json.bak.$(date +%F-%H%M%S)
|
||||
|
||||
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
|
||||
|
||||
npm run test:anchors
|
||||
```
|
||||
|
||||
### 12.4 “Le localhost auto-sync ne montre pas les dernières modifs”
|
||||
|
||||
Commande réflexe :
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/doctor-localhost.sh
|
||||
```
|
||||
|
||||
Puis :
|
||||
|
||||
```bash
|
||||
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
|
||||
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
|
||||
```
|
||||
|
||||
Si les SHA diffèrent :
|
||||
- le sync n’a pas tourné
|
||||
- ou l’agent sync a un problème
|
||||
|
||||
### 12.5 “Le SHA est bon mais le contenu web est faux”
|
||||
|
||||
Vérifier quel Astro écoute réellement :
|
||||
|
||||
```bash
|
||||
lsof -nP -iTCP:4321 -sTCP:LISTEN
|
||||
PID="$(lsof -tiTCP:4321 -sTCP:LISTEN | head -n 1)"
|
||||
ps -p "$PID" -o pid=,command=
|
||||
lsof -a -p "$PID" -d cwd
|
||||
```
|
||||
|
||||
Attendu :
|
||||
- commande contenant `astro dev`
|
||||
- cwd = `~/ops-local/archicratie/localhost-worktree`
|
||||
|
||||
### 12.6 Erreur `EBADENGINE`
|
||||
|
||||
Cause probable :
|
||||
- Node 23 utilisé au lieu de Node 22
|
||||
|
||||
Résolution :
|
||||
- forcer `node@22` dans les scripts et les LaunchAgents
|
||||
|
||||
### 12.7 Erreur `Operation not permitted`
|
||||
|
||||
Cause probable :
|
||||
- scripts d’ops placés sous `/Volumes/...`
|
||||
|
||||
Résolution :
|
||||
- garder les scripts sous :
|
||||
|
||||
```text
|
||||
~/ops-local/archicratie
|
||||
```
|
||||
|
||||
### 12.8 Erreur `EPERM` sur `astro.mjs`
|
||||
|
||||
Cause probable :
|
||||
- ancien worktree sur volume externe
|
||||
- ancien chemin résiduel
|
||||
- Astro lancé depuis un mauvais emplacement
|
||||
|
||||
Résolution :
|
||||
- worktree localhost sous :
|
||||
|
||||
```text
|
||||
~/ops-local/archicratie/localhost-worktree
|
||||
```
|
||||
|
||||
- scripts cohérents avec ce chemin
|
||||
- réinstallation propre via :
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/install-localhost-sync.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13) Redémarrage machine
|
||||
|
||||
Après reboot, le comportement attendu est :
|
||||
|
||||
1. le LaunchAgent sync se recharge
|
||||
2. le LaunchAgent Astro se recharge
|
||||
3. le worktree localhost est réaligné
|
||||
4. Astro redémarre sur `127.0.0.1:4321`
|
||||
|
||||
### Vérification rapide après reboot
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/doctor-localhost.sh
|
||||
```
|
||||
|
||||
Si nécessaire :
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/install-localhost-sync.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14) Procédure de secours manuelle
|
||||
|
||||
### Forcer un sync
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/auto-sync-localhost.sh
|
||||
```
|
||||
|
||||
### Réinstaller proprement le dispositif local
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/install-localhost-sync.sh
|
||||
```
|
||||
|
||||
### Diagnostic complet
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/doctor-localhost.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15) Décision d’exploitation finale
|
||||
|
||||
La politique retenue est la suivante :
|
||||
|
||||
- **repo canonique** = espace de développement
|
||||
- **worktree localhost** = miroir automatique de `main`
|
||||
- **ops sous HOME** = scripts, logs, automation
|
||||
- **LaunchAgent sync** = réalignement Git
|
||||
- **LaunchAgent Astro** = exécution stable du serveur local
|
||||
- **Astro local** = lancé uniquement depuis le worktree localhost
|
||||
|
||||
Cette séparation rend le dispositif plus :
|
||||
|
||||
- lisible
|
||||
- robuste
|
||||
- opérable
|
||||
- antifragile
|
||||
|
||||
---
|
||||
|
||||
## 16) Résumé opératoire
|
||||
|
||||
### Pour voir la vérité de `main`
|
||||
|
||||
Ouvrir :
|
||||
|
||||
```text
|
||||
http://127.0.0.1:4321
|
||||
```
|
||||
|
||||
Le serveur doit provenir de :
|
||||
|
||||
```text
|
||||
/Users/s-funia/ops-local/archicratie/localhost-worktree
|
||||
```
|
||||
|
||||
### Pour développer
|
||||
|
||||
Travailler dans :
|
||||
|
||||
```text
|
||||
/Volumes/FunIA/dev/archicratie-edition/site
|
||||
```
|
||||
|
||||
avec les commandes habituelles.
|
||||
|
||||
### Pour réparer vite
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/doctor-localhost.sh
|
||||
~/ops-local/archicratie/auto-sync-localhost.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17) Mémoire courte
|
||||
|
||||
Si un jour plus rien n’est clair, repartir de ces commandes :
|
||||
|
||||
```bash
|
||||
~/ops-local/archicratie/doctor-localhost.sh
|
||||
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
|
||||
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
|
||||
lsof -nP -iTCP:4321 -sTCP:LISTEN
|
||||
```
|
||||
|
||||
Puis lire :
|
||||
|
||||
```bash
|
||||
tail -n 120 ~/ops-local/archicratie/logs/auto-sync-localhost.log
|
||||
tail -n 120 ~/ops-local/archicratie/logs/astro-localhost.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18) Statut actuel visé
|
||||
|
||||
Quand tout fonctionne correctement :
|
||||
|
||||
- le worktree localhost pointe sur le même SHA que `origin/main`
|
||||
- `astro dev` écoute sur `127.0.0.1:4321`
|
||||
- son cwd est `~/ops-local/archicratie/localhost-worktree`
|
||||
- le contenu servi correspond au contenu mergé sur `main`
|
||||
|
||||
C’est l’état de référence à préserver.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/diagrams/out/archicratie-web-edition-git-ci-workflow-v1.png
Normal file
BIN
docs/diagrams/out/archicratie-web-edition-git-ci-workflow-v1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 395 KiB |
BIN
docs/diagrams/out/archicratie-web-edition-global-verbatim-v2.png
Normal file
BIN
docs/diagrams/out/archicratie-web-edition-global-verbatim-v2.png
Normal file
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: 360 KiB |
546
docs/runbooks/DEPLOY-BLUE-GREEN.md
Normal file
546
docs/runbooks/DEPLOY-BLUE-GREEN.md
Normal 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
docs/runbooks/EDGE-TRAEFIK.md
Normal file
147
docs/runbooks/EDGE-TRAEFIK.md
Normal 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
docs/runbooks/ENV-PUBLIC_SITE.md
Normal file
114
docs/runbooks/ENV-PUBLIC_SITE.md
Normal 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.
|
||||
1330
package-lock.json
generated
1330
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -4,35 +4,33 @@
|
||||
"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",
|
||||
|
||||
"build:search": "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 && npm run build:search",
|
||||
"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",
|
||||
|
||||
@@ -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
scripts/apply-annotation-ticket.mjs
Normal file
899
scripts/apply-annotation-ticket.mjs
Normal 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);
|
||||
});
|
||||
@@ -9,8 +9,9 @@ import { spawnSync } from "node:child_process";
|
||||
*
|
||||
* Conçu pour:
|
||||
* - prendre un ticket [Correction]/[Fact-check] (issue) avec Chemin + Ancre + Proposition
|
||||
* - retrouver le bon paragraphe dans le .mdx
|
||||
* - retrouver le bon paragraphe dans le .mdx/.md
|
||||
* - remplacer proprement
|
||||
* - ne JAMAIS toucher au frontmatter
|
||||
* - optionnel: écrire un alias d’ancre old->new (build-time) dans src/anchors/anchor-aliases.json
|
||||
* - optionnel: committer automatiquement
|
||||
* - optionnel: fermer le ticket (après commit)
|
||||
@@ -39,7 +40,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 +90,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 -------------------------- */
|
||||
|
||||
@@ -136,31 +138,26 @@ function scoreText(candidate, targetText) {
|
||||
let hit = 0;
|
||||
for (const w of tgtSet) if (blkSet.has(w)) hit++;
|
||||
|
||||
// Bonus si un long préfixe ressemble
|
||||
const tgtNorm = normalizeText(stripMd(targetText));
|
||||
const blkNorm = normalizeText(stripMd(candidate));
|
||||
const prefix = tgtNorm.slice(0, Math.min(180, tgtNorm.length));
|
||||
const prefixBonus = prefix && blkNorm.includes(prefix) ? 1000 : 0;
|
||||
|
||||
// Ratio bonus (0..100)
|
||||
const ratio = hit / Math.max(1, tgtSet.size);
|
||||
const ratioBonus = Math.round(ratio * 100);
|
||||
|
||||
return prefixBonus + hit + ratioBonus;
|
||||
}
|
||||
|
||||
function bestBlockMatchIndex(blocks, targetText) {
|
||||
let best = { i: -1, score: -1 };
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const sc = scoreText(blocks[i], targetText);
|
||||
if (sc > best.score) best = { i, score: sc };
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function splitParagraphBlocks(mdxText) {
|
||||
const raw = String(mdxText ?? "").replace(/\r\n/g, "\n");
|
||||
return raw.split(/\n{2,}/);
|
||||
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 isLikelyExcerpt(s) {
|
||||
@@ -172,6 +169,89 @@ function isLikelyExcerpt(s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* --------------------------- frontmatter / structure ------------------------ */
|
||||
|
||||
function normalizeNewlines(s) {
|
||||
return String(s ?? "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
function splitMdxFrontmatter(src) {
|
||||
const text = normalizeNewlines(src);
|
||||
const m = text.match(/^---\n[\s\S]*?\n---\n?/);
|
||||
|
||||
if (!m) {
|
||||
return {
|
||||
hasFrontmatter: false,
|
||||
frontmatter: "",
|
||||
body: text,
|
||||
};
|
||||
}
|
||||
|
||||
const frontmatter = m[0];
|
||||
const body = text.slice(frontmatter.length);
|
||||
|
||||
return {
|
||||
hasFrontmatter: true,
|
||||
frontmatter,
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
function joinMdxFrontmatter(frontmatter, body) {
|
||||
if (!frontmatter) return String(body ?? "");
|
||||
return String(frontmatter) + String(body ?? "");
|
||||
}
|
||||
|
||||
function assertFrontmatterIntegrity({ hadFrontmatter, originalFrontmatter, finalText, filePath }) {
|
||||
if (!hadFrontmatter) return;
|
||||
|
||||
const text = normalizeNewlines(finalText);
|
||||
|
||||
if (!text.startsWith("---\n")) {
|
||||
throw new Error(`Frontmatter perdu pendant la mise à jour de ${filePath}`);
|
||||
}
|
||||
|
||||
if (!text.startsWith(originalFrontmatter)) {
|
||||
throw new Error(`Frontmatter altéré pendant la mise à jour de ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function splitParagraphBlocksPreserve(bodyText) {
|
||||
const text = normalizeNewlines(bodyText);
|
||||
|
||||
if (!text) {
|
||||
return { blocks: [], separators: [] };
|
||||
}
|
||||
|
||||
const blocks = [];
|
||||
const separators = [];
|
||||
|
||||
const re = /(\n{2,})/g;
|
||||
let last = 0;
|
||||
let m;
|
||||
|
||||
while ((m = re.exec(text))) {
|
||||
blocks.push(text.slice(last, m.index));
|
||||
separators.push(m[1]);
|
||||
last = m.index + m[1].length;
|
||||
}
|
||||
|
||||
blocks.push(text.slice(last));
|
||||
|
||||
return { blocks, separators };
|
||||
}
|
||||
|
||||
function joinParagraphBlocksPreserve(blocks, separators) {
|
||||
if (!Array.isArray(blocks) || blocks.length === 0) return "";
|
||||
|
||||
let out = "";
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
out += blocks[i];
|
||||
if (i < separators.length) out += separators[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ------------------------------ utils système ------------------------------ */
|
||||
|
||||
function run(cmd, args, opts = {}) {
|
||||
@@ -251,7 +331,9 @@ function pickSection(body, 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);
|
||||
|
||||
@@ -266,11 +348,13 @@ function pickSection(body, markers) {
|
||||
"\n## Proposition",
|
||||
"\n## Problème",
|
||||
];
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -298,8 +382,6 @@ function extractAnchorIdAnywhere(text) {
|
||||
|
||||
function extractCheminFromAnyUrl(text) {
|
||||
const s = String(text || "");
|
||||
// Exemple: http://localhost:4321/archicratie/prologue/#p-3-xxxx
|
||||
// ou: /archicratie/prologue/#p-3-xxxx
|
||||
const m = s.match(/(\/[a-z0-9\-]+\/[a-z0-9\-\/]+\/)#p-\d+-[0-9a-f]{8}/i);
|
||||
return m ? m[1] : "";
|
||||
}
|
||||
@@ -400,7 +482,7 @@ async function fetchIssue({ forgeApiBase, owner, repo, token, issueNum }) {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
Accept: "application/json",
|
||||
"User-Agent": "archicratie-apply-ticket/2.0",
|
||||
"User-Agent": "archicratie-apply-ticket/2.1",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -416,7 +498,7 @@ async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment
|
||||
Authorization: `token ${token}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "archicratie-apply-ticket/2.0",
|
||||
"User-Agent": "archicratie-apply-ticket/2.1",
|
||||
};
|
||||
|
||||
if (comment) {
|
||||
@@ -425,7 +507,11 @@ async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment
|
||||
}
|
||||
|
||||
const url = `${base}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
|
||||
const res = await fetch(url, { method: "PATCH", headers, body: JSON.stringify({ state: "closed" }) });
|
||||
const res = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify({ state: "closed" }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const t = await res.text().catch(() => "");
|
||||
@@ -529,10 +615,9 @@ async function main() {
|
||||
console.log(`🔎 Fetch ticket #${issueNum} from ${owner}/${repo} …`);
|
||||
const issue = await fetchIssue({ forgeApiBase, owner, repo, token, issueNum });
|
||||
|
||||
// Guard PR (Pull Request = "Demande d'ajout" = pas un ticket éditorial)
|
||||
if (issue?.pull_request) {
|
||||
console.error(`❌ #${issueNum} est une Pull Request (demande d’ajout), pas un ticket éditorial.`);
|
||||
console.error(`➡️ Ouvre un ticket [Correction]/[Fact-check] depuis le site (Proposer), puis relance apply-ticket sur ce numéro.`);
|
||||
console.error("➡️ Ouvre un ticket [Correction]/[Fact-check] depuis le site (Proposer), puis relance apply-ticket sur ce numéro.");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
@@ -553,7 +638,6 @@ async function main() {
|
||||
ancre = (ancre || "").trim();
|
||||
if (ancre.startsWith("#")) ancre = ancre.slice(1);
|
||||
|
||||
// fallback si ticket mal formé
|
||||
if (!ancre) ancre = extractAnchorIdAnywhere(title) || extractAnchorIdAnywhere(body);
|
||||
|
||||
chemin = normalizeChemin(chemin);
|
||||
@@ -592,7 +676,6 @@ async function main() {
|
||||
const distHtmlPath = path.join(DIST_ROOT, chemin.replace(/^\/+|\/+$/g, ""), "index.html");
|
||||
await ensureBuildIfNeeded(distHtmlPath);
|
||||
|
||||
// Texte cible: préférence au texte complet (ticket), sinon dist si extrait probable
|
||||
let targetText = texteActuel;
|
||||
let distText = "";
|
||||
|
||||
@@ -609,21 +692,24 @@ async function main() {
|
||||
throw new Error("Impossible de reconstruire le texte du paragraphe (ni texte actuel, ni dist html).");
|
||||
}
|
||||
|
||||
const original = await fs.readFile(contentFile, "utf-8");
|
||||
const blocks = splitParagraphBlocks(original);
|
||||
const originalRaw = await fs.readFile(contentFile, "utf-8");
|
||||
const { hasFrontmatter, frontmatter, body: originalBody } = splitMdxFrontmatter(originalRaw);
|
||||
|
||||
const best = bestBlockMatchIndex(blocks, targetText);
|
||||
const split = splitParagraphBlocksPreserve(originalBody);
|
||||
const blocks = split.blocks;
|
||||
const separators = split.separators;
|
||||
|
||||
if (!blocks.length) {
|
||||
throw new Error(`Aucun bloc éditorial exploitable dans ${path.relative(CWD, contentFile)}`);
|
||||
}
|
||||
|
||||
const ranked = rankedBlockMatches(blocks, targetText, 5);
|
||||
const best = ranked[0] || { i: -1, score: -1, excerpt: "" };
|
||||
const runnerUp = ranked[1] || null;
|
||||
|
||||
// seuil de sécurité
|
||||
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,12 +717,34 @@ async function main() {
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
const nextBlocks = blocks.slice();
|
||||
nextBlocks[best.i] = afterBlock;
|
||||
const updated = nextBlocks.join("\n\n");
|
||||
|
||||
const updatedBody = joinParagraphBlocksPreserve(nextBlocks, separators);
|
||||
const updatedRaw = joinMdxFrontmatter(frontmatter, updatedBody);
|
||||
|
||||
assertFrontmatterIntegrity({
|
||||
hadFrontmatter: hasFrontmatter,
|
||||
originalFrontmatter: frontmatter,
|
||||
finalText: updatedRaw,
|
||||
filePath: path.relative(CWD, contentFile),
|
||||
});
|
||||
|
||||
console.log(`🧩 Matched block #${best.i + 1}/${blocks.length} score=${best.score}`);
|
||||
|
||||
@@ -650,13 +758,15 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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");
|
||||
await fs.writeFile(bakPath, originalRaw, "utf-8");
|
||||
}
|
||||
|
||||
await fs.writeFile(contentFile, updated, "utf-8");
|
||||
await fs.writeFile(contentFile, updatedRaw, "utf-8");
|
||||
console.log("✅ Applied.");
|
||||
|
||||
let aliasChanged = false;
|
||||
@@ -677,13 +787,13 @@ async function main() {
|
||||
|
||||
if (aliasChanged) {
|
||||
console.log(`✅ Alias ajouté: ${chemin} ${ancre} -> ${newId}`);
|
||||
// MàJ dist sans rebuild complet (inject seulement)
|
||||
run("node", ["scripts/inject-anchor-aliases.mjs"], { cwd: CWD });
|
||||
} else {
|
||||
console.log(`ℹ️ Alias déjà présent ou inutile (${ancre} -> ${newId}).`);
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
@@ -713,7 +823,6 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// mode manuel
|
||||
console.log("Next (manuel) :");
|
||||
console.log(` git diff -- ${path.relative(CWD, contentFile)}`);
|
||||
console.log(
|
||||
@@ -730,4 +839,4 @@ async function main() {
|
||||
main().catch((e) => {
|
||||
console.error("💥", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
246
scripts/build-annotations-index.mjs
Normal file
246
scripts/build-annotations-index.mjs
Normal 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);
|
||||
});
|
||||
@@ -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
scripts/check-annotations-media.mjs
Normal file
104
scripts/check-annotations-media.mjs
Normal 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);
|
||||
});
|
||||
@@ -27,11 +27,6 @@ function escRe(s) {
|
||||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function inferPageKeyFromFile(fileAbs) {
|
||||
const rel = path.relative(ANNO_DIR, fileAbs).replace(/\\/g, "/");
|
||||
return rel.replace(/\.(ya?ml|json)$/i, "");
|
||||
}
|
||||
|
||||
function normalizePageKey(s) {
|
||||
return String(s || "").replace(/^\/+/, "").replace(/\/+$/, "");
|
||||
}
|
||||
@@ -40,6 +35,31 @@ 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 {
|
||||
@@ -60,10 +80,12 @@ function getAlias(aliases, pageKey, oldId) {
|
||||
// supporte:
|
||||
// 1) { "<pageKey>": { "<old>": "<new>" } }
|
||||
// 2) { "<old>": "<new>" }
|
||||
const a1 = aliases?.[pageKey]?.[oldId];
|
||||
if (a1) return a1;
|
||||
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 a2;
|
||||
if (a2) return String(a2);
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -81,7 +103,11 @@ async function main() {
|
||||
const aliases = await loadAliases();
|
||||
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
|
||||
|
||||
let pages = 0;
|
||||
// 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 = [];
|
||||
@@ -105,7 +131,7 @@ async function main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pageKey = normalizePageKey(inferPageKeyFromFile(f));
|
||||
const { pageKey, paraId: shardParaId } = inferFromFile(f);
|
||||
|
||||
if (doc.page != null && normalizePageKey(doc.page) !== pageKey) {
|
||||
failures++;
|
||||
@@ -119,20 +145,44 @@ async function main() {
|
||||
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))) {
|
||||
failures++;
|
||||
notes.push(`- MISSING PAGE: dist/${pageKey}/index.html (from ${rel})`);
|
||||
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;
|
||||
}
|
||||
|
||||
pages++;
|
||||
const html = await fs.readFile(distFile, "utf8");
|
||||
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 (!/^p-\d+-/i.test(paraId)) {
|
||||
if (!isParaId(paraId)) {
|
||||
failures++;
|
||||
notes.push(`- INVALID ID: ${rel} (${paraId})`);
|
||||
continue;
|
||||
@@ -156,6 +206,7 @@ async function main() {
|
||||
}
|
||||
|
||||
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})`);
|
||||
@@ -170,4 +221,4 @@ async function main() {
|
||||
main().catch((e) => {
|
||||
console.error("FAIL: annotations check crashed:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
@@ -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,35 @@ 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 };
|
||||
// Compat legacy :
|
||||
// manifest collection="archicratie" + slug="archicrat-ia/..."
|
||||
// => on écrit bien dans src/content/archicrat-ia/...
|
||||
// => mais on conserve edition/status historiques de type archicratie/modele_sociopolitique
|
||||
const defaultsKey =
|
||||
String(it.collection || "").trim() === "archicratie" &&
|
||||
String(it.slug || "").trim().startsWith("archicrat-ia/")
|
||||
? "archicratie"
|
||||
: outCollection;
|
||||
|
||||
const defaults =
|
||||
schemaDefaultsByCollection[defaultsKey] || {
|
||||
edition: defaultsKey,
|
||||
status: "draft",
|
||||
level: 1,
|
||||
};
|
||||
|
||||
const fm = [
|
||||
"---",
|
||||
@@ -282,4 +320,4 @@ async function main() {
|
||||
main().catch((e) => {
|
||||
console.error("\nERROR:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
@@ -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
scripts/pick-proposer-issue.mjs
Normal file
241
scripts/pick-proposer-issue.mjs
Normal 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
scripts/purge-dist-dev-whoami.mjs
Normal file
31
scripts/purge-dist-dev-whoami.mjs
Normal 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);
|
||||
});
|
||||
131
scripts/switch-archicratie.sh
Executable file
131
scripts/switch-archicratie.sh
Executable 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"
|
||||
@@ -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
scripts/write-dev-whoami.mjs
Normal file
26
scripts/write-dev-whoami.mjs
Normal 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})`);
|
||||
20
scripts/write-ops-health.mjs
Normal file
20
scripts/write-ops-health.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = process.cwd();
|
||||
const outDir = path.join(root, "public", "__ops");
|
||||
const outFile = path.join(outDir, "health.json");
|
||||
|
||||
const payload = {
|
||||
service: "archicratie-site",
|
||||
env: process.env.PUBLIC_OPS_ENV || "unknown",
|
||||
upstream: process.env.PUBLIC_OPS_UPSTREAM || "unknown",
|
||||
buildSha: process.env.PUBLIC_BUILD_SHA || "unknown",
|
||||
builtAt: process.env.PUBLIC_BUILD_TIME || new Date().toISOString(),
|
||||
};
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
|
||||
console.log(`✅ ops health written: ${outFile}`);
|
||||
console.log(payload);
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
sources/docx/commencer/document-de-presentation.docx
Normal file
BIN
sources/docx/commencer/document-de-presentation.docx
Normal file
Binary file not shown.
@@ -1,13 +1,22 @@
|
||||
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"
|
||||
# =========================
|
||||
- source: sources/docx/archicrat-ia/Prologue—Archicratie-fondation_et_finalite_sociopolitique_et_historique-version_officielle.docx
|
||||
collection: archicratie
|
||||
slug: archicrat-ia/prologue
|
||||
title: "Prologue — Fondation et finalité sociopolitique et historique"
|
||||
title: "Prologue — Fondation, finalité sociopolitique et historique"
|
||||
order: 10
|
||||
|
||||
- source: sources/docx/archicrat-ia/Chapitre_1—Fondements_epistemologiques_et_modelisation_Archicratie-version_officielle.docx
|
||||
@@ -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,2 +1 @@
|
||||
{}
|
||||
|
||||
0
src/annotations/.gitkeep
Normal file
0
src/annotations/.gitkeep
Normal file
@@ -1,59 +0,0 @@
|
||||
schema: 1
|
||||
|
||||
# optionnel (si présent, doit matcher le chemin du fichier)
|
||||
page: archicrat-ia/prologue
|
||||
|
||||
paras:
|
||||
p-0-d7974f88:
|
||||
refs:
|
||||
- label: "Happycratie — (Cabanas & Illouz) via Cairn"
|
||||
url: "https://shs.cairn.info/revue-ethnologie-francaise-2019-4-page-813?lang=fr"
|
||||
kind: "article"
|
||||
- label: "Techno-féodalisme — Variations (OpenEdition)"
|
||||
url: "https://journals.openedition.org/variations/2290"
|
||||
kind: "article"
|
||||
|
||||
authors:
|
||||
- "Eva Illouz"
|
||||
- "Yanis Varoufakis"
|
||||
|
||||
quotes:
|
||||
- text: "Dans Happycratie, Edgar Cabanas et Eva Illouz..."
|
||||
source: "Happycratie, p.1"
|
||||
- text: "En eux-mêmes, les actifs ne sont ni féodaux ni capitalistes..."
|
||||
source: "Entretien Morozov/Varoufakis — techno-féodalisme"
|
||||
|
||||
media:
|
||||
- type: "image"
|
||||
src: "/public/media/archicrat-ia/prologue/p-0-d7974f88/schema-1.svg"
|
||||
caption: "Tableau explicatif"
|
||||
credit: "ChatGPT"
|
||||
- type: "image"
|
||||
src: "/public/media/archicrat-ia/prologue/p-0-d7974f88/schema-2.svg"
|
||||
caption: "Diagramme d’évolution"
|
||||
credit: "Yanis Varoufakis"
|
||||
|
||||
comments_editorial:
|
||||
- text: "TODO: nuancer / préciser — commentaire éditorial versionné (pas public)."
|
||||
status: "draft"
|
||||
|
||||
p-1-2ef25f29:
|
||||
refs:
|
||||
- label: "Kafka et le pouvoir — Bernard Lahire (Cairn)"
|
||||
url: "https://shs.cairn.info/franz-kafka--9782707159410-page-475?lang=fr"
|
||||
kind: "book"
|
||||
|
||||
authors:
|
||||
- "Bernard Lahire"
|
||||
|
||||
quotes:
|
||||
- text: "Si l’on voulait chercher quelque chose comme une vision du monde chez Kafka..."
|
||||
source: "Bernard Lahire, Franz Kafka, p.475+"
|
||||
|
||||
media:
|
||||
- type: "video"
|
||||
src: "/public/media/archicrat-ia/prologue/p-1-2ef25f29/bien_commun.mp4"
|
||||
caption: "Entretien avec Bernard Lahire"
|
||||
credit: "Cairn.info"
|
||||
|
||||
comments_editorial: []
|
||||
@@ -1,29 +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)}/`;
|
||||
|
||||
// ✅ On route l’Essai-thèse sur /archicrat-ia/<slug-sans-prefix>/
|
||||
// (Astro trailingSlash = always → on garde le "/" final)
|
||||
const strip = (s) => String(s || "").replace(/^archicrat-ia\//, "");
|
||||
const href = (slug) => `/archicrat-ia/${strip(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>
|
||||
@@ -163,4 +176,4 @@ const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
|
||||
const active = document.querySelector(".toc-global .toc-item.is-active");
|
||||
if (active) active.scrollIntoView({ block: "nearest" });
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
237
src/components/GlossaryAside.astro
Normal file
237
src/components/GlossaryAside.astro
Normal file
@@ -0,0 +1,237 @@
|
||||
---
|
||||
import {
|
||||
getGlossaryEntryAsideData,
|
||||
getGlossaryPortalLinks,
|
||||
hrefOfGlossaryEntry,
|
||||
slugOfGlossaryEntry,
|
||||
} from "../lib/glossary";
|
||||
|
||||
const {
|
||||
currentEntry,
|
||||
allEntries = [],
|
||||
} = Astro.props;
|
||||
|
||||
const currentSlug = slugOfGlossaryEntry(currentEntry);
|
||||
|
||||
const {
|
||||
displayFamily,
|
||||
displayDomain,
|
||||
displayLevel,
|
||||
showNoyau,
|
||||
showSameFamily,
|
||||
fondamentaux,
|
||||
sameFamilyTitle,
|
||||
sameFamilyEntries,
|
||||
relationSections,
|
||||
contextualTheory,
|
||||
} = getGlossaryEntryAsideData(currentEntry, allEntries);
|
||||
|
||||
const portalLinks = getGlossaryPortalLinks();
|
||||
---
|
||||
|
||||
<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__pills" aria-label="Repères de lecture">
|
||||
<span class="glossary-aside__pill glossary-aside__pill--family">
|
||||
{displayFamily}
|
||||
</span>
|
||||
|
||||
{displayDomain && (
|
||||
<span class="glossary-aside__pill">{displayDomain}</span>
|
||||
)}
|
||||
|
||||
{displayLevel && (
|
||||
<span class="glossary-aside__pill">{displayLevel}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="glossary-aside__block">
|
||||
<h2 class="glossary-aside__heading">Portails</h2>
|
||||
<ul class="glossary-aside__list">
|
||||
{portalLinks.map((item) => (
|
||||
<li><a href={item.href}>{item.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{showNoyau && (
|
||||
<section class="glossary-aside__block">
|
||||
<h2 class="glossary-aside__heading">Noyau archicratique</h2>
|
||||
<ul class="glossary-aside__list">
|
||||
{fondamentaux.map((entry) => {
|
||||
const active = slugOfGlossaryEntry(entry) === currentSlug;
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={hrefOfGlossaryEntry(entry)}
|
||||
aria-current={active ? "page" : undefined}
|
||||
class={active ? "is-active" : undefined}
|
||||
>
|
||||
{entry.data.term}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{showSameFamily && (
|
||||
<section class="glossary-aside__block">
|
||||
<h2 class="glossary-aside__heading">{sameFamilyTitle}</h2>
|
||||
<ul class="glossary-aside__list">
|
||||
{sameFamilyEntries.map((entry) => {
|
||||
const active = slugOfGlossaryEntry(entry) === currentSlug;
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={hrefOfGlossaryEntry(entry)}
|
||||
aria-current={active ? "page" : undefined}
|
||||
class={active ? "is-active" : undefined}
|
||||
>
|
||||
{entry.data.term}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{relationSections.length > 0 && (
|
||||
<section class="glossary-aside__block">
|
||||
<h2 class="glossary-aside__heading">Autour de cette fiche</h2>
|
||||
|
||||
{relationSections.map((section) => (
|
||||
<>
|
||||
<h3 class="glossary-aside__subheading">{section.title}</h3>
|
||||
<ul class="glossary-aside__list">
|
||||
{section.items.map((entry) => (
|
||||
<li><a href={hrefOfGlossaryEntry(entry)}>{entry.data.term}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{contextualTheory.length > 0 && (
|
||||
<section class="glossary-aside__block">
|
||||
<h2 class="glossary-aside__heading">Paysage théorique</h2>
|
||||
<ul class="glossary-aside__list">
|
||||
{contextualTheory.map((entry) => (
|
||||
<li><a href={hrefOfGlossaryEntry(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: 14px;
|
||||
background: rgba(127,127,127,0.05);
|
||||
}
|
||||
|
||||
.glossary-aside__block--intro{
|
||||
padding-top: 13px;
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
|
||||
.glossary-aside__back{
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-aside__title{
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .2px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.glossary-aside__pills{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.glossary-aside__pill{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid rgba(127,127,127,0.24);
|
||||
border-radius: 999px;
|
||||
background: rgba(127,127,127,0.04);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.glossary-aside__pill--family{
|
||||
border-color: rgba(127,127,127,0.38);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.glossary-aside__heading{
|
||||
margin: 0 0 11px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
opacity: .94;
|
||||
}
|
||||
|
||||
.glossary-aside__subheading{
|
||||
margin: 13px 0 8px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
opacity: .82;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.glossary-aside__list{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.glossary-aside__list li{
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
.glossary-aside__list a{
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.glossary-aside__list a.is-active{
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-aside__block,
|
||||
.glossary-aside__pill{
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
83
src/components/GlossaryCardGrid.astro
Normal file
83
src/components/GlossaryCardGrid.astro
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
import { hrefOfGlossaryEntry, type GlossaryEntry } from "../lib/glossary";
|
||||
|
||||
export interface Props {
|
||||
entries?: GlossaryEntry[];
|
||||
wide?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
entries = [],
|
||||
wide = false,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div class="glossary-cards">
|
||||
{entries.map((entry) => (
|
||||
<a
|
||||
class:list={[
|
||||
"glossary-card",
|
||||
wide && "glossary-card--wide",
|
||||
]}
|
||||
href={hrefOfGlossaryEntry(entry)}
|
||||
>
|
||||
<strong>{entry.data.term}</strong>
|
||||
<span>{entry.data.definitionShort}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.glossary-cards{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.glossary-card{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--glossary-border);
|
||||
border-radius: 18px;
|
||||
background: var(--glossary-bg-soft);
|
||||
text-decoration: none;
|
||||
transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.glossary-card:hover{
|
||||
transform: translateY(-1px);
|
||||
background: var(--glossary-bg-soft-strong);
|
||||
border-color: rgba(0,217,255,0.16);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-card--wide{
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.glossary-card strong{
|
||||
color: var(--glossary-accent);
|
||||
font-size: 1.04rem;
|
||||
line-height: 1.28;
|
||||
}
|
||||
|
||||
.glossary-card span{
|
||||
color: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
opacity: .94;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-card{
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.glossary-card:hover{
|
||||
background: rgba(255,255,255,0.07);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
src/components/GlossaryEntryBody.astro
Normal file
16
src/components/GlossaryEntryBody.astro
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="glossary-entry-body">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.glossary-entry-body{
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
:global(.glossary-entry-body h2),
|
||||
:global(.glossary-entry-body h3),
|
||||
:global(.glossary-relations h2),
|
||||
:global(.glossary-relations h3){
|
||||
scroll-margin-top: calc(var(--sticky-offset-px, 96px) + 18px);
|
||||
}
|
||||
</style>
|
||||
230
src/components/GlossaryEntryHero.astro
Normal file
230
src/components/GlossaryEntryHero.astro
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
interface Props {
|
||||
term: string;
|
||||
definitionShort: string;
|
||||
displayFamily: string;
|
||||
displayDomain?: string;
|
||||
displayLevel?: string;
|
||||
mobilizedAuthors?: string[];
|
||||
comparisonTraditions?: string[];
|
||||
}
|
||||
|
||||
const {
|
||||
term,
|
||||
definitionShort,
|
||||
displayFamily,
|
||||
displayDomain = "",
|
||||
displayLevel = "",
|
||||
mobilizedAuthors = [],
|
||||
comparisonTraditions = [],
|
||||
} = Astro.props;
|
||||
|
||||
const hasScholarlyMeta =
|
||||
mobilizedAuthors.length > 0 ||
|
||||
comparisonTraditions.length > 0;
|
||||
---
|
||||
|
||||
<header class="glossary-entry-head" data-ge-hero>
|
||||
<div class="glossary-entry-head__title">
|
||||
<h1>{term}</h1>
|
||||
</div>
|
||||
|
||||
<div class="glossary-entry-summary">
|
||||
<p class="glossary-entry-dek">
|
||||
<em>{definitionShort}</em>
|
||||
</p>
|
||||
|
||||
<div class="glossary-entry-signals" aria-label="Repères de lecture">
|
||||
<span class="glossary-pill glossary-pill--family">
|
||||
<strong>Famille :</strong> {displayFamily}
|
||||
</span>
|
||||
|
||||
{displayDomain && (
|
||||
<span class="glossary-pill">
|
||||
<strong>Domaine :</strong> {displayDomain}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{displayLevel && (
|
||||
<span class="glossary-pill">
|
||||
<strong>Niveau :</strong> {displayLevel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasScholarlyMeta && (
|
||||
<div class="glossary-entry-meta">
|
||||
{mobilizedAuthors.length > 0 && (
|
||||
<p>
|
||||
<strong>Auteurs mobilisés :</strong> {mobilizedAuthors.join(" / ")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{comparisonTraditions.length > 0 && (
|
||||
<p>
|
||||
<strong>Traditions de comparaison :</strong> {comparisonTraditions.join(" / ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.glossary-entry-head{
|
||||
position: sticky;
|
||||
top: calc(var(--sticky-header-h, 0px) + var(--page-gap, 12px));
|
||||
z-index: 11;
|
||||
margin: 0 0 24px;
|
||||
border: 1px solid rgba(127,127,127,0.18);
|
||||
border-radius: 28px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,0,0,0.60), rgba(0,0,0,0.92)),
|
||||
radial-gradient(900px 240px at 20% 0%, rgba(0,217,255,0.08), transparent 60%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-radius 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
border-color 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-entry-head__title{
|
||||
padding:
|
||||
var(--entry-hero-pad-top, 18px)
|
||||
var(--entry-hero-pad-x, 18px)
|
||||
calc(var(--entry-hero-pad-top, 18px) - 2px);
|
||||
transition: padding 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-entry-head h1{
|
||||
margin: 0;
|
||||
font-size: var(--entry-hero-h1-size, clamp(2.2rem, 4vw, 3.15rem));
|
||||
line-height: 1.02;
|
||||
letter-spacing: -.04em;
|
||||
font-weight: 850;
|
||||
transition: font-size 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-entry-summary{
|
||||
display: grid;
|
||||
gap: var(--entry-hero-gap, 14px);
|
||||
padding:
|
||||
calc(var(--entry-hero-pad-bottom, 18px) - 2px)
|
||||
var(--entry-hero-pad-x, 18px)
|
||||
var(--entry-hero-pad-bottom, 18px);
|
||||
border-top: 1px solid rgba(127,127,127,0.14);
|
||||
background: rgba(255,255,255,0.02);
|
||||
transition: gap 180ms ease, padding 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-entry-dek{
|
||||
margin: 0;
|
||||
max-width: var(--entry-hero-dek-maxw, 76ch);
|
||||
font-size: var(--entry-hero-dek-size, 1.04rem);
|
||||
line-height: var(--entry-hero-dek-lh, 1.55);
|
||||
opacity: .94;
|
||||
transition:
|
||||
max-width 180ms ease,
|
||||
font-size 180ms ease,
|
||||
line-height 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-entry-signals{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
transition: gap 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-pill{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid rgba(127,127,127,0.24);
|
||||
border-radius: 999px;
|
||||
background: rgba(127,127,127,0.05);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
transition:
|
||||
padding 180ms ease,
|
||||
font-size 180ms ease,
|
||||
background 120ms ease,
|
||||
border-color 120ms ease;
|
||||
}
|
||||
|
||||
.glossary-pill--family{
|
||||
border-color: rgba(127,127,127,0.36);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.glossary-entry-meta{
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(127,127,127,0.18);
|
||||
border-radius: 12px;
|
||||
background: rgba(127,127,127,0.04);
|
||||
max-height: var(--entry-hero-meta-max-h, 12rem);
|
||||
opacity: var(--entry-hero-meta-opacity, 1);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-height 180ms ease,
|
||||
opacity 140ms ease,
|
||||
padding 180ms ease,
|
||||
border-color 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-entry-meta p{
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.glossary-entry-meta p + p{
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px){
|
||||
.glossary-entry-signals{
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.glossary-pill{
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px){
|
||||
.glossary-entry-head{
|
||||
position: static;
|
||||
border-radius: 22px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.glossary-entry-head__title{
|
||||
padding: 14px 14px 12px;
|
||||
}
|
||||
|
||||
.glossary-entry-summary{
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.glossary-entry-dek{
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-entry-meta{
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.glossary-pill{
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
src/components/GlossaryEntryLegacyNote.astro
Normal file
31
src/components/GlossaryEntryLegacyNote.astro
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
interface Props {
|
||||
canonicalHref: string;
|
||||
term: string;
|
||||
}
|
||||
|
||||
const { canonicalHref, term } = Astro.props;
|
||||
---
|
||||
|
||||
<p class="glossary-legacy-note">
|
||||
Cette entrée a été renommée. L’intitulé canonique est :
|
||||
<a href={canonicalHref}>{term}</a>.
|
||||
</p>
|
||||
|
||||
<style>
|
||||
.glossary-legacy-note{
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(127,127,127,0.22);
|
||||
border-radius: 12px;
|
||||
background: rgba(127,127,127,0.05);
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-legacy-note{
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
185
src/components/GlossaryEntryStickySync.astro
Normal file
185
src/components/GlossaryEntryStickySync.astro
Normal file
@@ -0,0 +1,185 @@
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const boot = () => {
|
||||
const body = document.body;
|
||||
const root = document.documentElement;
|
||||
const hero = document.querySelector("[data-ge-hero]");
|
||||
const follow = document.getElementById("reading-follow");
|
||||
const mqMobile = window.matchMedia("(max-width: 860px)");
|
||||
|
||||
if (!body || !root || !hero || !follow) return;
|
||||
|
||||
const BODY_CLASS = "is-glossary-entry-page";
|
||||
const FOLLOW_ON_CLASS = "glossary-entry-follow-on";
|
||||
|
||||
let lastHeight = -1;
|
||||
let lastFollowOn = null;
|
||||
let raf = 0;
|
||||
|
||||
body.classList.add(BODY_CLASS);
|
||||
|
||||
const heroHeight = () =>
|
||||
Math.max(0, Math.round(hero.getBoundingClientRect().height || 0));
|
||||
|
||||
const computeFollowOn = () =>
|
||||
!mqMobile.matches &&
|
||||
follow.classList.contains("is-on") &&
|
||||
follow.style.display !== "none" &&
|
||||
follow.getAttribute("aria-hidden") !== "true";
|
||||
|
||||
const stripLocalSticky = () => {
|
||||
document
|
||||
.querySelectorAll(
|
||||
".glossary-entry-body h2, .glossary-entry-body h3, .glossary-relations h2, .glossary-relations h3"
|
||||
)
|
||||
.forEach((el) => {
|
||||
el.classList.remove("is-sticky");
|
||||
el.removeAttribute("data-sticky-active");
|
||||
});
|
||||
};
|
||||
|
||||
const applyLocalStickyHeight = () => {
|
||||
const h = mqMobile.matches ? 0 : heroHeight();
|
||||
if (h === lastHeight) return;
|
||||
lastHeight = h;
|
||||
|
||||
if (typeof window.__archiSetLocalStickyHeight === "function") {
|
||||
window.__archiSetLocalStickyHeight(h);
|
||||
} else {
|
||||
root.style.setProperty("--glossary-local-sticky-h", `${h}px`);
|
||||
}
|
||||
};
|
||||
|
||||
const syncFollowState = () => {
|
||||
const on = computeFollowOn();
|
||||
if (on === lastFollowOn) return;
|
||||
lastFollowOn = on;
|
||||
body.classList.toggle(FOLLOW_ON_CLASS, on);
|
||||
};
|
||||
|
||||
const syncAll = () => {
|
||||
stripLocalSticky();
|
||||
syncFollowState();
|
||||
applyLocalStickyHeight();
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
if (raf) return;
|
||||
raf = requestAnimationFrame(() => {
|
||||
raf = 0;
|
||||
syncAll();
|
||||
});
|
||||
};
|
||||
|
||||
const followObserver = new MutationObserver(schedule);
|
||||
followObserver.observe(follow, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "style", "aria-hidden"],
|
||||
subtree: false,
|
||||
});
|
||||
|
||||
const heroResizeObserver =
|
||||
typeof ResizeObserver !== "undefined"
|
||||
? new ResizeObserver(schedule)
|
||||
: null;
|
||||
|
||||
heroResizeObserver?.observe(hero);
|
||||
|
||||
window.addEventListener("resize", schedule);
|
||||
window.addEventListener("pageshow", schedule);
|
||||
|
||||
if (document.fonts?.ready) {
|
||||
document.fonts.ready.then(schedule).catch(() => {});
|
||||
}
|
||||
|
||||
if (mqMobile.addEventListener) {
|
||||
mqMobile.addEventListener("change", schedule);
|
||||
} else if (mqMobile.addListener) {
|
||||
mqMobile.addListener(schedule);
|
||||
}
|
||||
|
||||
schedule();
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(body.is-glossary-entry-page #reading-follow){
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-signals){
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-head){
|
||||
margin-bottom: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-summary){
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-dek){
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-pill){
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-meta){
|
||||
padding: 0;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on #reading-follow){
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on #reading-follow .reading-follow__inner){
|
||||
margin-top: -1px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-entry-page .glossary-entry-body h2.is-sticky),
|
||||
:global(body.is-glossary-entry-page .glossary-entry-body h2[data-sticky-active="true"]),
|
||||
:global(body.is-glossary-entry-page .glossary-entry-body h3.is-sticky),
|
||||
:global(body.is-glossary-entry-page .glossary-entry-body h3[data-sticky-active="true"]),
|
||||
:global(body.is-glossary-entry-page .glossary-relations h2.is-sticky),
|
||||
:global(body.is-glossary-entry-page .glossary-relations h2[data-sticky-active="true"]),
|
||||
:global(body.is-glossary-entry-page .glossary-relations h3.is-sticky),
|
||||
:global(body.is-glossary-entry-page .glossary-relations h3[data-sticky-active="true"]){
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
z-index: auto !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 860px){
|
||||
:global(body.is-glossary-entry-page.glossary-entry-follow-on .glossary-entry-head){
|
||||
margin-bottom: 20px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
src/components/GlossaryHomeAside.astro
Normal file
143
src/components/GlossaryHomeAside.astro
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
import {
|
||||
getFondamentaux,
|
||||
getGlossaryHomeStats,
|
||||
getGlossaryPortalLinks,
|
||||
hrefOfGlossaryEntry,
|
||||
} from "../lib/glossary";
|
||||
|
||||
const {
|
||||
allEntries = [],
|
||||
} = Astro.props;
|
||||
|
||||
const fondamentaux = getFondamentaux(allEntries);
|
||||
const portalLinks = getGlossaryPortalLinks();
|
||||
|
||||
const {
|
||||
totalEntries,
|
||||
paradigmesCount,
|
||||
doctrinesCount,
|
||||
metaRegimesCount,
|
||||
} = getGlossaryHomeStats(allEntries);
|
||||
---
|
||||
|
||||
<nav class="glossary-home-aside" aria-label="Navigation du portail du glossaire">
|
||||
<div class="glossary-home-aside__block glossary-home-aside__block--intro">
|
||||
<div class="glossary-home-aside__title">Glossaire archicratique</div>
|
||||
<div class="glossary-home-aside__meta">
|
||||
portail de lecture · cartographie conceptuelle
|
||||
</div>
|
||||
|
||||
<div class="glossary-home-aside__pills" aria-label="Repères de navigation">
|
||||
<span class="glossary-home-aside__pill">{totalEntries} entrées</span>
|
||||
<span class="glossary-home-aside__pill">{metaRegimesCount} méta-régimes</span>
|
||||
<span class="glossary-home-aside__pill">
|
||||
{doctrinesCount} doctrine{doctrinesCount > 1 ? "s" : ""} · {paradigmesCount} paradigme{paradigmesCount > 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="glossary-home-aside__block">
|
||||
<h2 class="glossary-home-aside__heading">Parcours du glossaire</h2>
|
||||
<ul class="glossary-home-aside__list">
|
||||
{portalLinks.map((item) => (
|
||||
<li><a href={item.href}>{item.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{fondamentaux.length > 0 && (
|
||||
<section class="glossary-home-aside__block">
|
||||
<h2 class="glossary-home-aside__heading">Noyau archicratique</h2>
|
||||
<ul class="glossary-home-aside__list">
|
||||
{fondamentaux.map((entry) => (
|
||||
<li><a href={hrefOfGlossaryEntry(entry)}>{entry.data.term}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.glossary-home-aside{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.glossary-home-aside__block{
|
||||
border: 1px solid rgba(127,127,127,0.22);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
background: rgba(127,127,127,0.05);
|
||||
}
|
||||
|
||||
.glossary-home-aside__block--intro{
|
||||
padding-top: 13px;
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
|
||||
.glossary-home-aside__title{
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .2px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.glossary-home-aside__meta{
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.glossary-home-aside__pills{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
.glossary-home-aside__pill{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid rgba(127,127,127,0.24);
|
||||
border-radius: 999px;
|
||||
background: rgba(127,127,127,0.04);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.glossary-home-aside__heading{
|
||||
margin: 0 0 11px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
opacity: .94;
|
||||
}
|
||||
|
||||
.glossary-home-aside__list{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.glossary-home-aside__list li{
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
.glossary-home-aside__list a{
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-home-aside__block,
|
||||
.glossary-home-aside__pill{
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
103
src/components/GlossaryHomeHero.astro
Normal file
103
src/components/GlossaryHomeHero.astro
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
export interface Props {
|
||||
kicker?: string;
|
||||
title?: string;
|
||||
intro?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
kicker = "Référentiel terminologique",
|
||||
title = "Glossaire archicratique",
|
||||
intro = "Ce glossaire n’est pas seulement un index de définitions. Il constitue une porte d’entrée dans la pensée archicratique : une cartographie raisonnée des concepts fondamentaux, des scènes, des dynamiques et des méta-régimes à partir desquels une société peut être décrite comme organisation de tensions et recherche de co-viabilité.",
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<header class="glossary-hero" id="glossary-hero">
|
||||
<p class="glossary-kicker">{kicker}</p>
|
||||
<h1>{title}</h1>
|
||||
<p class="glossary-intro">{intro}</p>
|
||||
<h2
|
||||
class="glossary-hero-follow"
|
||||
id="glossary-hero-follow"
|
||||
aria-hidden="true"
|
||||
></h2>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.glossary-hero{
|
||||
position: sticky;
|
||||
top: var(--glossary-sticky-top);
|
||||
z-index: 12;
|
||||
margin-bottom: 28px;
|
||||
padding: 14px 16px 18px;
|
||||
border: 1px solid rgba(127,127,127,0.18);
|
||||
border-radius: 28px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,0,0,0.60), rgba(0,0,0,0.90)),
|
||||
radial-gradient(900px 240px at 20% 0%, rgba(0,217,255,0.08), transparent 60%);
|
||||
transition:
|
||||
background 300ms cubic-bezier(.22,.8,.22,1),
|
||||
border-color 300ms cubic-bezier(.22,.8,.22,1),
|
||||
box-shadow 300ms cubic-bezier(.22,.8,.22,1);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
display: grid;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
.glossary-kicker{
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
opacity: .72;
|
||||
}
|
||||
|
||||
.glossary-hero h1{
|
||||
margin: 0;
|
||||
font-size: clamp(2.2rem, 4vw, 3.15rem);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -.04em;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.glossary-intro{
|
||||
margin: 0;
|
||||
max-width: 72ch;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.55;
|
||||
opacity: .94;
|
||||
}
|
||||
|
||||
.glossary-hero-follow{
|
||||
margin: 2px 0 0;
|
||||
min-height: var(--glossary-follow-height);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(.985);
|
||||
filter: blur(6px);
|
||||
transition:
|
||||
opacity 220ms cubic-bezier(.22,1,.36,1),
|
||||
transform 320ms cubic-bezier(.22,1,.36,1),
|
||||
filter 320ms cubic-bezier(.22,1,.36,1);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
will-change: opacity, transform, filter;
|
||||
}
|
||||
|
||||
.glossary-hero-follow.is-visible{
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
@media (max-width: 760px){
|
||||
.glossary-hero{
|
||||
top: calc(var(--glossary-sticky-top) - 2px);
|
||||
padding: 12px 14px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
109
src/components/GlossaryHomeSection.astro
Normal file
109
src/components/GlossaryHomeSection.astro
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
export interface Props {
|
||||
id?: string;
|
||||
title: string;
|
||||
intro?: string;
|
||||
followSection?: string;
|
||||
ctaHref?: string;
|
||||
ctaLabel?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
intro,
|
||||
followSection,
|
||||
ctaHref,
|
||||
ctaLabel,
|
||||
} = Astro.props;
|
||||
|
||||
const resolvedFollowSection = (followSection || title || "").trim();
|
||||
const showCta = Boolean(ctaHref && ctaLabel);
|
||||
---
|
||||
|
||||
<section id={id} class="glossary-section">
|
||||
<div class="glossary-section__head">
|
||||
<div>
|
||||
<h2 data-follow-section={resolvedFollowSection}>{title}</h2>
|
||||
|
||||
{intro && (
|
||||
<p class="glossary-intro">{intro}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCta && (
|
||||
<a class="glossary-cta" href={ctaHref}>
|
||||
{ctaLabel}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.glossary-section{
|
||||
margin-top: 42px;
|
||||
scroll-margin-top: calc(var(--glossary-sticky-top) + 190px);
|
||||
}
|
||||
|
||||
.glossary-section__head{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.glossary-section h2{
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 3vw, 2.55rem);
|
||||
line-height: 1.06;
|
||||
letter-spacing: -.03em;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.glossary-intro{
|
||||
margin: 0;
|
||||
max-width: 72ch;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.55;
|
||||
opacity: .94;
|
||||
}
|
||||
|
||||
.glossary-section__head .glossary-intro{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.glossary-cta{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 40px;
|
||||
border: 1px solid var(--glossary-border-strong);
|
||||
border-radius: 999px;
|
||||
padding: 7px 14px;
|
||||
color: var(--glossary-accent);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: transform 120ms ease, background 120ms ease;
|
||||
}
|
||||
|
||||
.glossary-cta:hover{
|
||||
background: var(--glossary-bg-soft-strong);
|
||||
text-decoration: none;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 760px){
|
||||
.glossary-section__head{
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.glossary-cta{
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
src/components/GlossaryPortalAside.astro
Normal file
122
src/components/GlossaryPortalAside.astro
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
interface LinkItem {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ariaLabel: string;
|
||||
title: string;
|
||||
meta?: string;
|
||||
backHref?: string;
|
||||
backLabel?: string;
|
||||
pageItems?: LinkItem[];
|
||||
usefulLinks?: LinkItem[];
|
||||
}
|
||||
|
||||
const {
|
||||
ariaLabel,
|
||||
title,
|
||||
meta,
|
||||
backHref = "/glossaire/",
|
||||
backLabel = "← Retour au glossaire",
|
||||
pageItems = [],
|
||||
usefulLinks = [],
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<nav class="glossary-portal-aside" aria-label={ariaLabel}>
|
||||
<div class="glossary-portal-aside__block">
|
||||
<a class="glossary-portal-aside__back" href={backHref}>{backLabel}</a>
|
||||
<div class="glossary-portal-aside__title">{title}</div>
|
||||
{meta && <div class="glossary-portal-aside__meta">{meta}</div>}
|
||||
</div>
|
||||
|
||||
{pageItems.length > 0 && (
|
||||
<div class="glossary-portal-aside__block">
|
||||
<h2 class="glossary-portal-aside__heading">Dans cette page</h2>
|
||||
<ul class="glossary-portal-aside__list">
|
||||
{pageItems.map((item) => (
|
||||
<li><a href={item.href}>{item.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{usefulLinks.length > 0 && (
|
||||
<div class="glossary-portal-aside__block">
|
||||
<h2 class="glossary-portal-aside__heading">Renvois utiles</h2>
|
||||
<ul class="glossary-portal-aside__list">
|
||||
{usefulLinks.map((item) => (
|
||||
<li><a href={item.href}>{item.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.glossary-portal-aside{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.glossary-portal-aside__block{
|
||||
border: 1px solid rgba(127,127,127,0.22);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(127,127,127,0.05);
|
||||
}
|
||||
|
||||
.glossary-portal-aside__back{
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-portal-aside__title{
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .2px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.glossary-portal-aside__meta{
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
opacity: .78;
|
||||
}
|
||||
|
||||
.glossary-portal-aside__heading{
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.glossary-portal-aside__list{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.glossary-portal-aside__list li{
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.glossary-portal-aside__list a{
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-portal-aside__block{
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
87
src/components/GlossaryPortalCta.astro
Normal file
87
src/components/GlossaryPortalCta.astro
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
export interface Props {
|
||||
href: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
href,
|
||||
label,
|
||||
icon = "↗",
|
||||
className,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<a class:list={["glossary-portal-cta", className]} href={href}>
|
||||
<span>{label}</span>
|
||||
<span aria-hidden="true">{icon}</span>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.glossary-portal-cta{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(0,217,255,0.24);
|
||||
border-radius: 999px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,217,255,0.10), rgba(0,217,255,0.04)),
|
||||
rgba(127,127,127,0.06);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.05),
|
||||
0 0 0 1px rgba(0,217,255,0.04);
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .01em;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
transform 120ms ease,
|
||||
background 120ms ease,
|
||||
border-color 120ms ease,
|
||||
box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.glossary-portal-cta:hover{
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(0,217,255,0.34);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,217,255,0.14), rgba(0,217,255,0.06)),
|
||||
rgba(127,127,127,0.08);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
0 0 0 1px rgba(0,217,255,0.08),
|
||||
0 10px 28px rgba(0,0,0,0.18);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-portal-cta:focus-visible{
|
||||
outline: 2px solid rgba(0,217,255,0.28);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px){
|
||||
.glossary-portal-cta{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-portal-cta{
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,217,255,0.12), rgba(0,217,255,0.05)),
|
||||
rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.glossary-portal-cta:hover{
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,217,255,0.16), rgba(0,217,255,0.07)),
|
||||
rgba(255,255,255,0.06);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
91
src/components/GlossaryPortalGrid.astro
Normal file
91
src/components/GlossaryPortalGrid.astro
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
export type GlossaryPortalGridItem = {
|
||||
href: string;
|
||||
title: string;
|
||||
description: string;
|
||||
meta: string;
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
items?: GlossaryPortalGridItem[];
|
||||
secondary?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
items = [],
|
||||
secondary = false,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
"glossary-portals",
|
||||
secondary && "glossary-portals--secondary",
|
||||
]}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<a class="glossary-portal-card" href={item.href}>
|
||||
<strong>{item.title}</strong>
|
||||
<span>{item.description}</span>
|
||||
<small>{item.meta}</small>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.glossary-portals{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.glossary-portal-card{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--glossary-border);
|
||||
border-radius: 18px;
|
||||
background: var(--glossary-bg-soft);
|
||||
text-decoration: none;
|
||||
transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.glossary-portal-card:hover{
|
||||
transform: translateY(-1px);
|
||||
background: var(--glossary-bg-soft-strong);
|
||||
border-color: rgba(0,217,255,0.16);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-portal-card strong{
|
||||
color: var(--glossary-accent);
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.28;
|
||||
}
|
||||
|
||||
.glossary-portal-card span{
|
||||
color: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
opacity: .94;
|
||||
}
|
||||
|
||||
.glossary-portal-card small{
|
||||
color: var(--glossary-accent);
|
||||
font-size: .94rem;
|
||||
line-height: 1.35;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-portal-card{
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.glossary-portal-card:hover{
|
||||
background: rgba(255,255,255,0.07);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
190
src/components/GlossaryPortalHero.astro
Normal file
190
src/components/GlossaryPortalHero.astro
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
interface Props {
|
||||
prefix: string;
|
||||
kicker: string;
|
||||
title: string;
|
||||
intro: string;
|
||||
moreParagraphs?: string[];
|
||||
introMaxWidth?: string;
|
||||
followIntroMaxWidth?: string;
|
||||
moreMaxHeight?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
prefix,
|
||||
kicker,
|
||||
title,
|
||||
intro,
|
||||
moreParagraphs = [],
|
||||
introMaxWidth = "72ch",
|
||||
followIntroMaxWidth = "68ch",
|
||||
moreMaxHeight = "18rem",
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class="glossary-portal-hero glossary-page-hero"
|
||||
data-glossary-portal-hero
|
||||
style={`--portal-hero-intro-max-w:${introMaxWidth}; --portal-hero-follow-intro-max-w:${followIntroMaxWidth}; --portal-hero-more-max-h:${moreMaxHeight};`}
|
||||
>
|
||||
<p class="glossary-portal-hero__kicker">{kicker}</p>
|
||||
<h1>{title}</h1>
|
||||
|
||||
<p class="glossary-portal-hero__intro">
|
||||
{intro}
|
||||
</p>
|
||||
|
||||
{moreParagraphs.length > 0 && (
|
||||
<div class="glossary-portal-hero__collapsible">
|
||||
<div
|
||||
class="glossary-portal-hero__more"
|
||||
id={`${prefix}-hero-more`}
|
||||
data-glossary-portal-more
|
||||
aria-hidden="false"
|
||||
>
|
||||
{moreParagraphs.map((paragraph) => (
|
||||
<p class="glossary-portal-hero__intro">{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="glossary-portal-hero__toggle"
|
||||
id={`${prefix}-hero-toggle`}
|
||||
data-glossary-portal-toggle
|
||||
type="button"
|
||||
aria-controls={`${prefix}-hero-more`}
|
||||
aria-expanded="false"
|
||||
hidden
|
||||
>
|
||||
lire la suite
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.glossary-portal-hero{
|
||||
position: sticky;
|
||||
top: calc(var(--sticky-header-h, 0px) + var(--page-gap, 12px));
|
||||
z-index: 11;
|
||||
margin: 0 0 24px;
|
||||
padding: 18px 18px 20px;
|
||||
border: 1px solid rgba(127,127,127,0.18);
|
||||
border-radius: 28px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,0,0,0.60), rgba(0,0,0,0.92)),
|
||||
radial-gradient(900px 240px at 20% 0%, rgba(0,217,255,0.08), transparent 60%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
display: grid;
|
||||
row-gap: 14px;
|
||||
transition:
|
||||
margin-bottom 180ms ease,
|
||||
border-radius 180ms ease,
|
||||
padding 180ms ease,
|
||||
row-gap 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__kicker{
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
opacity: .72;
|
||||
}
|
||||
|
||||
.glossary-portal-hero h1{
|
||||
margin: 0;
|
||||
font-size: clamp(2.2rem, 4vw, 3.15rem);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -.04em;
|
||||
font-weight: 850;
|
||||
transition: font-size 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__intro{
|
||||
margin: 0;
|
||||
max-width: var(--portal-hero-intro-max-w);
|
||||
font-size: 1.04rem;
|
||||
line-height: 1.55;
|
||||
opacity: .94;
|
||||
transition:
|
||||
font-size 180ms ease,
|
||||
line-height 180ms ease,
|
||||
max-width 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__collapsible{
|
||||
display: grid;
|
||||
row-gap: 6px;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__more{
|
||||
display: grid;
|
||||
row-gap: 14px;
|
||||
max-height: var(--portal-hero-more-max-h);
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
transition:
|
||||
max-height 220ms ease,
|
||||
opacity 180ms ease;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__toggle{
|
||||
display: none;
|
||||
align-self: flex-start;
|
||||
width: fit-content;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: .01em;
|
||||
opacity: .56;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: .12em;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__toggle:hover{
|
||||
opacity: .84;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__toggle:focus-visible{
|
||||
outline: 2px solid rgba(0,217,255,0.24);
|
||||
outline-offset: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__toggle[hidden]{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 860px){
|
||||
.glossary-portal-hero{
|
||||
position: static;
|
||||
border-radius: 22px;
|
||||
margin-bottom: 20px;
|
||||
padding: 14px 14px 16px;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__intro{
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__more{
|
||||
max-height: none;
|
||||
opacity: 1;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.glossary-portal-hero__toggle{
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
src/components/GlossaryPortalPanel.astro
Normal file
96
src/components/GlossaryPortalPanel.astro
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
export interface Props {
|
||||
id?: string;
|
||||
title: string;
|
||||
count?: string;
|
||||
intro?: string;
|
||||
surface?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
count,
|
||||
intro,
|
||||
surface = false,
|
||||
className,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
"glossary-portal-panel",
|
||||
surface && "glossary-portal-panel--surface",
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<div class="glossary-portal-panel__head">
|
||||
<h3 id={id}>{title}</h3>
|
||||
{count && <span class="glossary-portal-panel__count">{count}</span>}
|
||||
</div>
|
||||
|
||||
{intro && <p class="glossary-portal-panel__intro">{intro}</p>}
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.glossary-portal-panel{
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.glossary-portal-panel--surface{
|
||||
padding: 18px 18px 16px;
|
||||
border: 1px solid var(--glossary-border, rgba(127,127,127,0.18));
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)),
|
||||
var(--glossary-bg-soft, rgba(127,127,127,0.035));
|
||||
}
|
||||
|
||||
.glossary-portal-panel__head{
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.glossary-portal-panel__head h3{
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.3;
|
||||
scroll-margin-top: calc(var(--sticky-offset-px, 96px) + 36px);
|
||||
}
|
||||
|
||||
.glossary-portal-panel__count{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(127,127,127,0.22);
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
opacity: .78;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.glossary-portal-panel__intro{
|
||||
max-width: 78ch;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-portal-panel--surface{
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)),
|
||||
rgba(255,255,255,0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
src/components/GlossaryPortalSection.astro
Normal file
67
src/components/GlossaryPortalSection.astro
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
interface Props {
|
||||
id: string;
|
||||
title: string;
|
||||
count?: string;
|
||||
intro?: string;
|
||||
final?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
count,
|
||||
intro,
|
||||
final = false,
|
||||
className,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<section class:list={["glossary-portal-section", final && "glossary-portal-section--final", className]}>
|
||||
<div class="glossary-portal-section__head">
|
||||
<h2 id={id}>{title}</h2>
|
||||
{count && <span class="glossary-portal-section__count">{count}</span>}
|
||||
</div>
|
||||
|
||||
{intro && <p class="glossary-portal-section__intro">{intro}</p>}
|
||||
|
||||
<slot />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.glossary-portal-section{
|
||||
margin-top: 34px;
|
||||
scroll-margin-top: calc(var(--sticky-offset-px, 96px) + 28px);
|
||||
}
|
||||
|
||||
.glossary-portal-section h2{
|
||||
scroll-margin-top: calc(var(--sticky-offset-px, 96px) + 28px);
|
||||
}
|
||||
|
||||
.glossary-portal-section__head{
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.glossary-portal-section__count{
|
||||
font-size: 13px;
|
||||
opacity: .72;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.glossary-portal-section__intro{
|
||||
max-width: 78ch;
|
||||
margin: 0;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.glossary-portal-section--final{
|
||||
margin-top: 42px;
|
||||
}
|
||||
</style>
|
||||
294
src/components/GlossaryPortalStickySync.astro
Normal file
294
src/components/GlossaryPortalStickySync.astro
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
interface Props {
|
||||
heroMoreId: string;
|
||||
heroToggleId: string;
|
||||
sectionHeadSelector?: string;
|
||||
mobileBreakpoint?: number;
|
||||
autoCollapseDelta?: number;
|
||||
}
|
||||
|
||||
const {
|
||||
heroMoreId,
|
||||
heroToggleId,
|
||||
sectionHeadSelector = ".glossary-portal-section__head",
|
||||
mobileBreakpoint = 860,
|
||||
autoCollapseDelta = 160,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{ heroMoreId, heroToggleId, sectionHeadSelector, mobileBreakpoint, autoCollapseDelta }}
|
||||
>
|
||||
(() => {
|
||||
const boot = () => {
|
||||
const body = document.body;
|
||||
const root = document.documentElement;
|
||||
const hero = document.querySelector("[data-glossary-portal-hero]");
|
||||
const follow = document.getElementById("reading-follow");
|
||||
const heroMore = document.getElementById(heroMoreId);
|
||||
const heroToggle = document.getElementById(heroToggleId);
|
||||
|
||||
if (!body || !root || !hero || !follow) return;
|
||||
|
||||
const BODY_CLASS = "is-glossary-portal-page";
|
||||
const FOLLOW_ON_CLASS = "glossary-portal-follow-on";
|
||||
const EXPANDED_CLASS = "glossary-portal-hero-expanded";
|
||||
const mqMobile = window.matchMedia(`(max-width: ${mobileBreakpoint}px)`);
|
||||
|
||||
let expandedAtY = null;
|
||||
let lastScrollY = window.scrollY || 0;
|
||||
let raf = 0;
|
||||
|
||||
body.classList.add(BODY_CLASS);
|
||||
|
||||
const heroHeight = () =>
|
||||
Math.max(0, Math.round(hero.getBoundingClientRect().height || 0));
|
||||
|
||||
const stripLocalSticky = () => {
|
||||
document.querySelectorAll(sectionHeadSelector).forEach((el) => {
|
||||
el.classList.remove("is-sticky");
|
||||
el.removeAttribute("data-sticky-active");
|
||||
});
|
||||
};
|
||||
|
||||
const computeFollowOn = () =>
|
||||
!mqMobile.matches &&
|
||||
follow.classList.contains("is-on") &&
|
||||
follow.style.display !== "none" &&
|
||||
follow.getAttribute("aria-hidden") !== "true";
|
||||
|
||||
const applyLocalStickyHeight = () => {
|
||||
const h = mqMobile.matches ? 0 : heroHeight();
|
||||
|
||||
if (typeof window.__archiSetLocalStickyHeight === "function") {
|
||||
window.__archiSetLocalStickyHeight(h);
|
||||
} else {
|
||||
root.style.setProperty("--glossary-local-sticky-h", `${h}px`);
|
||||
}
|
||||
};
|
||||
|
||||
const syncFollowState = () => {
|
||||
const on = computeFollowOn();
|
||||
body.classList.toggle(FOLLOW_ON_CLASS, on);
|
||||
return on;
|
||||
};
|
||||
|
||||
const collapseHero = () => {
|
||||
if (!body.classList.contains(EXPANDED_CLASS)) return;
|
||||
|
||||
body.classList.remove(EXPANDED_CLASS);
|
||||
expandedAtY = null;
|
||||
|
||||
if (heroMore) {
|
||||
heroMore.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
||||
if (heroToggle) {
|
||||
heroToggle.hidden = false;
|
||||
heroToggle.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
try {
|
||||
window.__archiUpdateFollow?.();
|
||||
} catch {}
|
||||
|
||||
schedule();
|
||||
};
|
||||
|
||||
const expandHero = () => {
|
||||
body.classList.add(EXPANDED_CLASS);
|
||||
expandedAtY = window.scrollY || 0;
|
||||
|
||||
if (heroMore) {
|
||||
heroMore.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
if (heroToggle) {
|
||||
heroToggle.hidden = true;
|
||||
heroToggle.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
try {
|
||||
window.__archiUpdateFollow?.();
|
||||
} catch {}
|
||||
|
||||
schedule();
|
||||
};
|
||||
|
||||
const syncHeroState = () => {
|
||||
const followOn = computeFollowOn();
|
||||
const expanded = body.classList.contains(EXPANDED_CLASS);
|
||||
const collapsed = followOn && !expanded;
|
||||
|
||||
if (!followOn || mqMobile.matches) {
|
||||
body.classList.remove(EXPANDED_CLASS);
|
||||
expandedAtY = null;
|
||||
|
||||
if (heroMore) {
|
||||
heroMore.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
if (heroToggle) {
|
||||
heroToggle.hidden = true;
|
||||
heroToggle.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (heroMore) {
|
||||
heroMore.setAttribute("aria-hidden", collapsed ? "true" : "false");
|
||||
}
|
||||
|
||||
if (heroToggle) {
|
||||
heroToggle.hidden = !collapsed;
|
||||
heroToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
}
|
||||
};
|
||||
|
||||
const maybeAutoCollapseOnScroll = () => {
|
||||
if (mqMobile.matches) {
|
||||
lastScrollY = window.scrollY || 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!computeFollowOn()) {
|
||||
lastScrollY = window.scrollY || 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body.classList.contains(EXPANDED_CLASS)) {
|
||||
lastScrollY = window.scrollY || 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (expandedAtY == null) {
|
||||
lastScrollY = window.scrollY || 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentY = window.scrollY || 0;
|
||||
const scrollingDown = currentY > lastScrollY;
|
||||
const delta = currentY - expandedAtY;
|
||||
|
||||
if (scrollingDown && delta >= autoCollapseDelta) {
|
||||
collapseHero();
|
||||
}
|
||||
|
||||
lastScrollY = currentY;
|
||||
};
|
||||
|
||||
const syncAll = () => {
|
||||
stripLocalSticky();
|
||||
syncFollowState();
|
||||
syncHeroState();
|
||||
applyLocalStickyHeight();
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
if (raf) return;
|
||||
raf = requestAnimationFrame(() => {
|
||||
raf = 0;
|
||||
requestAnimationFrame(syncAll);
|
||||
});
|
||||
};
|
||||
|
||||
heroToggle?.addEventListener("click", expandHero);
|
||||
|
||||
const onScroll = () => {
|
||||
maybeAutoCollapseOnScroll();
|
||||
schedule();
|
||||
};
|
||||
|
||||
const followObserver = new MutationObserver(schedule);
|
||||
followObserver.observe(follow, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "style", "aria-hidden"],
|
||||
subtree: false,
|
||||
});
|
||||
|
||||
const heroResizeObserver =
|
||||
typeof ResizeObserver !== "undefined"
|
||||
? new ResizeObserver(schedule)
|
||||
: null;
|
||||
|
||||
heroResizeObserver?.observe(hero);
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.addEventListener("resize", schedule);
|
||||
window.addEventListener("pageshow", schedule);
|
||||
|
||||
if (document.fonts?.ready) {
|
||||
document.fonts.ready.then(schedule).catch(() => {});
|
||||
}
|
||||
|
||||
if (mqMobile.addEventListener) {
|
||||
mqMobile.addEventListener("change", schedule);
|
||||
} else if (mqMobile.addListener) {
|
||||
mqMobile.addListener(schedule);
|
||||
}
|
||||
|
||||
schedule();
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(body.is-glossary-portal-page #reading-follow){
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-portal-page.glossary-portal-follow-on .glossary-portal-hero){
|
||||
margin-bottom: 0;
|
||||
padding: 12px 16px 14px;
|
||||
row-gap: 10px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-portal-page.glossary-portal-follow-on .glossary-portal-hero h1){
|
||||
font-size: clamp(1.9rem, 3.2vw, 2.55rem);
|
||||
}
|
||||
|
||||
:global(body.is-glossary-portal-page.glossary-portal-follow-on .glossary-portal-hero__intro){
|
||||
max-width: var(--portal-hero-follow-intro-max-w, 68ch);
|
||||
font-size: .98rem;
|
||||
line-height: 1.48;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-portal-page.glossary-portal-follow-on:not(.glossary-portal-hero-expanded) .glossary-portal-hero__more){
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-portal-page.glossary-portal-follow-on:not(.glossary-portal-hero-expanded) .glossary-portal-hero__toggle){
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-portal-page.glossary-portal-follow-on #reading-follow .reading-follow__inner){
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
:global(body.is-glossary-portal-page .glossary-portal-section__head.is-sticky),
|
||||
:global(body.is-glossary-portal-page .glossary-portal-section__head[data-sticky-active="true"]){
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
z-index: auto !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
</style>
|
||||
92
src/components/GlossaryRelationCards.astro
Normal file
92
src/components/GlossaryRelationCards.astro
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
import type { GlossaryRelationBlock } from "../lib/glossary";
|
||||
import { hrefOfGlossaryEntry } from "../lib/glossary";
|
||||
|
||||
interface Props {
|
||||
relationBlocks: GlossaryRelationBlock[];
|
||||
}
|
||||
|
||||
const { relationBlocks = [] } = Astro.props;
|
||||
const relationsHeadingId = "relations-conceptuelles";
|
||||
---
|
||||
|
||||
{relationBlocks.length > 0 && (
|
||||
<section
|
||||
class="glossary-relations"
|
||||
aria-labelledby={relationsHeadingId}
|
||||
>
|
||||
<h2 id={relationsHeadingId}>Relations conceptuelles</h2>
|
||||
<div class="glossary-relations-grid">
|
||||
{relationBlocks.map((block) => (
|
||||
<section class={`glossary-relations-card ${block.className}`}>
|
||||
<h3>{block.title}</h3>
|
||||
<ul>
|
||||
{block.items.map((item) => (
|
||||
<li>
|
||||
<a href={hrefOfGlossaryEntry(item)}>{item.data.term}</a>
|
||||
<span> — {item.data.definitionShort}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.glossary-relations{
|
||||
margin-top: 26px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid rgba(127,127,127,0.18);
|
||||
}
|
||||
|
||||
.glossary-relations h2{
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.glossary-relations-grid{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.glossary-relations-card{
|
||||
border: 1px solid rgba(127,127,127,0.22);
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(127,127,127,0.05);
|
||||
}
|
||||
|
||||
.glossary-relations-card h3{
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.glossary-relations-card ul{
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.glossary-relations-card li{
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.glossary-relations-card li:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.glossary-relations-card span{
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.glossary-relations-card{
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -25,12 +25,12 @@
|
||||
|
||||
{/* ✅ actions références en haut (niveau 2 uniquement) */}
|
||||
<div class="panel-top-actions level-2" aria-label="Actions références">
|
||||
<div class="panel-actions">
|
||||
<div class="panel-actions">
|
||||
<button class="panel-btn panel-btn--primary" id="panel-ref-submit" type="button">
|
||||
Soumettre une référence (Gitea)
|
||||
Soumettre une référence (Gitea)
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-msg" id="panel-ref-msg" hidden></div>
|
||||
</div>
|
||||
<div class="panel-msg" id="panel-ref-msg" hidden></div>
|
||||
</div>
|
||||
|
||||
<section class="panel-block level-2" aria-label="Références et auteurs">
|
||||
@@ -60,7 +60,7 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* ✅ Lightbox media (pop-up au-dessus du panel) */}
|
||||
{/* ✅ Lightbox media (plein écran) */}
|
||||
<div class="panel-lightbox" id="panel-lightbox" hidden aria-hidden="true">
|
||||
<div class="panel-lightbox__overlay" data-close="1"></div>
|
||||
<div class="panel-lightbox__dialog" role="dialog" aria-modal="true" aria-label="Aperçu du média">
|
||||
@@ -93,6 +93,9 @@
|
||||
const btnMediaSubmit = root.querySelector("#panel-media-submit");
|
||||
const msgMedia = root.querySelector("#panel-media-msg");
|
||||
|
||||
const btnRefSubmit = root.querySelector("#panel-ref-submit");
|
||||
const msgRef = root.querySelector("#panel-ref-msg");
|
||||
|
||||
const taComment = root.querySelector("#panel-comment-text");
|
||||
const btnSend = root.querySelector("#panel-comment-send");
|
||||
const msgComment = root.querySelector("#panel-comment-msg");
|
||||
@@ -101,9 +104,6 @@
|
||||
const lbContent = root.querySelector("#panel-lightbox-content");
|
||||
const lbCaption = root.querySelector("#panel-lightbox-caption");
|
||||
|
||||
const btnRefSubmit = root.querySelector("#panel-ref-submit");
|
||||
const msgRef = root.querySelector("#panel-ref-msg");
|
||||
|
||||
const docTitle = document.body?.dataset?.docTitle || document.title || "Archicratie";
|
||||
const docVersion = document.body?.dataset?.docVersion || "";
|
||||
|
||||
@@ -114,6 +114,16 @@
|
||||
let currentParaId = "";
|
||||
let mediaShowAll = (localStorage.getItem("archicratie:panel:mediaAll") === "1");
|
||||
|
||||
// ===== cosmetics: micro flash “update” =====
|
||||
let _flashT = 0;
|
||||
function flashUpdate(){
|
||||
try {
|
||||
root.classList.add("is-updating");
|
||||
if (_flashT) clearTimeout(_flashT);
|
||||
_flashT = setTimeout(() => root.classList.remove("is-updating"), 180);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ===== globals =====
|
||||
function getG() {
|
||||
return window.__archiGitea || { ready: false, base: "", owner: "", repo: "" };
|
||||
@@ -121,9 +131,6 @@
|
||||
function getAuthInfoP() {
|
||||
return window.__archiAuthInfoP || Promise.resolve({ ok: false, groups: [] });
|
||||
}
|
||||
function isDev() {
|
||||
return Boolean((window.__archiFlags && window.__archiFlags.dev) || /^(localhost|127\.0\.0\.1|\[::1\])$/i.test(location.hostname));
|
||||
}
|
||||
|
||||
const access = { ready: false, canUsers: false };
|
||||
|
||||
@@ -137,23 +144,20 @@
|
||||
return Array.isArray(groups) && groups.some((x) => String(x).toLowerCase() === gg);
|
||||
}
|
||||
|
||||
// ✅ règle mission : readers + editors peuvent soumettre médias + commentaires
|
||||
// ✅ dev fallback : si /_auth/whoami n’existe pas, on autorise pour tester
|
||||
// ✅ readers + editors peuvent soumettre médias + commentaires + refs
|
||||
getAuthInfoP().then((info) => {
|
||||
const groups = Array.isArray(info?.groups) ? info.groups : [];
|
||||
const canReaders = inGroup(groups, "readers");
|
||||
const canEditors = inGroup(groups, "editors");
|
||||
|
||||
access.canUsers = Boolean((info?.ok && (canReaders || canEditors)) || (isDev() && !info?.ok));
|
||||
const whoamiSkipped = Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped);
|
||||
access.canUsers = Boolean((info?.ok && (canReaders || canEditors)) || whoamiSkipped);
|
||||
access.ready = true;
|
||||
|
||||
if (btnMediaSubmit) btnMediaSubmit.disabled = !access.canUsers;
|
||||
if (btnSend) btnSend.disabled = !access.canUsers;
|
||||
|
||||
if (btnRefSubmit) btnRefSubmit.disabled = !access.canUsers;
|
||||
|
||||
|
||||
// si pas d'accès, on informe (soft)
|
||||
if (!access.canUsers) {
|
||||
if (msgHead) {
|
||||
msgHead.hidden = false;
|
||||
@@ -162,12 +166,12 @@
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
// fallback dev
|
||||
access.ready = true;
|
||||
if (isDev()) {
|
||||
if (Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped)) {
|
||||
access.canUsers = true;
|
||||
if (btnMediaSubmit) btnMediaSubmit.disabled = false;
|
||||
if (btnSend) btnSend.disabled = false;
|
||||
if (btnRefSubmit) btnRefSubmit.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -209,8 +213,11 @@
|
||||
async function loadIndex() {
|
||||
if (_idxP) return _idxP;
|
||||
_idxP = (async () => {
|
||||
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" }).catch(() => null);
|
||||
if (res && res.ok) return await res.json();
|
||||
try {
|
||||
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" });
|
||||
if (res && res.ok) return await res.json();
|
||||
} catch {}
|
||||
_idxP = null;
|
||||
return null;
|
||||
})();
|
||||
return _idxP;
|
||||
@@ -251,24 +258,22 @@
|
||||
return issue.toString();
|
||||
}
|
||||
|
||||
// Ouvre un nouvel onglet UNE SEULE FOIS (évite le double-open Safari/Firefox + noopener).
|
||||
function openNewTab(url) {
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
return true; // on ne peut pas détecter proprement un blocage sans retomber dans le double-open
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
function openNewTab(url) {
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ====== GARDES ANTI-DOUBLONS ======
|
||||
const _openStamp = new Map();
|
||||
function openOnce(key, fn) {
|
||||
const now = Date.now();
|
||||
@@ -297,13 +302,21 @@
|
||||
}
|
||||
|
||||
// ===== Lightbox =====
|
||||
function lockScroll(on) {
|
||||
try {
|
||||
document.documentElement.classList.toggle("archi-lb-open", !!on);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
if (!lb) return;
|
||||
lb.hidden = true;
|
||||
lb.setAttribute("aria-hidden", "true");
|
||||
if (lbContent) clear(lbContent);
|
||||
if (lbCaption) { lbCaption.hidden = true; lbCaption.textContent = ""; }
|
||||
lockScroll(false);
|
||||
}
|
||||
|
||||
function openLightbox({ type, src, caption }) {
|
||||
if (!lb || !lbContent) return;
|
||||
clear(lbContent);
|
||||
@@ -342,6 +355,7 @@
|
||||
else { lbCaption.hidden = true; lbCaption.textContent = ""; }
|
||||
}
|
||||
|
||||
lockScroll(true);
|
||||
lb.hidden = false;
|
||||
lb.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
@@ -359,7 +373,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Renders =====
|
||||
function renderLevel2(data) {
|
||||
clear(elL2);
|
||||
if (!elL2) return;
|
||||
@@ -369,7 +382,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(data.authors) && data.authors.length) {
|
||||
if (Array.isArray(data.mobilizedAuthors) && data.mobilizedAuthors.length) {
|
||||
const h = document.createElement("h3");
|
||||
h.className = "panel-subtitle";
|
||||
h.textContent = "Auteurs";
|
||||
@@ -377,7 +390,7 @@
|
||||
|
||||
const ul = document.createElement("ul");
|
||||
ul.className = "panel-list";
|
||||
for (const a of data.authors) {
|
||||
for (const a of data.mobilizedAuthors) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = esc(a);
|
||||
ul.appendChild(li);
|
||||
@@ -559,11 +572,22 @@
|
||||
async function updatePanel(paraId) {
|
||||
currentParaId = paraId || currentParaId || "";
|
||||
if (elId) elId.textContent = currentParaId || "—";
|
||||
|
||||
flashUpdate();
|
||||
|
||||
hideMsg(msgHead);
|
||||
hideMsg(msgMedia);
|
||||
hideMsg(msgComment);
|
||||
hideMsg(msgRef);
|
||||
|
||||
const idx = await loadIndex();
|
||||
|
||||
if (!idx && msgHead && msgHead.hidden) {
|
||||
msgHead.hidden = false;
|
||||
msgHead.textContent = "Index annotations indisponible (annotations-index.json).";
|
||||
msgHead.dataset.kind = "info";
|
||||
}
|
||||
|
||||
const data = idx?.pages?.[pageKey]?.paras?.[currentParaId] || null;
|
||||
|
||||
renderLevel2(data);
|
||||
@@ -571,7 +595,6 @@
|
||||
renderLevel4(data);
|
||||
}
|
||||
|
||||
// ===== media "voir tous" =====
|
||||
if (btnMediaAll) {
|
||||
bindClickOnce(btnMediaAll, (ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -583,7 +606,6 @@
|
||||
btnMediaAll.textContent = mediaShowAll ? "Réduire la liste" : "Voir tous les éléments";
|
||||
}
|
||||
|
||||
// ===== media submit (readers + editors) =====
|
||||
if (btnMediaSubmit) {
|
||||
bindClickOnce(btnMediaSubmit, (ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -626,27 +648,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== référence submit (readers + editors) =====
|
||||
if (btnRefSubmit) {
|
||||
if (btnRefSubmit) {
|
||||
bindClickOnce(btnRefSubmit, (ev) => {
|
||||
ev.preventDefault();
|
||||
hideMsg(msgRef);
|
||||
ev.preventDefault();
|
||||
hideMsg(msgRef);
|
||||
|
||||
if (guardEventOnce(ev, "gitea_open_ref")) return;
|
||||
if (guardEventOnce(ev, "gitea_open_ref")) return;
|
||||
|
||||
if (!currentParaId) return showMsg(msgRef, "Choisis d’abord un paragraphe (scroll / survol).", "warn");
|
||||
if (!getG().ready) return showMsg(msgRef, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
|
||||
if (btnRefSubmit.disabled) return showMsg(msgRef, "Connexion requise (readers/editors).", "error");
|
||||
if (!currentParaId) return showMsg(msgRef, "Choisis d’abord un paragraphe (scroll / survol).", "warn");
|
||||
if (!getG().ready) return showMsg(msgRef, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
|
||||
if (btnRefSubmit.disabled) return showMsg(msgRef, "Connexion requise (readers/editors).", "error");
|
||||
|
||||
const pageUrl = new URL(location.href);
|
||||
pageUrl.search = "";
|
||||
pageUrl.hash = currentParaId;
|
||||
const pageUrl = new URL(location.href);
|
||||
pageUrl.search = "";
|
||||
pageUrl.hash = currentParaId;
|
||||
|
||||
const paraTxt = getParaText(currentParaId);
|
||||
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
|
||||
const paraTxt = getParaText(currentParaId);
|
||||
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
|
||||
|
||||
const title = `[Reference] ${currentParaId} — ${docTitle}`;
|
||||
const body = [
|
||||
const title = `[Reference] ${currentParaId} — ${docTitle}`;
|
||||
const body = [
|
||||
`Chemin: ${location.pathname}`,
|
||||
`URL: ${pageUrl.toString()}`,
|
||||
`Ancre: #${currentParaId}`,
|
||||
@@ -664,18 +685,16 @@
|
||||
``,
|
||||
`---`,
|
||||
`Note: issue générée depuis le site (pré-remplissage).`,
|
||||
].join("\n");
|
||||
].join("\n");
|
||||
|
||||
const url = buildIssueURL({ title, body });
|
||||
if (!url) return showMsg(msgRef, "Impossible de générer l’issue.", "error");
|
||||
const url = buildIssueURL({ title, body });
|
||||
if (!url) return showMsg(msgRef, "Impossible de générer l’issue.", "error");
|
||||
|
||||
const ok = openOnce(`ref:${currentParaId}`, () => openNewTab(url));
|
||||
if (!ok) showMsg(msgRef, "Si rien ne s’ouvre : autorise les popups pour ce site.", "error");
|
||||
const ok = openOnce(`ref:${currentParaId}`, () => openNewTab(url));
|
||||
if (!ok) showMsg(msgRef, "Si rien ne s’ouvre : autorise les popups pour ce site.", "error");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== commentaire (readers + editors) =====
|
||||
if (btnSend) {
|
||||
bindClickOnce(btnSend, (ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -727,60 +746,31 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== wiring: para courant (aligné sur le paragraphe sous le reading-follow) =====
|
||||
function isPara(el) {
|
||||
return Boolean(el && el.nodeType === 1 && el.matches && el.matches('.reading p[id^="p-"]'));
|
||||
// ===== wiring: para courant (SOURCE OF TRUTH = EditionLayout) =====
|
||||
function onCurrentPara(ev) {
|
||||
try {
|
||||
const id = ev?.detail?.id ? String(ev.detail.id) : "";
|
||||
if (!id || !/^p-\d+-/i.test(id)) return;
|
||||
if (id === currentParaId) return;
|
||||
updatePanel(id);
|
||||
} catch {}
|
||||
}
|
||||
window.addEventListener("archicratie:currentPara", onCurrentPara);
|
||||
|
||||
function pickParaAtY(y) {
|
||||
const x = Math.max(0, Math.round(window.innerWidth * 0.5));
|
||||
const candidates = [
|
||||
document.elementFromPoint(x, y),
|
||||
document.elementFromPoint(Math.min(window.innerWidth - 1, x + 60), y),
|
||||
document.elementFromPoint(Math.max(0, x - 60), y),
|
||||
].filter(Boolean);
|
||||
const initial = String(location.hash || "").replace(/^#/, "").trim();
|
||||
|
||||
for (const c of candidates) {
|
||||
if (isPara(c)) return c;
|
||||
const p = c.closest ? c.closest('.reading p[id^="p-"]') : null;
|
||||
if (isPara(p)) return p;
|
||||
}
|
||||
return null;
|
||||
if (/^p-\d+-/i.test(initial)) {
|
||||
updatePanel(initial);
|
||||
} else if (window.__archiCurrentParaId && /^p-\d+-/i.test(String(window.__archiCurrentParaId))) {
|
||||
updatePanel(String(window.__archiCurrentParaId));
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const id = String(window.__archiCurrentParaId || "").trim();
|
||||
if (/^p-\d+-/i.test(id)) updatePanel(id);
|
||||
} catch {}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
let _lastPicked = "";
|
||||
function syncFromFollowLine() {
|
||||
const off = Number(document.documentElement.style.getPropertyValue("--sticky-offset-px")) || 0;
|
||||
const y = Math.round(off + 8);
|
||||
const p = pickParaAtY(y);
|
||||
if (!p || !p.id) return;
|
||||
if (p.id === _lastPicked) return;
|
||||
_lastPicked = p.id;
|
||||
|
||||
// met à jour l'app global (EditionLayout écoute déjà currentPara)
|
||||
try { window.dispatchEvent(new CustomEvent("archicratie:currentPara", { detail: { id: p.id } })); } catch {}
|
||||
|
||||
// et met à jour le panel immédiatement (sans attendre)
|
||||
updatePanel(p.id);
|
||||
}
|
||||
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => {
|
||||
ticking = false;
|
||||
syncFromFollowLine();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.addEventListener("resize", onScroll);
|
||||
|
||||
// Initial: hash > sinon calc
|
||||
const initial = String(location.hash || "").replace(/^#/, "");
|
||||
if (/^p-\d+-/i.test(initial)) updatePanel(initial);
|
||||
else setTimeout(() => { try { syncFromFollowLine(); } catch {} }, 0);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -793,6 +783,8 @@
|
||||
position: sticky;
|
||||
top: calc(var(--sticky-header-h) + var(--page-gap));
|
||||
align-self: start;
|
||||
|
||||
--thumb: 92px; /* ✅ taille des vignettes (80–110 selon goût) */
|
||||
}
|
||||
|
||||
:global(body[data-reading-level="3"]) .page-panel{
|
||||
@@ -910,28 +902,33 @@
|
||||
/* actions médias en haut */
|
||||
.panel-top-actions{ margin-top: 8px; }
|
||||
|
||||
/* ===== media thumbnails (150x150) ===== */
|
||||
/* ===== media thumbnails (plus petits + plus denses) ===== */
|
||||
.panel-media-grid{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--thumb), 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.panel-media-tile{
|
||||
width: 150px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(127,127,127,.20);
|
||||
border-radius: 14px;
|
||||
padding: 8px;
|
||||
background: rgba(127,127,127,0.04);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.panel-media-tile:hover{
|
||||
transform: translateY(-1px);
|
||||
background: rgba(127,127,127,0.07);
|
||||
border-color: rgba(127,127,127,.32);
|
||||
}
|
||||
|
||||
.panel-media-tile img{
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: var(--thumb);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
@@ -939,8 +936,8 @@
|
||||
}
|
||||
|
||||
.panel-media-ph{
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
height: var(--thumb);
|
||||
border-radius: 10px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -983,7 +980,11 @@
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ===== Lightbox ===== */
|
||||
/* ===== Lightbox (plein écran “cinéma”) ===== */
|
||||
:global(html.archi-lb-open){
|
||||
overflow: hidden; /* ✅ empêche le scroll derrière */
|
||||
}
|
||||
|
||||
.panel-lightbox{
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -993,58 +994,66 @@
|
||||
.panel-lightbox__overlay{
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.80);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
background: rgba(0,0,0,0.84);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.panel-lightbox__dialog{
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: calc(var(--sticky-header-h) + 16px);
|
||||
width: min(520px, calc(100vw - 48px));
|
||||
max-height: calc(100vh - (var(--sticky-header-h) + 32px));
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
width: min(1100px, 92vw);
|
||||
max-height: 92vh;
|
||||
overflow: auto;
|
||||
|
||||
border: 1px solid rgba(127,127,127,0.22);
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.10);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 12px;
|
||||
}
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
border-radius: 18px;
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.panel-lightbox__dialog{
|
||||
background: rgba(0,0,0,0.28);
|
||||
}
|
||||
background: rgba(20,20,20,0.55);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
|
||||
padding: 16px;
|
||||
box-shadow: 0 24px 70px rgba(0,0,0,0.55);
|
||||
}
|
||||
|
||||
.panel-lightbox__close{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin-left: auto;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 30px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(127,127,127,0.35);
|
||||
background: rgba(127,127,127,0.10);
|
||||
|
||||
width: 44px;
|
||||
height: 40px;
|
||||
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.22);
|
||||
background: rgba(255,255,255,0.10);
|
||||
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.panel-lightbox__content{
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.panel-lightbox__content img,
|
||||
.panel-lightbox__content video{
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
max-height: calc(92vh - 160px);
|
||||
object-fit: contain;
|
||||
|
||||
background: rgba(0,0,0,0.22);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.panel-lightbox__content audio{
|
||||
@@ -1052,13 +1061,14 @@
|
||||
}
|
||||
|
||||
.panel-lightbox__caption{
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
opacity: .92;
|
||||
color: rgba(255,255,255,0.92);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px){
|
||||
.page-panel{ display: none; }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -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="/archicrat-ia/">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>
|
||||
112
src/content.config.ts
Normal file
112
src/content.config.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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",
|
||||
"dispositif",
|
||||
"figure",
|
||||
"qualification",
|
||||
"epistemologie",
|
||||
]),
|
||||
family: z.enum([
|
||||
"concept-fondamental",
|
||||
"scene",
|
||||
"dynamique",
|
||||
"pathologie",
|
||||
"topologie",
|
||||
"meta-regime",
|
||||
"paradigme",
|
||||
"doctrine",
|
||||
"verbe",
|
||||
"dispositif-ia",
|
||||
"tension-irreductible",
|
||||
"figure",
|
||||
"qualification",
|
||||
"epistemologie",
|
||||
]
|
||||
)
|
||||
.optional(),
|
||||
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,
|
||||
};
|
||||
@@ -12,37 +12,6 @@ source:
|
||||
kind: docx
|
||||
path: "sources/docx/archicrat-ia/Chapitre_1—Fondements_epistemologiques_et_modelisation_Archicratie-version_officielle.docx"
|
||||
---
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
À 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
À 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.
|
||||
|
||||
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 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 1.1 — Hypothèse fondatrice : L’archicratie comme paradigme triadique de régulation
|
||||
|
||||
@@ -52,11 +21,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 +61,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 +77,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 +91,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.
|
||||
|
||||
@@ -192,9 +161,9 @@ Les *objets archicratifs* sont ceux qui manifestent — ou masquent — l’exis
|
||||
|
||||
Ces objets n’ont pas pour fonction de produire un effet régulateur direct. Mais ils changent tout au monde politique, car ils transforment un ordre de fait en un ordre contestable, c’est-à-dire potentiellement réversible. Là réside leur pouvoir propre : ils inscrivent la norme dans le champ de la délibération. Ils ne gouvernent pas, mais ils rendent le gouvernement visible, énonçable et amendable.
|
||||
|
||||
Un simple registre de saisine citoyenne — tenu à jour, accessible, lisible — est un puissant *objet d’archicration*. Il permet à une décision d’être rouverte, à une politique d’être contestée, à un dispositif d’être réévalué. Une décision administrative motivée — dont les motifs sont publiés, les délais indiqués, les voies de recours explicitement mentionnées — devient, en ce sens, un support d’opposabilité. Inversement, son absence signe une forme d’autoritarisme silencieux, une neutralisation de la scène. De même, un procès-verbal d’audition contradictoire — où les arguments des différentes parties sont repris, répondus, hiérarchisés — est un *objet archicratif* à haute valeur politique. Il permet mise en visibilité d’une épreuve, une documentation de l’écoute, une matérialisation de la scène.
|
||||
Un simple registre de saisine citoyenne — tenu à jour, accessible, lisible — est un puissant *objet d’archicration*. Il permet à une décision d’être rouverte, à une politique d’être contestée, à un dispositif d’être réévalué. Une décision administrative motivée — dont les motifs sont publiés, les délais indiqués, les voies de recours explicitement mentionnées — devient, en ce sens, un support d’opposabilité. Inversement, son absence signe une forme d’autoritarisme silencieux, une neutralisation de la scène. De même, un procès-verbal d’audition contradictoire — où les arguments des différentes parties sont repris, répondus, hiérarchisés — est un *objet archicratif* à haute valeur politique. Il rend visible une épreuve, documente l’écoute et matérialise la scène.
|
||||
|
||||
Mais comme pour les *objets arcaux et cratiaux*, la puissance politique d’un *objet archicratif* ne tient pas à sa simple existence formelle. Encore faut-il qu’il soit effectif, accessible, activable. Une plateforme de consultation publique qui n’est ni analysée, ni suivie d’effet, ni même modérée de manière transparente, n’est pas un objet d’archicration — c’est une *simulacre archicratique*. Un délai de recours de 24 heures sans possibilité d’assistance juridique n’est pas un mécanisme de contradictoire, mais un leurre procédural. Il en va de même pour un registre de doléances enfermé dans une armoire, jamais traité, jamais rendu public, n’ouvrant aucune scène, ne garantissant aucun différé. Il mime l’ouverture archicrative, tout en l’empêchant.
|
||||
Mais comme pour les *objets arcaux et cratiaux*, la puissance politique d’un *objet archicratif* ne tient pas à sa simple existence formelle. Encore faut-il qu’il soit effectif, accessible, activable. Une plateforme de consultation publique qui n’est ni analysée, ni suivie d’effet, ni même modérée de manière transparente, n’est pas un objet d’archicration — c’est *un simulacre archicratique*. Un délai de recours de 24 heures sans possibilité d’assistance juridique n’est pas un mécanisme de contradictoire, mais un leurre procédural. Il en va de même pour un registre de doléances enfermé dans une armoire, jamais traité, jamais rendu public, n’ouvrant aucune scène, ne garantissant aucun différé. Il mime l’ouverture archicrative, tout en l’empêchant.
|
||||
|
||||
Ainsi, les *objets d’archicration* sont hautement vulnérables à la capture, au vide symbolique, à la neutralisation rituelle. Ce sont les objets les plus spectaculaires, mais aussi les plus faciles à rendre ineffectifs, car leur forme suffit à donner l’illusion de la dispute. D’où l’importance, dans le paradigme archicratique, de ne jamais les considérer de manière formelle ou nominale, mais toujours selon une grille d’évaluation substantielle :
|
||||
|
||||
@@ -214,9 +183,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 +195,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 +213,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.
|
||||
|
||||
@@ -256,17 +225,17 @@ Il importe alors de penser la régulation non comme une mécanique fluide, mais
|
||||
|
||||
C’est pourquoi la cartographie archicratique ne vise pas à repérer des dispositifs « purs », mais à diagnostiquer des régimes de composition : *comment s’agencent, dans tel ou tel secteur, l’arcalité, la cratialité et l’archicration ? Quelle est la qualité de leur articulation ? Quels seuils de domination ou de carence observe-t-on ?* Cette dynamique nous conduit directement à la section 1.1.4, où nous décrirons les premiers cas limites — ces régulations déséquilibrées où un seul pôle l’emporte, où les autres se retirent, où l’ordre se fige ou s’effondre. Ce sont ces cas extrêmes qui révéleront, par contraste, ce qui fait la cohérence — ou la défaillance — d’une régulation.
|
||||
|
||||
Ainsi, loin de reposer sur une addition de fonctions ou un empilement de principes, le paradigme archicratique se fonde sur l’analyse différentielle de l’articulation dynamique entre fondement, opération et épreuve. C’est cette tension constitutive — ni soluble, ni effaçable — qui fait de la régulation un fait politique, et non une simple organisation. Et c’est par l’analyse rigoureuse de cette dynamique que nous pourrons, dans les sections ultérieures, établir une typologie des régimes (1.5), des déséquilibres (1.6), des porteurs (1.7), des temporalités (1.8), et des cas empiriques (1.9).
|
||||
Ainsi, loin de reposer sur une addition de fonctions ou un empilement de principes, le paradigme archicratique se fonde sur l’analyse différentielle de l’articulation dynamique entre fondement, opération et épreuve. C’est cette tension constitutive — ni soluble, ni effaçable — qui fait de la régulation un fait politique, et non une simple organisation. Et c’est par l’analyse rigoureuse de cette dynamique que nous pourrons, dans les sections ultérieures, préciser les premiers déséquilibres (1.1.4), puis plus tard dans le chapitre, les formes dynamiques de la tenue archicratique (1.5), les repères heuristiques (1.6) et les morphologies opérantes (1.7).
|
||||
|
||||
### 1.1.4 — Premiers cas limites : déséquilibres, courts-circuitages et régulations instables
|
||||
|
||||
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 +243,11 @@ 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.
|
||||
À l’inverse, ces cas-limites valent aussi comme révélateurs d’une exigence positive : ils indiquent, par contraste, ce qu’une tenue archicratique minimale doit préserver pour qu’une régulation demeure habitable, révisable et politiquement soutenable.
|
||||
|
||||
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.
|
||||
|
||||
@@ -294,7 +265,7 @@ Et c’est dans cette tension — entre diagnostic et normativité, entre transv
|
||||
|
||||
### 1.2.1 — Régime, forme, structure : clarification des niveaux d’analyse
|
||||
|
||||
Penser le politique suppose toujours, à un moment donné, de se situer dans une hiérarchie conceptuelle : quels sont les niveaux auxquels se déploient les régulations ? Où s’arrête la description ? Où commence la structure ? Quand l’analyse quitte-t-elle l’empirisme des formes visibles pour formuler une hypothèse sur les conditions de possibilité elles-mêmes ? Le paradigme archicratique, en tant qu’hypothèse structurante, impose de clarifier cette gradation. Car on ne peut pas l’identifier ni en mesurer la portée sans distinguer trois plans souvent confondus : le régime, la forme et la structure.
|
||||
Pour mesurer la portée du paradigme archicratique, il faut d’abord distinguer trois plans souvent confondus : le régime, la forme et la structure. Penser le politique suppose toujours, à un moment donné, de se situer dans une hiérarchie conceptuelle : à quels niveaux se déploient les régulations ? où s’arrête la description ? où commence l’hypothèse sur leurs conditions de possibilité ?
|
||||
|
||||
Un régime, au sens classique des sciences politiques (depuis Aristote jusqu’à Linz, Sartori ou Bobbio), désigne un type de gouvernement fondé sur une certaine configuration d’institutions, de normes et de principes de légitimation. Démocratie représentative, monarchie constitutionnelle, autoritarisme plébiscitaire, théocratie — autant de régimes qui décrivent des architectures institutionnelles différenciées, avec leurs acteurs, leurs règles du jeu, leurs modes de reproduction. Le régime est donc un ensemble visible, énonçable, formalisé, que l’on peut inscrire dans une histoire constitutionnelle, dans un droit positif, dans une morphologie institutionnelle repérable.
|
||||
|
||||
@@ -302,7 +273,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.
|
||||
|
||||
@@ -310,34 +281,36 @@ En clarifiant ce niveau d’analyse, on peut désormais définir l’*archicrati
|
||||
|
||||
### 1.2.2 — L’archicratie comme méta-régime transversal de la régulation
|
||||
|
||||
Comme nous l’avons étayé l’*archicratie* n’est pas un type de régime politique au sens classique du terme. Elle n’est ni une démocratie, ni une autocratie, ni une technocratie, ni une bureaucratie au sens formel. Elle n’est pas définie par un mode d’accès au pouvoir, une forme de représentation, ou une structure constitutionnelle donnée. Elle ne se substitue pas aux régimes : elle les traverse, les parasite, les reconfigure, les dilue ou les redouble. Ce que le paradigme archicratique propose, ce n’est donc pas d’ajouter une étiquette à la typologie déjà saturée des formes politiques, mais de formuler une hypothèse forte sur la structure profonde qui rend possible — ou impossible — la régulation politique dans ses dimensions fondamentales.
|
||||
Comme nous l’avons établi, l’archicratie n’est pas un type de régime politique au sens classique du terme. Elle n’est ni une démocratie, ni une autocratie, ni une technocratie, ni une bureaucratie au sens formel. Elle n’est pas définie par un mode d’accès au pouvoir, une forme de représentation, ou une structure constitutionnelle donnée. Elle ne se substitue pas aux régimes : elle les traverse, les reconfigure, parfois les parasite ou les redouble. Ce que le paradigme archicratique propose, ce n’est donc pas d’ajouter une étiquette à la typologie déjà saturée des formes politiques, mais de formuler une hypothèse forte sur la structure profonde qui rend possible — ou impossible — la régulation politique dans ses dimensions fondamentales.
|
||||
|
||||
C’est en ce sens que nous définissons l’*archicratie comme un méta-régime de régulation*.
|
||||
|
||||
Un méta-régime n’est pas une catégorie de surface. Il ne désigne pas un type d’organisation institutionnelle, ni même un modèle de gouvernement parmi d’autres. Il opère à un niveau plus profond : celui des conditions de possibilité de la régulation, celui de l’articulation — ou de la disjonction — entre les trois dimensions constitutives que nous avons décrites : l’*arcalité* (fondement), la *cratialité* (opération), et l’*archicration* (épreuve). Là où les régimes désignent des structures politiques historiquement repérables, le méta-régime désigne la manière dont ces structures rendent (ou non) leurs propres régulations visibles, discutables, contestables et révisables.
|
||||
Un méta-régime n’est pas une catégorie de surface. Il ne désigne pas un type d’organisation institutionnelle, ni même un modèle de gouvernement parmi d’autres. Il opère à un niveau plus profond : celui des conditions de possibilité de la régulation, celui de l’articulation — ou de la disjonction — entre les trois dimensions constitutives que nous avons décrites : l’*arcalité* (fondement), la *cratialité* (opération), et l’*archicration* (épreuve). Là où les régimes désignent des structures politiques historiquement repérables, le méta-régime désigne la manière dont ces structures rendent (ou non) leurs propres régulations exposables à la contradiction, à la reprise et à la révision.
|
||||
|
||||
Dit autrement, un *méta-régime* n’est pas une forme politique particulière, mais un rapport entre les formes, un *agencement systémique entre ce qui fonde, ce qui opère, et ce qui rend opposable*. Il ne se lit pas dans les discours publics, ni même dans les textes constitutionnels : il se repère dans la cohérence (ou l’incohérence) d’un dispositif de régulation. Ce que propose le paradigme archicratique, c’est donc une grille d’intelligibilité transversale : un outil analytique permettant de diagnostiquer des états de régulation dans toute configuration politique donnée, en interrogeant non pas leur légalité ou leur conformité, mais leur tenue archicrative effective.
|
||||
Dit autrement, un *méta-régime* n’est pas une forme politique particulière, mais un rapport entre les formes, un *agencement systémique entre ce qui fonde, ce qui opère, et ce qui rend opposable*. Il ne se lit pas dans les discours publics, ni même dans les textes constitutionnels : il se repère dans la cohérence (ou l’incohérence) d’un dispositif de régulation. Ce que propose le paradigme archicratique, c’est donc un analyseur transversal des états de régulation dans toute configuration politique donnée, en interrogeant non pas leur légalité ou leur conformité, mais leur tenue archicrative effective.
|
||||
|
||||
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.
|
||||
|
||||
La seconde, que nous nommons *contextualisée*, qui affirme que chaque régime porte sa cohérence interne, ses normes propres, son rythme historique. Certes. Mais le paradigme archicratique montre que certaines tensions traversent tous les régimes : celle entre ce qui fonde, ce qui opère, et ce qui conteste. Et que c’est dans cette tension que se joue la co-viabilité politique réelle d’un ordre.
|
||||
|
||||
En ce sens, on peut dire que le paradigme archicratique relève d’une démarche post-weberienne : il ne cherche pas à classer les régimes d’autorité selon des types idéaux de légitimation (traditionnelle, charismatique, légale-rationnelle), mais à analyser les conditions concrètes de l’opposabilité des régulations, indépendamment de leurs formes déclarées. Il reformule la question du pouvoir non en termes de commandement, mais en termes de co-présence entre les dimensions de fondation, d’effectuation, et de mise en épreuve.
|
||||
En ce sens, on peut dire que le paradigme archicratique relève d’une démarche post-wébérienne : il ne cherche pas à classer les régimes d’autorité selon des types idéaux de légitimation (traditionnelle, charismatique, légale-rationnelle), mais à analyser les conditions concrètes de l’opposabilité des régulations, indépendamment de leurs formes déclarées. Il reformule la question du pouvoir non en termes de commandement, mais en termes de co-présence entre les dimensions de fondation, d’effectuation, et de mise en épreuve.
|
||||
|
||||
C’est cette transversalité — au-delà des régimes, des époques, des configurations — qui fonde l’*archicratie* comme méta-régime analytique. Elle ne se substitue pas aux typologies existantes, mais les traverse. Elle n’invalide pas les concepts de démocratie, d’État, de représentation, mais les reconfigure en introduisant une dimension de *régulation par tensions de co-viabilité*.
|
||||
|
||||
Pour cette raison, l’archicratie peut être mobilisée à la fois comme hypothèse heuristique, comme outil critique, et comme grille de diagnostic différentiel. Elle ne prétend pas tout expliquer, mais elle permet de poser cette question décisive : *dans un régime donné, qui peut invoquer quoi ? Qui peut agir sur quoi ? Qui peut contester quoi ? Et selon quelles formes, sous quels délais, avec quelle effectivité ?*
|
||||
Pour cette raison, l’archicratie peut être mobilisée à la fois comme hypothèse heuristique, comme outil critique, et comme cadre de diagnostic différentiel. Elle ne prétend pas tout expliquer, mais elle permet de poser cette question décisive : *dans un régime donné, qui peut invoquer quoi ? Qui peut agir sur quoi ? Qui peut contester quoi ? Et selon quelles formes, sous quels délais, avec quelle effectivité ?*
|
||||
|
||||
Autrement dit : le pouvoir régule — *mais est-il régulé ?*
|
||||
Et si, non : *où est passée la scène ?*
|
||||
Et, si ce n’est pas le cas, où est passée la scène ?
|
||||
|
||||
Inversement, lorsqu’une scène tient encore, même localement, est-ce elle qui rend de nouveau possible la suspension, la comparution et la requalification des prises régulatrices.
|
||||
|
||||
### 1.2.3 — Les critères de validité paradigmatique : fécondité, puissance et opposabilité
|
||||
|
||||
Ce qui rend un paradigme légitime, c’est sa capacité à soutenir la pensée, à générer de nouvelles distinctions, à organiser l’intelligibilité du réel, mais surtout à s’exposer à l’épreuve, c’est-à-dire à la mise en tension critique, à la validation empirique, à la falsifiabilité théorique et à la réversibilité herméneutique. À cette aune, le paradigme archicratique ne saurait se contenter d’être une intuition éclairante ou une métaphore suggestive ; il doit se doter d’un ensemble de critères de validité, rigoureusement définis, qui justifient son usage comme instrument analytique, son efficience comme outil heuristique, et sa robustesse comme cadre de pensée politique. Un paradigme sert à rendre visible ce qui était jusqu’alors obscurci, dispersé, méconnu ou neutralisé.
|
||||
Ce qui rend un paradigme légitime, c’est sa capacité à soutenir la pensée, à générer de nouvelles distinctions, à organiser l’intelligibilité du réel, mais surtout à s’exposer à l’épreuve, c’est-à-dire à la mise à l’épreuve contradictoire, à la validation empirique, à la falsifiabilité théorique et à la réversibilité herméneutique. À cette aune, le paradigme archicratique ne saurait se contenter d’être une intuition éclairante ou une métaphore suggestive ; il doit se doter d’un ensemble de critères de validité, précisément définis, qui justifient son usage comme instrument analytique, son efficience comme outil heuristique, et sa robustesse comme cadre de pensée politique. Un paradigme sert à rendre visible ce qui était jusqu’alors obscurci, dispersé, méconnu ou neutralisé.
|
||||
|
||||
Trois conditions principales structurent cette exigence de validité : la fécondité analytique, la puissance explicative, et surtout l’opposabilité empirique — cette dernière constituant le seuil le plus élevé de toute ambition théorique sérieuse. En cela, la validité paradigmatique ne relève ni du consensus idéologique, ni de la cohérence formelle interne : elle engage une éthique épistémique, un devoir de rigueur, un engagement de réfutabilité qui doit orienter l’ensemble de notre construction.
|
||||
|
||||
@@ -353,7 +326,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 +342,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.
|
||||
Par ailleurs, ce paradigme pourra être confronté à des contre-exemples méthodiques — ce qui devra être éprouvé dans les usages empiriques ultérieurs du paradigme — 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é à élucider 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 un opérateur 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
|
||||
|
||||
@@ -383,7 +356,7 @@ La construction conceptuelle que nous avons amorcée ne pourra véritablement ê
|
||||
|
||||
Ce que nous proposons ici n’est donc pas une compilation de principes abstraits ou de normes morales : il s’agit d’une *charte de validité*. Les axiomes qui suivent assurent la cohérence interne de la proposition archicratique. Ils ne valent pas comme protocole d’évaluation ni comme grille de classement. Ils orientent la lecture conceptuelle des cas, sans prétendre à une mesure. Ces axiomes ont principalement une triple fonction.
|
||||
|
||||
D’abord, ils stabilisent le cadre : ils garantissent que le paradigme archicratique ne dérive pas en inflation métaphorique, ni en concept de substitution. Ensuite, ils rendent le paradigme opératoire : en définissant les conditions de son application, ils permettent une lecture outillée et rigoureuse des régulations réelles. Enfin, ils l’exposent à la critique : chacun de ces axiomes peut être discuté, révoqué, confronté, ce qui constitue l’indice de leur légitimité et non de leur fragilité.
|
||||
D’abord, ils stabilisent le cadre : ils garantissent que le paradigme archicratique ne dérive pas en inflation métaphorique, ni en concept de substitution. Ensuite, ils en rendent possible la mise en œuvre analytique : en définissant les conditions de son application, ils permettent une lecture outillée et rigoureuse des régulations réelles. Enfin, ils l’exposent à la critique : chacun de ces axiomes peut être discuté, révoqué, confronté, ce qui constitue l’indice de leur légitimité et non de leur fragilité.
|
||||
|
||||
Il faut ici rappeler les grandes exigences posées par les théoriciens de la science : Karl Popper (1934) insistait sur la falsifiabilité comme critère de démarcation entre science et idéologie ; Imre Lakatos (1978) montrait que tout programme de recherche scientifique devait contenir un noyau dur protégé par un ensemble de conditions négociables — c’est-à-dire une structure qui permet à la fois la stabilité théorique et l’ouverture à la révision. À leur suite, mais dans un champ spécifique — celui de la philosophie politique — nous affirmons qu’un paradigme doit se rendre intelligible par sa propre explicitation méthodique, sous peine de sombrer dans la circularité ou dans le fétichisme terminologique.
|
||||
|
||||
@@ -401,7 +374,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.
|
||||
|
||||
@@ -477,7 +452,7 @@ Dans la section 1.4, nous déploierons une cartographie systémique de ces objet
|
||||
|
||||
Cet axiome tire toutes les conséquences analytiques et politiques de ce que nous avons établi comme architecture fondatrice du paradigme archicratique : la régulation politique ne repose pas sur un centre unique d’autorité, ni sur une mécanique univoque d’exécution, ni sur une utopie dialogique sans ancrage. Elle repose sur une tenue dynamique entre trois fonctions irréductibles : fonder, opérer, disputer.
|
||||
|
||||
Mais plus encore que leur distinction, c’est leur non-substituabilité qui constitue le cœur de l’axiome ici énoncé. Ce que nous affirmons, c’est que chacun des trois pôles est nécessaire, mais aucun n’est suffisant. Pris isolément, chacun génère une forme de déséquilibre radical, une défaillance politique, un court-circuit de la régulation. Le paradigme archicratique, dès lors, ne repose pas sur une addition bien plus sur une *disjonction fonctionnelle stricte* : les trois fonctions doivent être tenues ensemble, sans qu’aucune ne puisse se substituer aux deux autres.
|
||||
Mais plus encore que leur distinction, c’est leur non-substituabilité qui constitue le cœur de l’axiome ici énoncé. Ce que nous affirmons, c’est que chacun des trois pôles est nécessaire, mais aucun n’est suffisant. Pris isolément, chacun génère une forme de déséquilibre radical, une défaillance politique, un court-circuit de la régulation. Le paradigme archicratique, dès lors, ne repose pas sur une simple addition, mais bien sur une disjonction fonctionnelle stricte : les trois fonctions doivent être tenues ensemble, sans qu’aucune ne puisse se substituer aux deux autres.
|
||||
|
||||
Ce principe a une portée théorique majeure. Il interdit toute régression vers une ontologie mono-polaire du politique. Il récuse les réductions trop souvent opérées dans l’analyse des dispositifs. Lorsque le politique est saisi uniquement comme fondement, valeur, idéal ou texte, la régulation devient pur symbolisme. C’est le règne du mythe ou du droit sans effets. Les chartes sont proclamées, mais rien ne les rend opératoires. La Constitution devient un fétiche ; la norme, un leurre. Cette sur-arcalisation engendre des régimes rhétoriques, où le fondement reste sans effet.
|
||||
|
||||
@@ -487,9 +462,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 +526,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.
|
||||
|
||||
@@ -573,7 +548,7 @@ Ce que cet axiome protège, en somme, c’est la vitalité épistémique du para
|
||||
|
||||
**Un paradigme n’est valable que s’il résiste à l’épreuve des régimes. Un outil d’analyse n’est robuste que s’il traverse les configurations hétérogènes — monarchies, démocraties, technocraties, gouvernances hybrides — sans s’effondrer dans la généralité vide ou la contradiction interne. Le paradigme archicratique doit démontrer sa capacité d’opérabilité transversale : il doit s’appliquer à la diversité des dispositifs politiques sans renoncer à sa grammaire.**
|
||||
|
||||
L’*archicratie* n’est pas une forme de régime politique — elle n’est pas un type au sens de la typologie wébérienne (monarchie, démocratie, autoritarisme). Elle ne désigne ni une structure institutionnelle, ni une idéologie, ni une culture politique. Elle est une modalité de régulation, un style d’agencement entre trois pôles — *arcalité, cratialité, archicration* — qui peut être observé à travers une variété de formes politiques. Ce que cet axiome affirme, c’est que le paradigme archicratique doit pouvoir être mobilisé comme méta-régimes de régulation :
|
||||
L’*archicratie* n’est pas une forme de régime politique — elle n’est pas un type au sens de la typologie wébérienne (monarchie, démocratie, autoritarisme). Elle ne désigne ni une structure institutionnelle, ni une idéologie, ni une culture politique. Elle est une modalité de régulation, un style d’agencement entre trois pôles — *arcalité, cratialité, archicration* — qui peut être observé à travers une variété de formes politiques. Ce que cet axiome affirme, c’est que le paradigme archicratique doit pouvoir être mobilisé comme méta-régime de régulation :
|
||||
|
||||
- dans des régimes démocratiques libéraux (ex. UE, États-Unis) : où l’*arcalité* est pluraliste, la *cratialité* technicisée, et l’*archicration* souvent mimée ou saturée ;
|
||||
|
||||
@@ -614,7 +589,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.
|
||||
|
||||
@@ -626,7 +601,7 @@ Ces huit axiomes dessinent ensemble une épistémologie forte, souple, rigoureus
|
||||
|
||||
En cela, cette section s’est attelée à instituer une discipline de pensée. Bien plus encore, une exigence de probité intellectuelle qui sera maintenue dans les sections suivantes : qu’il s’agisse d’explorer la topologie interne/externe des pôles (section 1.4), de qualifier les formes dynamiques de tenues archicratiques (section 1.5), de présenter des repères de l’archicratie (section 1.6), ou d’en cartographier les morphologies opérantes (sections 1.7), c’est toujours à l’aune de ces axiomes que nos propositions devront être jugées.
|
||||
|
||||
Ce chapitre ne vise pas à fonder un dogme, mais à instituer un outil. Et tout outil digne de ce nom devait commencer par l’exposé rigoureux de ses conditions de validité.
|
||||
Ces huit axiomes fixent la discipline de pensée du paradigme archicratique. C’est à leur lumière qu’il faut désormais lire sa grammaire topologique, ses formes dynamiques, ses repères heuristiques et ses morphologies opérantes.
|
||||
|
||||
## **1.4 —** La grammaire topologique interne/externe de l’*archicratie*
|
||||
|
||||
@@ -642,7 +617,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 +691,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 +719,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 +811,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 +873,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 +929,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.
|
||||
|
||||
@@ -1002,25 +977,27 @@ C’est dans ces configurations que l’analyse archicratique prend tout son sen
|
||||
|
||||
En somme, penser la *co-viabilité* d’un dispositif régulateur, c’est penser sa tenue dans le différé, son ouverture à l’extériorité, sa capacité de reconfiguration, sans perdre son ancrage ni dissoudre sa légitimité. Ce n’est ni un équilibre idéal, ni une norme absolue : c’est une exigence politique minimale. Et c’est à cette exigence que répond, dans sa vocation critique, le paradigme archicratique.
|
||||
|
||||
## **1.5 — Formes dynamiques de tenues archicratiques**
|
||||
## **1.5 — Formes dynamiques de la tenue archicratique**
|
||||
|
||||
Penser un paradigme relationnel, c’est refuser de figer les configurations dans des états fixes, des catégories stables ou des oppositions binaires. Le paradigme archicratique ne cherche pas à déterminer si un système est équilibré ou déréglé, mais à analyser comment il tient, par quelles prises, dans quelle configuration de relations entre *arcalité, cratialité et archicration*, et surtout, jusqu’où cette tenue est vivable, soutenable, opposable. La régulation n’est pas un état ; c’est une forme — toujours en tension, toujours en transformation.
|
||||
|
||||
Ce que nous proposons dans cette section, c’est une typologie des formes de tenue archicratique — autrement dit, une modélisation des manières dont les trois pôles du paradigme s’articulent ou se désarticulent dans les dispositifs réels. Car toute régulation effective engage nécessairement ces trois dimensions, mais elle le fait selon des équilibres hétérogènes, des désajustements partiels, des déséquilibres provisoirement tenus, des saturations masquées ou des ajustements régénérants.
|
||||
|
||||
Nous nommerons ici forme de tenue archicratique toute configuration empirique ou modélisable dans laquelle les trois pôles sont en relation active, selon des degrés de présence, d’articulation, de visibilité et d’effectivité différenciés. Certaines formes permettent la viabilité démocratique du dispositif — nous les qualifierons de *synchrotopiques* : elles maintiennent la tension entre les pôles dans un espace de co-viabilité. D’autres sont marquées par l’hypertrophie d’un pôle, l’effacement d’un autre, ou la déconnexion entre niveaux — elles conduisent à des formes dites *hypertopiques* ou *entropiques*. Enfin, certaines configurations ne relèvent d’aucune pathologie manifeste, mais laissent émerger des signes cliniques faibles de désarticulation : perte de scène, opacité opérative, ritualisation creuse des fondements, etc.
|
||||
Nous nommerons ici forme de tenue archicratique toute configuration empirique ou modélisable dans laquelle les trois pôles sont en relation active, selon des degrés de présence, d’articulation, de visibilité et d’effectivité différenciés. Certaines formes permettent la viabilité démocratique du dispositif — nous les qualifierons de *synchrotopiques* : elles maintiennent la tension entre les pôles dans un espace de co-viabilité. D’autres sont marquées par l’hypertrophie d’un pôle, l’effacement d’un autre, ou la déconnexion entre niveaux — elles conduisent à des formes dites hypertopiques, hypotopiques ou atopiques. Enfin, certaines configurations ne relèvent d’aucune pathologie manifeste, mais laissent émerger des signes cliniques faibles de désarticulation : perte de scène, opacité opérative, ritualisation creuse des fondements, etc.
|
||||
|
||||
Ces formes de tenues ne valent ni comme jugement moral, ni comme idéal-type figé. Elles doivent être comprises comme des formes observables et évolutives. Elles ne sont pas des essences, mais des positions dans un espace de viabilité régulatoire. Et leur analyse permet, dans chaque cas empirique, de répondre à la question fondamentale : *qu’est-ce qui tient ici ? Comment ? À quel prix ? Et pour combien de temps ?*
|
||||
Ces configurations de tenue ne valent ni comme jugement moral, ni comme idéal-type figé. Elles doivent être comprises comme des formes observables et évolutives. Elles ne sont pas des essences, mais des positions dans un espace de viabilité régulatoire. Et leur analyse permet, dans chaque cas empirique, de répondre à la question fondamentale : *qu’est-ce qui tient ici ? Comment ? À quel prix ? Et pour combien de temps ?*
|
||||
|
||||
Il ne s’agit donc pas de réhabiliter une typologie figée (équilibré vs déséquilibré), ni de fantasmer un modèle harmonieux. Il s’agit de fournir une cartographie critique, ancrée, falsifiable, capable de guider l’analyse empirique des dispositifs de régulation contemporaine, sans céder à l’abstraction ni au moralisme.
|
||||
Il ne s’agit donc pas de réhabiliter une typologie figée (équilibré vs déséquilibré), ni de fantasmer un modèle harmonieux.
|
||||
|
||||
Dans les sous-sections qui suivent, nous distinguerons trois grandes formes dynamiques de tenue archicratique :
|
||||
Dans les sous-sections qui suivent, nous distinguerons quatre grandes formes dynamiques de tenue archicratique :
|
||||
|
||||
1. La *forme synchrotopique* : tension vivable, différenciation claire des pôles, articulation régulée — une régulation habitable.
|
||||
La forme *synchrotopique* : tension vivable, différenciation claire des pôles, articulation régulée — une régulation habitable.
|
||||
|
||||
2. La *forme hypertopique* : domination d’un pôle archicratique, avec effets de blocage, d’asymétrie ou de dévitalisation.
|
||||
La forme *hypertopique* : domination d’un pôle archicratique, avec effets de blocage, d’asymétrie ou de dévitalisation.
|
||||
|
||||
3. La *forme entropique* : perte de liaison, saturation, effondrement de la dispute, invisibilisation des fondements, opacité des instruments.
|
||||
La forme *hypotopique* : effacements, désaffiliations, mises en latence ou désarrimages des prises régulatrices.
|
||||
|
||||
La forme *atopique* : mimétisme des pôles, vacuité des prises, spectralisation de la régulation.
|
||||
|
||||
Chacune de ces formes sera décrite avec ses critères de reconnaissance, ses symptômes internes, ses objets d’épreuve et ses points de bascule potentiels.
|
||||
|
||||
@@ -1084,15 +1061,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 +1077,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 +1103,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 +1113,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 +1127,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 +1180,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 +1199,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.
|
||||
|
||||
@@ -1270,13 +1249,13 @@ De ce point de vue, l’archicration n’est pas un protocole à décliner, mais
|
||||
|
||||
À ce stade du chapitre, le paradigme archicratique s’est exposé dans ses principes, dans ses conditions de lisibilité, d’usage et d’épreuve, dans ses *seuils critiques* (détectabilité, repérage, ouverture critique), ainsi que dans ses *ancrages différés* (scènes, seuils, temporalités). Mais il reste à en consolider la prise morphologique : non plus sous l’angle de ses conditions de possibilité générales, mais à travers l’analyse concrète de ses formes d’incarnation, de ses figures empiriques, de ses variations structurelles. Car une régulation ne se donne jamais dans le vide : elle se trame, se matérialise, se temporalise, et surtout s’expérimente. Le moment morphologique constitue donc le point de bascule entre la formulation du paradigme et sa puissance heuristique dans les régimes concrets.
|
||||
|
||||
Ce chantier ne répète en rien ce qui a été posé jusqu’ici. Il s’en distingue par son objet, sa méthode et sa finalité. La section 1.1 posait le cadre d’intelligibilité général du paradigme : la triade *arcalité-cratialité-archicration*, pensée comme structure de fondation, de mise en œuvre et de reprise d’un ordre. La section 1.4 déployait l’architecture du paradigme dans sa forme purement théorique : triptyque de prises, logique de différenciation, schéma des articulations internes. Ce que propose à présent la section 1.7, c’est une *approche stratifiée*, *située*, *matérielle* et *expérientielle* de ces trois prises, telles qu’elles apparaissent, circulent, se figent ou s’ouvrent dans les régimes de régulation réels — avec toute la diversité, la conflictualité, l’hétérogénéité et l’imperfection que cela suppose. Il ne s’agit donc plus de conceptualiser les pôles, mais d’en cartographier les formes.
|
||||
Ce chantier ne répète en rien ce qui a été posé jusqu’ici. Il s’en distingue par son objet, sa méthode et sa finalité. La section 1.1 posait le cadre d’intelligibilité général du paradigme : la triade *arcalité-cratialité-archicration*, pensée comme structure de fondation, de mise en œuvre et de reprise d’un ordre. La section 1.4 déployait l’architecture du paradigme dans sa forme purement théorique : triptyque de prises, logique de différenciation, schéma des articulations internes. Les sections 1.5 et 1.6 en ont ensuite éprouvé les formes dynamiques et dégagé les premiers repères heuristiques ; la section 1.7 peut dès lors en proposer la cartographie morphologique. Une *approche stratifiée*, *située*, *matérielle* et *expérientielle* de ces trois prises, telles qu’elles apparaissent, circulent, se figent ou s’ouvrent dans les régimes de régulation réels — avec toute la diversité, la conflictualité, l’hétérogénéité et l’imperfection que cela suppose. Il ne s’agit donc plus de conceptualiser les pôles, mais d’en cartographier les formes.
|
||||
|
||||
Car ce qui fait valeur dans un paradigme critique, ce n’est pas sa seule élégance morphologique, mais sa capacité à discerner des régularités, à documenter des bifurcations, à nommer des fractures — non pour assigner, mais pour ouvrir la possibilité d’une lecture située. Une forme d’*arcalité* n’est pas un idéal-type, mais une configuration référentielle historiquement, symboliquement et techniquement composée. Une *cratialité* ne vaut pas par sa fidélité à un protocole d’exécution, mais par l’agencement spécifique de ses opérateurs, de ses instruments, de ses temporalités d’action. Une *archicration* ne se mesure pas à l’existence nominale d’un droit au recours, mais à la consistance vécue de la scène qu’elle institue ou qu’elle empêche. C’est à ces formes multiples — *opérantes, affaiblies, désincarnées, empêchées, contournées* — que nous allons désormais nous confronter.
|
||||
|
||||
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 +1321,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 +1331,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 +1397,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.
|
||||
|
||||
@@ -1456,7 +1435,7 @@ Elle est ce qui empêche l’ordre de se refermer dans la tautologie de ses prop
|
||||
|
||||
## **Conclusion du chapitre 1 — La théorie archicratique : régulation politique, plasticité critique et scène de requalification**
|
||||
|
||||
Le paradigme archicratique ne se présente ni comme une doctrine politique parmi d’autres, ni comme une méthode normative de classement des régimes. Il n’ajoute pas une théorie supplémentaire à celles de la souveraineté, de la gouvernance ou de l’autorité. Ce qu’il propose, c’est un cadre d’intelligibilité critique, ancré dans une grammaire morphologique du politique qui ne repose ni sur des catégories fixes, ni sur une logique de fondement ultime. Il s’agit d’un *outillage rigoureux pour rendre lisibles les formes concrètes de la régulation, dans leur articulation spécifique entre arcalité, cratialité et archicration dans des contextes tout à fait situés*.
|
||||
Cette conclusion ne referme pas une doctrine ; elle resserre les conditions sous lesquelles le paradigme archicratique peut valoir comme cadre d’intelligibilité critique du politique. Il ne s’ajoute ni comme typologie supplémentaire, ni comme méthode normative de classement des régimes ; il propose un outillage morphologique pour rendre lisibles les formes concrètes de la régulation dans l’articulation située de l’arcalité, de la cratialité et de l’archicration.
|
||||
|
||||
Le paradigme archicratique n’impose pas une grille préexistante à des réalités déjà constituées ; il permet d’analyser, depuis leurs tensions propres, les manières dont ces réalités se composent, se désarticulent, se reconfigurent. En ce sens, il relève d’une dynamique transductive, au sens où chaque régulation est saisie non comme un objet figé, mais comme une configuration en devenir, traversée de forces, de seuils, de mémoires et de requalifications. La transduction, ici, ne désigne pas un raisonnement par analogie (au sens psychologique), mais un processus d’individuation progressive, au contact des tensions internes d’un système. Le paradigme ne se veut pas surplombant mais co-engendré par les régimes qu’il examine.
|
||||
|
||||
@@ -1476,7 +1455,7 @@ Dans cette perspective, l’*archicratie* devient la forme intelligible de cette
|
||||
|
||||
C’est en ce sens qu’il s’agit d’un paradigme non pas normatif, mais épistémologiquement productif. Il ne dit pas ce que doit être un bon régime : il rend lisible ce qui rend une régulation habitable, amendable, transformable — et ce qui, à l’inverse, la ferme sur ses effets, la fige dans ses gestes, la condamne à se répéter et à terme à péricliter. Il propose une forme critique non doctrinale, une manière d’identifier des seuils, de qualifier des scènes, de repérer des tensions, sans présumer a priori d’un idéal de société. En cela, il s’adresse autant à des régimes démocratiques qu’à des régulations algorithmiques, à des dispositifs communautaires qu’à des configurations bureaucratiques. C’est un instrument d’écoute morphologique, un opérateur d’exposition et de traduction entre ce qui agit et ce qui peut être corrigé.
|
||||
|
||||
Mais cette productivité critique engage aussi une exigence : si un régime se donne à lire archicratiquement, alors il accepte d’être adressé, de comparaître, d’être reconfiguré depuis ce qu’il fait. Et c’est là, précisément, que la valence politique du paradigme se précise. Car un pouvoir peut bien se proclamer légitime, équitable, nécessaire — il ne devient habitable que s’il peut être mis à l’épreuve de sa propre scène. La *scène archicrative*, au cœur du paradigme, est ce lieu où le pouvoir ne s’effondre pas quand il est exposé : il s’y rend recevable. Il y gagne en consistance ce qu’il perd en clôture. Il y devient politique non par proclamation, mais par acceptation de sa propre reformulation.
|
||||
Mais cette productivité critique engage aussi une exigence : si un régime se donne à lire archicratiquement, alors il accepte d’être adressé, de comparaître, d’être reconfiguré depuis ce qu’il fait. Et c’est là que la valence politique du paradigme se précise. Cette précision n’érige pas l’archicratie en idéal normatif préalable ; elle indique seulement le seuil à partir duquel une régulation cesse d’être purement fonctionnelle pour devenir politiquement habitable. Car un pouvoir peut bien se proclamer légitime, équitable, nécessaire — il ne devient habitable que s’il peut être mis à l’épreuve de sa propre scène. La *scène archicrative*, au cœur du paradigme, est ce lieu où le pouvoir ne s’effondre pas quand il est exposé : il s’y rend recevable. Il y gagne en consistance ce qu’il perd en clôture. Il y devient politique non par proclamation, mais par acceptation de sa propre reformulation.
|
||||
|
||||
Il fallait dès lors que cette modélisation ne se contente pas d’une intuition politique, d’un geste polémique, ou d’un effet d’annonce critique. Elle devait pouvoir fonder une validité épistémologique solide, à même de garantir que la grille archicratique ne dérive ni en système doctrinaire, ni en méthodologie molle. C’est à cette exigence que ce premier chapitre a voulu répondre, en établissant les conditions épistémologiques, morphologiques et fonctionnelles d’un paradigme rigoureux — autrement dit : sa possibilité scientifique, sa cohérence structurale et son opérabilité critique.
|
||||
|
||||
@@ -1518,8 +1497,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é.
|
||||
@@ -12,6 +12,7 @@ source:
|
||||
kind: docx
|
||||
path: "sources/docx/archicrat-ia/Chapitre_2–Archeogenese_des_regimes_de_co-viabilite-version_officielle.docx"
|
||||
---
|
||||
|
||||
À ce stade de notre essai, l’*archicratie* a été définie, formalisée et modélisée comme une *structure dynamique de co-viabilité tensionnelle*, articulant trois pôles fondamentaux : l’*arcalité* (ce qui fonde), la *cratialité* (ce qui opère) et l’*archicration* (ce qui régule). Le chapitre 1 a montré que l’*archicratie* n’est pas une fiction idéologique ni une utopie normative, mais un système intelligible, repérable, heuristique et herméneutique. Ce système, avons-nous démontré, constitue un *régime d’intelligibilité politique générale*, susceptible d’unifier les lectures hétérogènes du pouvoir, de la gouvernance, de l’organisation sociale et de la dynamique historique.
|
||||
|
||||
Mais toute théorie du politique qui se veut universelle doit, pour être complète, affronter le sol rugueux de l’empirique. C’est précisément ce que vise ce deuxième chapitre. Ici, le centre de gravité se déplace de l’épistémologique vers l’historique, du théorique vers l’archéologique, de l’abstraction vers la sédimentation. Il s’agit de retracer, sur la longue durée, la manière dont les sociétés humaines ont historiquement construit, adopté, incarné, ritualisé, sacralisé, scripturalisé, technicisé et algorithmisé leurs dispositifs de régulation.
|
||||
@@ -64,19 +65,19 @@ Il ne s’agit pas de reconstruire une chronologie historique, mais de mettre au
|
||||
|
||||
Nous allons donc développer une typologie en cinq profils de régulation archicratique, chacun incarnant une logique d’agencement particulière entre les trois vecteurs fondamentaux du pouvoir régulateur. Ces cinq formes sont :
|
||||
|
||||
- L’*archicratie proto-symbolique*, caractéristique des sociétés paléolithiques ou dites « à mémoire vive », où la co‑viabilité repose sur l’incorporation rituelle, la mémoire affective et la structuration mimétique des appartenances ;
|
||||
- L’archicratie proto-symbolique, caractéristique des sociétés paléolithiques ou dites « à mémoire vive », où la co-viabilité repose sur l’incorporation rituelle, la mémoire affective et la structuration mimétique des appartenances ;
|
||||
|
||||
|
||||
|
||||
- L’*archicratie sacro‑institutionnelle*, propre aux sociétés religieuses ou théocratiques, dans lesquelles l’invisible structure le visible et où l’autorité se différencie radicalement de la souveraineté ;
|
||||
- L’archicratie sacro-institutionnelle, propre aux sociétés religieuses ou théocratiques, dans lesquelles l’invisible structure le visible et où l’autorité se différencie radicalement de la souveraineté ;
|
||||
|
||||
|
||||
|
||||
- L’*archicratie techno‑logistique*, fondée sur l’idée de mégamachine, dans laquelle la coordination impersonnelle précède le commandement et où les flux prennent le pas sur les figures ;
|
||||
- L’archicratie techno-logistique, fondé sur l’idée de mégamachine, dans laquelle la coordination impersonnelle précède le commandement et où les flux prennent le pas sur les figures ;
|
||||
|
||||
|
||||
|
||||
- L’*archicratie scripturo‑normative*, qui institue la norme dans l’écrit, fait de l’archive un vecteur d’autorité différée, et de la procédure un opérateur de légitimation ;
|
||||
- L’archicratie scripturo-normative, qui institue la norme dans l’écrit, fait de l’archive un vecteur d’autorité différée, et de la procédure un opérateur de légitimation ;
|
||||
|
||||
|
||||
|
||||
@@ -88,11 +89,11 @@ Nous allons donc développer une typologie en cinq profils de régulation archic
|
||||
|
||||
|
||||
|
||||
- L’*archicratie historiographique*, qui fonde la légitimité sur l’activation d’un récit collectif institué, consigné dans des textes‑repères, et réactualisé par des protocoles publics de lecture, de commémoration ou de transmission. L’ordre y repose sur la fidélité narrative à une mémoire partagée, toujours réécrite et rituellement réactivée ;
|
||||
- L’archicratie historiographique, qui fonde la légitimité sur l’activation d’un récit collectif institué, consigné dans des textes-repères, et réactualisé par des protocoles publics de lecture, de commémoration ou de transmission. L’ordre y repose sur la fidélité narrative à une mémoire partagée, toujours réécrite et rituellement réactivée ;
|
||||
|
||||
|
||||
|
||||
- L’*archicratie épistémique*, dans laquelle l’autorité procède de la preuve, de la démonstration et de la formalisation objective, et où la co‑viabilité se construit par la validation critique, la reproductibilité et la neutralisation des points de vue dans un espace de raison partagée ;
|
||||
- L’archicratie épistémique, dans laquelle l’autorité procède de la preuve, de la démonstration et de la formalisation objective, et où la co-viabilité se construit par la validation critique, la reproductibilité et la neutralisation des points de vue dans un espace de raison partagée ;
|
||||
|
||||
|
||||
|
||||
@@ -120,7 +121,7 @@ Ce que nous appelons ici *régulation proto-symbolique* ne désigne pas une phas
|
||||
|
||||
Il s’agit ici de montrer, à partir de données archéologiques certifiées et d’analyses anthropologiques convergentes, que ces sociétés ont su distribuer des formes et des forces dans des configurations stables, différenciées et évolutives. L’*archicratie*, dans ce cadre, constitue le politique en régime d’apparition, dès lors que le collectif humain suspend la violence, institue un seuil et met en forme ce qui pourrait dissoudre le lien. C’est cette grammaire que nous allons maintenant déplier dans quelques figures concrètes, ancrées dans les sols, les corps et les signes du Paléolithique.
|
||||
|
||||
Les *sépultures* paléolithiques, au-delà de représenter des gestes d’inhumation, sont les premières scènes différées où la communauté s’éprouve elle‑même comme sujet régulé. Là où l’animal abandonne ou dissimule la dépouille, l’humain préhistorique institue un lieu, un temps, un geste : il fait du corps éteint un point de passage, un opérateur d’ordre. Les archéologues qui ont rouvert ces tombes ont trouvé la trace d’une grammaire structurée de la mémoire, où se tissent les trois prises de l’*archicratie* : le fondement (*arcalité*), la puissance agissante (*cratialité*) et la mise en scène (*archicration*).
|
||||
Les sépultures paléolithiques, au-delà de représenter des gestes d’inhumation, sont les premières scènes différées où la communauté s’éprouve elle-même comme sujet régulé. Là où l’animal abandonne ou dissimule la dépouille, l’humain préhistorique institue un lieu, un temps, un geste : il fait du corps éteint un point de passage, un opérateur d’ordre. Les archéologues qui ont rouvert ces tombes ont trouvé la trace d’une grammaire structurée de la mémoire, où se tissent les trois prises de l’archicratie : le fondement (arcalité), la puissance agissante (cratialité) et la mise en scène (archicration).
|
||||
|
||||
Découvert en 1955 à l’est de Moscou, le site de Sungir (culture gravettienne) a livré deux sépultures exceptionnelles : un adulte et deux enfants âgés d’environ dix ans, inhumés tête‑bêche, leurs corps couverts de plus de 10 000 perles d’ivoire de mammouth finement taillées, accompagnées de pointes en os, de disques et de bracelets (Trinkaus & Buzhilova, 2018). Les analyses isotopiques ont montré qu’un même groupe d’individus a consacré des centaines d’heures à la fabrication des parures caractérisant un travail collectif différé, un investissement rituel sans finalité utilitaire.
|
||||
|
||||
@@ -130,7 +131,7 @@ En termes archicratiques, Sungir manifeste une *arcalité saturée* de signes
|
||||
|
||||
En Moravie, à Dolní Věstonice, une triple sépulture datée de 28 000 ans (Svoboda, 2015) présente trois jeunes individus déposés côte à côte, orientés vers l’est, l’un partiellement recouvert d’ocre rouge, les mains croisées sur le bassin. Autour, un vaste habitat de huttes semi‑souterraines, des fours d’argile, et des figurines animales et humaines — dont la célèbre Vénus en terre cuite, l’une des plus anciennes céramiques connues.
|
||||
|
||||
Olga Soffer y lit une « invention de la symbolisation partagée » : la céramique — objet de régulation — est un moyen de fixer, de différer et de transformer le geste collectif. Dans cette configuration, la *cratialité* se manifeste dans la fabrication (la main, la chaleur, la matière), l’*arcalité* dans la codification du lieu et des orientations, l’*archicration* dans la cérémonie funéraire elle‑même, qui lie le feu, l’ocre et le corps pour donner forme à la perte.
|
||||
Olga Soffer y lit une « invention de la symbolisation partagée » : la céramique — objet de régulation — est un moyen de fixer, de différer et de transformer le geste collectif. Dans cette configuration, la cratialité se manifeste dans la fabrication (la main, la chaleur, la matière), l’arcalité dans la codification du lieu et des orientations, l’archicration dans la cérémonie funéraire elle-même, qui lie le feu, l’ocre et le corps pour donner forme à la perte.
|
||||
|
||||
Alain Testart (*Critique du don*, 2007, p. 83‑90), quant à lui, rappelle que l’ocre et le feu appartiennent au même champ symbolique de la transformation : « on ne rend pas la vie, on la traduit ». Ici, la traduction devient scène de régulation, où la société se confronte à la finitude en la modulant rituellement.
|
||||
|
||||
@@ -359,7 +360,7 @@ Ce qui nous intéresse alors, c’est leur puissance de modulation du lien, leur
|
||||
|
||||
En cela, ces méta-régimes sacrés sans État nous lèguent une leçon politique d’une rare intensité : l’ordre n’est pas toujours là où on croit le voir. Il peut résider dans ce qui ne se montre pas, dans ce qui ne se dit pas, dans ce qui ne se décide pas — mais qui se manifeste, se transmet, se déploie à travers une constellation rituelle de formes, de rythmes, de paroles et de silences partagés. Et c’est cette grammaire spécifique que nous reconnaissons aussi dans notre paradigme comme *archicratique* : un pouvoir qui peut se donner sans accaparement, qui peut régler sans règne et qui peut moduler sans imposition.
|
||||
|
||||
Cette compréhension est absolument capitale pour notre théorie : elle montre que la mise en ordre rituelle, différée, symbolique — sans instance souveraine — est historiquement attestée et empiriquement documentée. Elle éclaire, par contraste, les dispositifs contemporains de régulation distribuée — algorithmes, consensus décentralisés, structures non-hiérarchiques — qui, sans en reproduire la forme sacrale, en partagent une proximité structurelle : une orchestration de la *co‑viabilité* sans pouvoir constitué et centralisé.
|
||||
Cette compréhension est absolument capitale pour notre théorie : elle montre que la mise en ordre rituelle, différée, symbolique — sans instance souveraine — est historiquement attestée et empiriquement documentée. Elle éclaire, par contraste, les dispositifs contemporains de régulation distribuée — algorithmes, consensus décentralisés, structures non-hiérarchiques — qui, sans en reproduire la forme sacrale, en partagent une proximité structurelle : une orchestration de la co-viabilité sans pouvoir constitué et centralisé.
|
||||
|
||||
Tableau de synthèse — régulation sacrée non-étatique
|
||||
|
||||
@@ -521,7 +522,7 @@ Tableau de synthèse — *Mégamachines : régulation techno-logistique*
|
||||
| **Standards métriques et calendaires** (unités de poids, de volume, calendriers) | Harmonisation des flux et des cycles | Archicration métrique et cyclique | Mesure comme opérateur d’universalité logistique | Temporisation des gestes, régulation des cadences | Compatibilité des rythmes, régularité des échanges, fluidité opératoire |
|
||||
| **Agencements opératoires** (plans urbains, séquençage des tâches, structures productives) | Séquençage et optimisation des fonctions sociales | Archicration procédurale par compatibilité structurelle | Organisation géométrique des espaces, circulation modulée | Répartition des efforts, cadencement des énergies | Co-viabilité par interopérabilité des segments et synchronisation des gestes |
|
||||
|
||||
Ce tableau synthétique met en lumière, la spécificité du régime techno-logistique en tant que forme historique pleinement archicratique. Chaque composante fonctionnelle y est à la fois opérateur de d’*archicration*, vecteur d’*arcalité* ou de *cratialité*, et inducteur d’une forme de *co-viabilité* calibrée, extensible et durable. Ce qui frappe ici, c’est que la régulation ne réside plus dans une figure, un récit ou un interdit, mais dans l’*agencement fonctionnel des dispositifs* — des murs, des outils, des axes, des calculs, des activités — dont l’*efficacité performative* suffit à assurer la pérennité du lien collectif. En cela, la *mégamachine* cristallise une mutation ontopolitique majeure puisque la régulation opère ici par infrastructure, par ordre distribué et par opérationnalité. Ce modèle constitue un archétype régulateur autonome, dont la fécondité heuristique sera déterminante pour la suite de notre enquête archicratique.
|
||||
Ce tableau synthétique met en lumière, la spécificité du régime techno-logistique en tant que forme historique pleinement archicratique. Chaque composante fonctionnelle y est à la fois opérateur d’archicration, vecteur d’arcalité ou de cratialité, et inducteur d’une forme de co-viabilité calibrée, extensible et durable. Ce qui frappe ici, c’est que la régulation ne réside plus dans une figure, un récit ou un interdit, mais dans l’agencement fonctionnel des dispositifs — des murs, des outils, des axes, des calculs, des activités — dont l’efficacité performative suffit à assurer la pérennité du lien collectif. En cela, la mégamachine cristallise une mutation ontopolitique majeure puisque la régulation opère ici par infrastructure, par ordre distribué et par opérationnalité. Ce modèle constitue un archétype régulateur autonome, dont la fécondité heuristique sera déterminante pour la suite de notre enquête archicratique.
|
||||
|
||||
### 2.2.4 — *Archicrations scripturo-normatives*
|
||||
|
||||
@@ -623,7 +624,7 @@ Cela signifie que la vérité régulatrice repose sur une configuration et une c
|
||||
|
||||
C’est cette temporalité non linéaire stratifiée qui permet au système d’absorber les conflits sans rupture systémique : à tout litige peut correspondre une trace antérieure à requalifier, un précédent à convoquer ou un statut à préciser. Les énoncés deviennent ainsi matrices de référence et de continuité normative, non pas parce qu’ils imposent, mais parce qu’ils permettent la reprise, la comparaison et la relance sous couvert d’égalité formelle de traitement.
|
||||
|
||||
Cette capacité d’adaptation, de souplesse et de requalification confère à la régulation *scripturo-normative* une forme de plasticité procédurale : il peut fonctionner dans des contextes à faible centralisation étatique, dans des systèmes plusieurs statuts, ou même dans des structures palatiales partiellement disjointes. Il suffit qu’existe une infrastructure d’écriture, de classement et d’activation pour que la *co-viabilité* soit maintenue. Et de fait, comme le prouvent les travaux récents de Piotr Michalowski (*The Correspondence of the Kings of Ur*, 2006), certaines cités sumériennes parvenaient à articuler des réseaux de reconnaissance sans administration hiérarchique forte, mais avec une chaîne de tablettes, de messagers et d’archives interreliées.
|
||||
Cette capacité d’adaptation, de souplesse et de requalification confère à la régulation *scripturo-normative* une forme de plasticité procédurale : il peut fonctionner dans des contextes à faible centralisation étatique, dans des systèmes à plusieurs statuts, ou même dans des structures palatiales partiellement disjointes. Il suffit qu’existe une infrastructure d’écriture, de classement et d’activation pour que la *co-viabilité* soit maintenue. Et de fait, comme le prouvent les travaux récents de Piotr Michalowski (*The Correspondence of the Kings of Ur*, 2006), certaines cités sumériennes parvenaient à articuler des réseaux de reconnaissance sans administration hiérarchique forte, mais avec une chaîne de tablettes, de messagers et d’archives interreliées.
|
||||
|
||||
Ce régime ne suppose donc pas une verticalité absolue. Il peut opérer latéralement, par circulation des documents, par interconnexion des archives, par activation circonstanciée des écrits. Il s’agit d’une architecture de reconnaissance, dont les piliers sont des fragments de texte, des sceaux, des signatures, des clauses, des lieux de dépôt. Ce que la *co-viabilité* gagne ici, c’est une forme de stabilité dans l’ajustement différé, une régulation qui n’impose pas d’uniformité, mais qui encadre la pluralité des situations dans un maillage d’inscriptions mobilisables.
|
||||
|
||||
@@ -1045,15 +1046,15 @@ Cette ambivalence entre narration structurante et analyse réflexive s’incarne
|
||||
|
||||
Cette méthode, fondée sur l’enquête (*historia*), ne relève pas encore de la critique moderne, mais elle constitue un moment décisif de l’*archicration* : en confrontant plusieurs récits, Hérodote produit un lieu de régulation où ce qui peut être retenu comme histoire devient objet de débat, de composition, voire de hiérarchisation. Il ne s’agit pas seulement de transmettre un récit fondateur, mais de rendre possible une régulation par confrontation de versions. En cela, Hérodote opère à la jonction entre l’activation de figures exemplaires (le courage des Spartiates, la ruse d’Ulysse) et l’*archicration* des récits divergents (sur les origines du conflit, la nature du pouvoir perse ou les mœurs des Scythes).
|
||||
|
||||
Dans tous ces cas, l’archicration historiographique ne se confond ni avec la censure, ni avec un acte d’écriture. Elle suppose un dispositif de confrontation, un lieu de régulation, un arbitrage — explicite ou implicite — des formes narratives reconnues comme valides pour assurer la *co-viabilité* politique. celle-ci se donne à voir dans des lieux précis : la chancellerie où l’on compile les chroniques, la cour impériale où l’on statue sur les versions divergentes d’une conquête, l’académie où l’on débat des filiations légitimes, l’assemblée où l’on statue parfois sur ce qui doit être enseigné comme vérité officielle. Ces espaces sont des institutions de mémoire autorisée.
|
||||
Dans tous ces cas, l’archicration historiographique ne se confond ni avec la censure, ni avec un acte d’écriture. Elle suppose un dispositif de confrontation, un lieu de régulation, un arbitrage — explicite ou implicite — des formes narratives reconnues comme valides pour assurer la co-viabilité politique. Celle-ci se donne à voir dans des lieux précis : la chancellerie où l’on compile les chroniques, la cour impériale où l’on statue sur les versions divergentes d’une conquête, l’académie où l’on débat des filiations légitimes, l’assemblée où l’on statue parfois sur ce qui doit être enseigné comme vérité officielle. Ces espaces sont des institutions de mémoire autorisée.
|
||||
|
||||
Cette histoire politique devient alors une sorte de grammaire en acte : on n’oblige pas en décrétant, mais en décidant collectivement quel passé fait autorité. Pour autant il serait erroné de réduire la fonction archicratique à la simple réactivation rituelle du passé. La normativité narrative naît de l’épreuve du débat régulateur. En effet, l’histoire ne contraint qu’en tant qu’elle a été validée par une instance — formelle ou diffuse — de mise en cohérence, d’arbitrage et de filtration mémorielle. L’*archicration historiographique* ne consiste donc pas à répéter, mais à décider ce qui, dans le passé raconté, continue de fonder la possibilité du vivre ensemble.
|
||||
Cette histoire politique devient alors une sorte de grammaire en acte : on n’oblige pas en décrétant, mais en décidant collectivement quel passé fait autorité. Pour autant, il serait erroné de réduire la fonction archicratique à la simple réactivation rituelle du passé. La normativité narrative naît de l’épreuve du débat régulateur. En effet, l’histoire ne contraint qu’en tant qu’elle a été validée par une instance — formelle ou diffuse — de mise en cohérence, d’arbitrage et de filtration mémorielle. L’*archicration historiographique* ne consiste donc pas à répéter, mais à décider ce qui, dans le passé raconté, continue de fonder la possibilité du vivre ensemble.
|
||||
|
||||
Dans le méta-régime historiographique, l’autorité se perpétue surtout par une grammaire implicite de la légitimité, progressivement intériorisée par les figures de pouvoir, les élites dirigeantes, les héritiers du trône ou les gardiens de l’ordre social. Cette transmission relève d’une pédagogie de la souveraineté et d’une logique aristocratique : on apprend à gouverner en lisant l’histoire, on s’oriente dans le présent en relisant les gestes des fondateurs, on se légitime en s’inscrivant dans la lignée des précédents perçus ou reconnus comme exemplaires.
|
||||
|
||||
Ce régime de régulation repose sur une codification implicite, dans laquelle le texte historique fonctionne comme miroir normatif — pour orienter, calibrer, formater les attentes liées à l’exercice du pouvoir. C’est pourquoi, dans de nombreuses traditions politiques, l’apprentissage du pouvoir passe par la lecture d’œuvres historiographiques tenues pour canoniques, organisées non comme savoirs passés, mais comme réservoirs de modèles opératoires à mobiliser.
|
||||
|
||||
Cette transmission n’emprunte pas uniquement les voies du texte écrit ou de l’enseignement formel : elle peut aussi s’incarner dans des supports iconiques, rituels ou performatifs, qui assurent la continuité normative par d’autres formes de répétition régulatrice. Telles peuvent être le cas de fresques murales, stèles commémoratives, bas-reliefs dynastiques, chants narratifs ou cycles iconographiques. Ces dispositifs, bien qu’extra-textuels, structurent eux aussi des trames de légitimation fondées sur la mémoire séquencée. Ils contribuent à stabiliser visuellement ou rituellement les figures exemplaires, en les insérant publiquement dans une scénographie permanente du pouvoir hérité. En cela, ils participent pleinement de la *cratialité historiographique*, et parfois de son activation archicratique, dès lors que ces images ou récits chantés sont sélectionnés, ritualisés ou modifiés dans un cadre d’arbitrage politique.
|
||||
Cette transmission n’emprunte pas uniquement les voies du texte écrit ou de l’enseignement formel : elle peut aussi s’incarner dans des supports iconiques, rituels ou performatifs, qui assurent la continuité normative par d’autres formes de répétition régulatrice. Tel peut être le cas de fresques murales, stèles commémoratives, bas-reliefs dynastiques, chants narratifs ou cycles iconographiques. Ces dispositifs, bien qu’extra-textuels, structurent eux aussi des trames de légitimation fondées sur la mémoire séquencée. Ils contribuent à stabiliser visuellement ou rituellement les figures exemplaires, en les insérant publiquement dans une scénographie permanente du pouvoir hérité. En cela, ils participent pleinement de la *cratialité historiographique*, et parfois de son activation archicratique, dès lors que ces images ou récits chantés sont sélectionnés, ritualisés ou modifiés dans un cadre d’arbitrage politique.
|
||||
|
||||
C’est pourquoi de nombreuses cultures recourent à des formes non textuelles d’historiographie — fresques murales, stèles commémoratives, chants épiques ou cycles iconographiques — pour insérer les figures exemplaires dans une scénographie publique du pouvoir hérité. Ces dispositifs visuels ou oraux ne transmettent pas simplement un souvenir : ils stabilisent, incarnent, et ritualisent la légitimité par la répétition figurative.
|
||||
|
||||
@@ -1174,7 +1175,7 @@ C’est pourquoi, à son niveau le plus abstrait, l’*arcalité épistémique*
|
||||
|
||||
Comme l’écrivait Foucault dans *Les mots et les choses*, ce savoir est référentiel sans transcendance : il fonctionne par agencements, grilles, séries, catégories, sans appel à une révélation, un principe supérieur ou un ordre sacré. Il construit un régime d’objectivité qui fait obligation par sa structure, non par son origine.
|
||||
|
||||
Il convient toutefois de noter que certains régimes de transmission orale, bien que rattachés à des méta-régimes théologiques ou cosmologiques (comme la récitation védique, les hadiths ou les canons bouddhiques), manifestent des degrés très élevés de formalisation cognitive : chaînes de validation orale, protocoles mnémotechniques, systèmes de correction. Ces dispositifs ne relèvent pas directement de l’*archicration épistémique*, mais en partagent certaines structures formelles : ils construisent des matrices d’ordre tout aussi rigoureuses, assurant une co‑viabilité cognitive sans inscription graphique. La parole y est régulée par des protocoles mnémotechniques, des chaînes de validation orale et des systèmes de correction collective. Ce sont là des formes d’*arcalité* formelle sans écriture, où la parole elle‑même devient structure.
|
||||
Il convient toutefois de noter que certains régimes de transmission orale, bien que rattachés à des méta-régimes théologiques ou cosmologiques (comme la récitation védique, les hadiths ou les canons bouddhiques), manifestent des degrés très élevés de formalisation cognitive : chaînes de validation orale, protocoles mnémotechniques, systèmes de correction. Ces dispositifs ne relèvent pas directement de l’archicration épistémique, mais en partagent certaines structures formelles : ils construisent des matrices d’ordre tout aussi rigoureuses, assurant une co-viabilité cognitive sans inscription graphique. La parole y est régulée par des protocoles mnémotechniques, des chaînes de validation orale et des systèmes de correction collective. Ce sont là des formes d’arcalité formelle sans écriture, où la parole elle-même devient structure.
|
||||
|
||||
En cela, le savoir institué constitue une source d’ordre, à la manière d’un cosmos intellectuel. Il ne renvoie pas à un Dieu, à une Nature ou à une Loi, mais à une grammaire interne de cohérence, qui oblige parce qu’elle est reçue comme logique, opératoire et reproductible.
|
||||
|
||||
@@ -1218,7 +1219,7 @@ En Inde ancienne, un même processus de filtration s’opère à travers les éc
|
||||
|
||||
Une autre forme d’*archicration épistémique* consiste dans l’opération de clôture canonique, c’est-à-dire la définition d’un corpus considéré comme recevable — par exclusion de ce qui est jugé erroné, apocryphe, ou non conforme. C’est ici que se joue le cœur normatif du régime : non pas transmettre tout ce qui est su, mais décider de ce qui mérite de l’être et d’exclure le reste.
|
||||
|
||||
L’exemple de l’Académie de Platon (fondée vers 387 av. J.-C.) est, à cet égard, éclairant. Les dialogues de Platon ne forment pas à eux seuls un système, mais c’est au sein de l’Académie que va s’élaborer, par reprises, commentaires et enseignements, un canon de lectures platoniciennes, au détriment d’autres traditions concurrentes (pythagoricienne, sophistique, cynique). Ce que nous considérons aujourd’hui comme la « pensée platonicienne » est donc déjà un résultat archicratique : une sélection stabilisée, enseignée, réinterprétée dans un cadre d’autorité. Le savoir devient ainsi le lieu d’un arbitrage régulateur collectif, assurant la *co‑viabilité* cognitive des sociétés savantes.
|
||||
L’exemple de l’Académie de Platon (fondée vers 387 av. J.-C.) est, à cet égard, éclairant. Les dialogues de Platon ne forment pas à eux seuls un système, mais c’est au sein de l’Académie que va s’élaborer, par reprises, commentaires et enseignements, un canon de lectures platoniciennes, au détriment d’autres traditions concurrentes (pythagoricienne, sophistique, cynique). Ce que nous considérons aujourd’hui comme la « pensée platonicienne » est donc déjà un résultat archicratique : une sélection stabilisée, enseignée, réinterprétée dans un cadre d’autorité. Le savoir devient ainsi le lieu d’un arbitrage régulateur collectif, assurant la co-viabilité cognitive des sociétés savantes.
|
||||
|
||||
Plus tard, au XIIIe siècle, l’université de Paris fournit un autre exemple majeur. Comme l’a montré Alain de Libera (*La philosophie médiévale*, 1993), le corpus aristotélicien avait alors fait l’objet d’une sélection et d’une régulation dans les facultés des arts et de théologie. Certains textes étaient interdits à l’enseignement, d’autres réservés à des commentaires précis. Le pouvoir ne s’exerce plus ici en interdisant l’accès aux livres, mais en fixant les modalités acceptables de leur usage — et donc, en orientant leur pouvoir régulateur.
|
||||
|
||||
@@ -1305,7 +1306,7 @@ Le régime épistémique moderne trouve son prolongement dans les formes contemp
|
||||
| **Appareils de validation** | Filtrer et formaliser la légitimité des savoirs transmis | *Archicration* de la reconnaissance (canon, exclusion, agrément) | Constitution de corpus clos, reconnaissance institutionnelle du vrai enseignable | Production de seuils cognitifs (examens, autorisations, habilitations) | Définition institutionnelle du dicible cognitif |
|
||||
| **Obligation cognitive** | Créer une forme d’adhésion sans coercition | Archicration par standardisation des formats de vérité | Obligation par structure (cohérence démonstrative, opérabilité logique) | Contrainte douce par régularité formelle et reproductibilité | Cohésion par alignement intellectuel, non par commandement explicite |
|
||||
|
||||
L’*archicration épistémique* s’impose comme un régime autonome de régulation, où l’obligation ne procède ni d’un commandement transcendant, ni d’une mémoire collective, ni d’un récit dynastique, mais de la structuration formalisée du pensable. L’*arcalité* y repose sur l’objectivation du réel dans des corpus cohérents et transmissibles ; la *cratialité* s’y exerce par la maîtrise technique de ces systèmes par des figures lettrées, experts, scribes ou maîtres ; l’*archicration*, enfin, s’y manifeste à travers les dispositifs collectifs de certification, de canonisation, de sélection des interprètes autorisés et de filtrage des corpus valides. Ce régime fonde une *co-viabilité cognitive* non pas en prescrivant des comportements, mais en stabilisant des opérations mentales partagées. L’ordre y devient synonyme d’intelligibilité. En retour, cette co‑viabilité cognitive conditionne la co‑viabilité sociale puisque la société se maintient dans la mesure où ses acteurs partagent un langage commun de preuves, d’arguments et de raisons.
|
||||
L’archicration épistémique s’impose comme un régime autonome de régulation, où l’obligation ne procède ni d’un commandement transcendant, ni d’une mémoire collective, ni d’un récit dynastique, mais de la structuration formalisée du pensable. L’arcalité y repose sur l’objectivation du réel dans des corpus cohérents et transmissibles ; la cratialité s’y exerce par la maîtrise technique de ces systèmes par des figures lettrées, experts, scribes ou maîtres ; l’archicration, enfin, s’y manifeste à travers les dispositifs collectifs de certification, de canonisation, de sélection des interprètes autorisés et de filtrage des corpus valides. Ce régime fonde une co-viabilité cognitive non pas en prescrivant des comportements, mais en stabilisant des opérations mentales partagées. L’ordre y devient synonyme d’intelligibilité. En retour, cette co-viabilité cognitive conditionne la co-viabilité sociale puisque la société se maintient dans la mesure où ses acteurs partagent un langage commun de preuves, d’arguments et de raisons.
|
||||
|
||||
Il existe un dernier méta-régime, tout aussi structurant, à traiter : l’*archicration esthético-symbolique*. Ici, l’efficacité régulatrice ne réside ni dans l’explication, ni dans la déduction, mais dans la capacité de certaines formes — visuelles, rythmiques, architecturales, stylistiques — à produire de l’adhésion, de l’évidence ou de l’ajustement. Celui-ci repose sur une économie des formes sensibles, où la régulation opère par saturation symbolique, par allégeance stylistique, par alignement affectif et émotionnel. Il ne se contente pas de transmettre un contenu : il façonne une atmosphère normative.
|
||||
|
||||
@@ -1329,7 +1330,7 @@ Un phénomène similaire, mais plus élaboré, se retrouve dans l’usage des ti
|
||||
|
||||
Cette *arcalité* par la forme sensible s’incarne également dans les textiles à motifs normés, dans les cultures andines de Chavín, Paracas ou Nasca (900–200 av. J.-C.), où ils remplissaient une fonction de signalisation statutaire. Le vêtement y devient code social : chaque figure — zigzags, losanges, condors stylisés — n’est pas un simple ornement, mais une balise de visibilité sociale, comme l’a démontré Anne Paul dans ses travaux sur les textiles Paracas. La reconnaissance ne passe pas par l’individu, mais par l’agencement formel du tissu. L’adhésion naît du port du motif légitime : ce n’est pas le sens qui impose la forme, mais la conformité de la forme qui engendre la reconnaissance.
|
||||
|
||||
Cette logique *esthético-symbolique* s’incarne également dans les gestes stylisés, les postures codifiées, les chorégraphies ritualisées. Dans de nombreuses sociétés anciennes, les danses collectives, les processions réglées ou les attitudes corporelles prescrites — comme le port du buste incliné, la marche hiérarchisée ou l’étirement des bras en croix — ne traduisent pas une habitude spontanée, mais un dispositif de régulation symbolique par l’ajustement rythmique des corps. On en trouve des attestations dans les frises cérémonielles de la vallée de l’Indus, dans les fresques de Beni Hasan (Égypte, Moyen Empire), ou dans les rituels dansés des sociétés andines. Le geste devient ici forme instituée, et la posture elle-même, une syntaxe silencieuse de la hiérarchie ou de la convenance. Il ne s’agit pas d’une chorégraphie libre, mais d’un langage gestuel chargé de régulation symbolique. Et cela, bien souvent, sans que les participants en aient une conscience articulée : les postures s’apprennent par immersion, les rythmes s’incorporent par mimétisme, et la justesse du geste se mesure moins à l’intention qu’à l’aisance à s’y conformer. La *co‑viabilité* naît alors de cette capacité collective à reproduire des intensités collectivement synchronisées, inscrites dans une grammaire sensorielle incorporée.
|
||||
Cette logique esthético-symbolique s’incarne également dans les gestes stylisés, les postures codifiées, les chorégraphies ritualisées. Dans de nombreuses sociétés anciennes, les danses collectives, les processions réglées ou les attitudes corporelles prescrites — comme le port du buste incliné, la marche hiérarchisée ou l’étirement des bras en croix — ne traduisent pas une habitude spontanée, mais un dispositif de régulation symbolique par l’ajustement rythmique des corps. On en trouve des attestations dans les frises cérémonielles de la vallée de l’Indus, dans les fresques de Beni Hasan (Égypte, Moyen Empire), ou dans les rituels dansés des sociétés andines. Le geste devient ici forme instituée, et la posture elle-même, une syntaxe silencieuse de la hiérarchie ou de la convenance. Il ne s’agit pas d’une chorégraphie libre, mais d’un langage gestuel chargé de régulation symbolique. Et cela, bien souvent, sans que les participants en aient une conscience articulée : les postures s’apprennent par immersion, les rythmes s’incorporent par mimétisme, et la justesse du geste se mesure moins à l’intention qu’à l’aisance à s’y conformer. La co-viabilité naît alors de cette capacité collective à reproduire des intensités collectivement synchronisées, inscrites dans une grammaire sensorielle incorporée.
|
||||
|
||||
L’*arcalité esthético-symbolique* agit aussi par évidence partagée de la forme stabilisée. Le tracé récurrent, l’agencement bordant, la répétition chorégraphique, la gradation chromatique — tous participent d’un monde ordonné par la forme, où le visible devient vecteur de légitimité. Comme le souligne l’anthropologue britannique Tim Ingold, dans *Lines: A Brief History* (2007), les lignes que les sociétés tracent, dans l’espace, sur les objets ou sur les corps, ne délimitent pas seulement des zones : elles organisent les comportements. Elles prescrivent sans énoncer, elles disposent sans contraindre. Ces lignes ne sont pas uniquement des repères : elles activent une normativité perceptive qui conditionne la reconnaissance conjointe du convenable. Ce qui est perçu comme correct ne passe pas par le jugement, mais par l’ajustement du corps à une morphologie autorisée.
|
||||
|
||||
@@ -2776,7 +2777,7 @@ Si l’infrastructure machinique forme le soubassement matériel du méta-régim
|
||||
|
||||
C’est ici qu’il nous faut reconfigurer le concept même d’*archicration*. Dans ses formes traditionnelles — juridiques, théologiques, bureaucratiques — la régulation procédait par inscription : lois, normes, rites, règles explicites, lisibles, opposables. L’*archicration téra-machinique*, elle, procède par anticipation silencieuse : *elle module au lieu de normer, elle prévient au lieu de contraindre*. Dans ce régime, le pouvoir n’a plus besoin d’interdire : il rend improbable, il redirige l’attention, il pré-filtre les possibles. Il n’émet pas de signal fort : il *corrige en amont*, par suggestion, par structuration de l’environnement. Ce que Deleuze, dans son célèbre *Post-scriptum sur les sociétés de contrôle* (1990), anticipait déjà comme la substitution du *moulage* disciplinaire par la *modulation* *continue* trouve ici son actualisation et sa plénitude. La modulation devient la forme propre de l’*archicration prédictive*, *mais en plus d’être continue, elle en devient discrète*.
|
||||
|
||||
Les *algorithmes prédictifs* — qu’ils s’appliquent à la gestion des flux logistiques, à l’attribution de crédits bancaires, à la surveillance policière prédictive, à la prescription culturelle ou aux plateformes de recrutement — n’imposent pas, mais *orchestrent*. Ils régulent non pas par la règle, mais par le poids statistique du comportement probable. C’est là que réside la spécificité du pouvoir machinique : il repose sur l’agrégation des passés, sur la projection automatisée de futurs, sur l’optimisation probabiliste des présents. Cette orchestration prédictive n’est pas que politique ou cognitive : elle est également financière. Dans les méta-régimes *cybernético-calculatoires* contemporains, les algorithmes prédictifs s’insèrent dans des circuits d’anticipation économique — marchés de la donnée, monétisation du trafic attentionnel, rentabilisation des trajectoires comportementales. Ce que Shoshana Zuboff a nommé le *capitalisme de surveillance* (2019) n’est pas simplement un modèle marchand : c’est un méta-régime archicratique où la valeur est produite par la prédictibilité elle-même. Plus une conduite est modélisable, plus elle devient exploitable économiquement. Ainsi, la régulation algorithmique opère une fusion silencieuse entre *normativité adaptative* et *captation de valeur* : la *co-viabilité* devient , le social devient dérivé comportemental, la subjectivité devient matière première prédictive. La *temporalité archicratique* est ici transformée : le pouvoir ne s’exerce plus dans l’instant de la décision, mais dans la continuité d’un *temps anticipé*, *continuellement recalculé, potentiellement monétisable*.
|
||||
Les algorithmes prédictifs — qu’ils s’appliquent à la gestion des flux logistiques, à l’attribution de crédits bancaires, à la surveillance policière prédictive, à la prescription culturelle ou aux plateformes de recrutement — n’imposent pas, mais orchestrent. Ils régulent non pas par la règle, mais par le poids statistique du comportement probable. C’est là que réside la spécificité du pouvoir machinique : il repose sur l’agrégation des passés, sur la projection automatisée de futurs, sur l’optimisation probabiliste des présents. Cette orchestration prédictive n’est pas que politique ou cognitive : elle est également financière. Dans les méta-régimes cybernético-calculatoires contemporains, les algorithmes prédictifs s’insèrent dans des circuits d’anticipation économique — marchés de la donnée, monétisation du trafic attentionnel, rentabilisation des trajectoires comportementales. Ce que Shoshana Zuboff a nommé le capitalisme de surveillance (2019) n’est pas simplement un modèle marchand : c’est un méta-régime archicratique où la valeur est produite par la prédictibilité elle-même. Plus une conduite est modélisable, plus elle devient exploitable économiquement. Ainsi, la régulation algorithmique opère une fusion silencieuse entre normativité adaptative et captation de valeur : la co-viabilité devient elle-même un gisement de valorisation, le social devient dérivé comportemental, la subjectivité devient matière première prédictive. La temporalité archicratique est ici transformée : le pouvoir ne s’exerce plus dans l’instant de la décision, mais dans la continuité d’un temps anticipé, continuellement recalculé, potentiellement monétisable.
|
||||
|
||||
Cette reconfiguration du pouvoir par la prédiction appelle à la convocation d’une autre penseuse majeure de notre époque : *Antoinette Rouvroy*, en collaboration avec *Thomas Berns*, a forgé le concept de *gouvernementalité algorithmique* (2009), qui constitue une des formulations les plus éclairantes de cette nouvelle archicration. Selon eux, l’algorithme ne cherche pas à produire de la subjectivité, mais au contraire à *court-circuiter* le sujet. Il ne s’adresse pas à des individus, mais à des profils, à des tendances statistiques, à des *corpus d’actions passées* devenus modèles opératoires. L’archicration n’est plus interpellation, mais *captation anonyme*. Elle est dé-subjectivante par essence, parce qu’elle opère dans l’inconscient computationnel du système, sans interaction réflexive.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user