Compare commits
158 Commits
chore/fix-
...
bot/propos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | |||
| 672e6d03d0 | |||
| 2881fdaf01 | |||
| db98a3787b | |||
| f9ea3760e2 | |||
| 78eb9cbb58 | |||
| 00e1a1d4b0 | |||
| ab3758bbc2 | |||
| 12d3d81518 | |||
| e2468be522 | |||
| dc2826df08 |
@@ -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 -->
|
||||
/...
|
||||
|
||||
|
||||
449
.gitea/workflows/anno-apply-pr.yml
Normal file
@@ -0,0 +1,449 @@
|
||||
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");
|
||||
}
|
||||
|
||||
// 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;
|
||||
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"
|
||||
|
||||
# Fast skip on obvious non-approved label events (avoid noise),
|
||||
# BUT do NOT skip if label payload is weird/unknown.
|
||||
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 title = String(issue.title || "");
|
||||
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 allowed = new Set(["type/media","type/reference","type/comment"]);
|
||||
const proposer = new Set(["type/correction","type/fact-check"]);
|
||||
|
||||
const out = [];
|
||||
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
|
||||
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
|
||||
|
||||
// HARD gate: must currently have state/approved (avoids depending on event payload)
|
||||
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 (allowed.has(type)) {
|
||||
// proceed
|
||||
} else if (proposer.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 (Proposer / unsupported / missing Type)
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env || true
|
||||
|
||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||
|
||||
# IMPORTANT: do NOT comment for "not_approved_label_present" (avoid spam on other label events)
|
||||
if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
|
||||
echo "ℹ️ skip reason=${SKIP_REASON} -> no comment"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||
|
||||
REASON="${SKIP_REASON:-}"
|
||||
TYPE="${ISSUE_TYPE:-}"
|
||||
|
||||
if [[ "$REASON" == proposer_type:* ]]; then
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors**.\n✅ Aucun traitement automatique."
|
||||
elif [[ "$REASON" == unsupported_type:* ]]; then
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : Type non supporté par le bot (${TYPE}).\n\nTypes supportés : type/media, type/reference, type/comment."
|
||||
else
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\nAjoute : Type: 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 a échoué (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 créée pour 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
@@ -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
|
||||
577
.gitea/workflows/deploy-staging-live.yml
Normal file
@@ -0,0 +1,577 @@
|
||||
name: Deploy staging+live (annotations)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
description: "Force FULL deploy (rebuild+restart) even if gate would hotpatch-only (1=yes, 0=no)"
|
||||
required: false
|
||||
default: "0"
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
DOCKER_API_VERSION: "1.43"
|
||||
COMPOSE_VERSION: "2.29.7"
|
||||
ASTRO_TELEMETRY_DISABLED: "1"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
concurrency:
|
||||
group: deploy-staging-live-main
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: nas-deploy
|
||||
container:
|
||||
image: localhost:5000/archicratie/nas-deploy-node22@sha256:fefa8bb307005cebec07796661ab25528dc319c33a8f1e480e1d66f90cd5cff6
|
||||
|
||||
steps:
|
||||
- name: Tools sanity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git --version
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Checkout (push or workflow_dispatch, no external actions)
|
||||
env:
|
||||
EVENT_JSON: /var/run/act/workflow/event.json
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
|
||||
|
||||
node --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const repoObj = ev?.repository || {};
|
||||
|
||||
const cloneUrl =
|
||||
repoObj?.clone_url ||
|
||||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
||||
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
|
||||
|
||||
const defaultBranch = repoObj?.default_branch || "main";
|
||||
|
||||
// Push-range (most reliable for change detection)
|
||||
const before = String(ev?.before || "").trim();
|
||||
const after =
|
||||
(process.env.GITHUB_SHA && String(process.env.GITHUB_SHA).trim()) ||
|
||||
String(ev?.after || ev?.sha || ev?.head_commit?.id || ev?.pull_request?.head?.sha || "").trim();
|
||||
|
||||
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
||||
|
||||
fs.writeFileSync("/tmp/deploy.env", [
|
||||
`REPO_URL=${shq(cloneUrl)}`,
|
||||
`DEFAULT_BRANCH=${shq(defaultBranch)}`,
|
||||
`BEFORE=${shq(before)}`,
|
||||
`AFTER=${shq(after)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
source /tmp/deploy.env
|
||||
echo "Repo URL: $REPO_URL"
|
||||
echo "Default branch: $DEFAULT_BRANCH"
|
||||
echo "BEFORE: ${BEFORE:-<empty>}"
|
||||
echo "AFTER: ${AFTER:-<empty>}"
|
||||
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git remote add origin "$REPO_URL"
|
||||
|
||||
# Checkout AFTER (or default branch if missing)
|
||||
if [[ -n "${AFTER:-}" ]]; then
|
||||
git fetch --depth 50 origin "$AFTER"
|
||||
git -c advice.detachedHead=false checkout -q FETCH_HEAD
|
||||
else
|
||||
git fetch --depth 50 origin "$DEFAULT_BRANCH"
|
||||
git -c advice.detachedHead=false checkout -q "origin/$DEFAULT_BRANCH"
|
||||
AFTER="$(git rev-parse HEAD)"
|
||||
echo "AFTER='$AFTER'" >> /tmp/deploy.env
|
||||
echo "Resolved AFTER: $AFTER"
|
||||
fi
|
||||
|
||||
git log -1 --oneline
|
||||
|
||||
- name: Gate — decide SKIP vs HOTPATCH vs FULL rebuild
|
||||
env:
|
||||
INPUT_FORCE: ${{ inputs.force }}
|
||||
EVENT_JSON: /var/run/act/workflow/event.json
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
|
||||
FORCE="${INPUT_FORCE:-0}"
|
||||
|
||||
# Lire before/after du push depuis event.json (merge-proof)
|
||||
node --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const before = ev?.before || "";
|
||||
const after = ev?.after || ev?.sha || "";
|
||||
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
||||
fs.writeFileSync("/tmp/gate.env", [
|
||||
`EV_BEFORE=${shq(before)}`,
|
||||
`EV_AFTER=${shq(after)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
source /tmp/gate.env
|
||||
|
||||
BEFORE="${EV_BEFORE:-}"
|
||||
AFTER="${EV_AFTER:-}"
|
||||
if [[ -z "${AFTER:-}" ]]; then
|
||||
AFTER="${SHA:-}"
|
||||
fi
|
||||
|
||||
echo "Gate ctx: BEFORE=${BEFORE:-<empty>} AFTER=${AFTER:-<empty>} FORCE=${FORCE}"
|
||||
|
||||
# Produire une liste CHANGED fiable :
|
||||
# - si BEFORE/AFTER valides -> git diff before..after
|
||||
# - sinon fallback -> diff parent1..after ou show after
|
||||
CHANGED=""
|
||||
Z40="0000000000000000000000000000000000000000"
|
||||
|
||||
if [[ -n "${BEFORE:-}" && "${BEFORE}" != "${Z40}" ]] \
|
||||
&& git cat-file -e "${BEFORE}^{commit}" 2>/dev/null \
|
||||
&& git cat-file -e "${AFTER}^{commit}" 2>/dev/null; then
|
||||
CHANGED="$(git diff --name-only "${BEFORE}" "${AFTER}" || true)"
|
||||
else
|
||||
P1="$(git rev-parse "${AFTER}^" 2>/dev/null || true)"
|
||||
if [[ -n "${P1:-}" ]] && git cat-file -e "${P1}^{commit}" 2>/dev/null; then
|
||||
CHANGED="$(git diff --name-only "${P1}" "${AFTER}" || true)"
|
||||
else
|
||||
CHANGED="$(git show --name-only --pretty="" "${AFTER}" | sed '/^$/d' || true)"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "%s\n" "${CHANGED}" > /tmp/changed.txt
|
||||
|
||||
echo "== changed files (first 200) =="
|
||||
sed -n '1,200p' /tmp/changed.txt || true
|
||||
|
||||
# Flags
|
||||
HAS_FULL=0
|
||||
HAS_HOTPATCH=0
|
||||
|
||||
# HOTPATCH si annotations/media touchés
|
||||
if grep -qE '^(src/annotations/|public/media/)' /tmp/changed.txt; then
|
||||
HAS_HOTPATCH=1
|
||||
fi
|
||||
|
||||
# FULL si build-impacting (robuste)
|
||||
# 1) Tout src/ SAUF src/annotations/
|
||||
if grep -qE '^src/' /tmp/changed.txt && grep -qEv '^src/annotations/' /tmp/changed.txt; then
|
||||
HAS_FULL=1
|
||||
fi
|
||||
|
||||
# 2) scripts/
|
||||
if grep -qE '^scripts/' /tmp/changed.txt; then
|
||||
HAS_FULL=1
|
||||
fi
|
||||
|
||||
# 3) Tout public/ SAUF public/media/
|
||||
if grep -qE '^public/' /tmp/changed.txt && grep -qEv '^public/media/' /tmp/changed.txt; then
|
||||
HAS_FULL=1
|
||||
fi
|
||||
|
||||
# 4) fichiers racine qui changent le build / l’image
|
||||
if grep -qE '^(package\.json|package-lock\.json|astro\.config\.mjs|tsconfig\.json|\.npmrc|\.nvmrc|Dockerfile|docker-compose\.yml|nginx\.conf)$' /tmp/changed.txt; then
|
||||
HAS_FULL=1
|
||||
fi
|
||||
|
||||
echo "Gate flags: HAS_FULL=${HAS_FULL} HAS_HOTPATCH=${HAS_HOTPATCH}"
|
||||
|
||||
# Décision
|
||||
if [[ "${FORCE}" == "1" ]]; then
|
||||
GO=1
|
||||
MODE="full"
|
||||
echo "✅ force=1 -> MODE=full (rebuild+restart)"
|
||||
elif [[ "${HAS_FULL}" == "1" ]]; then
|
||||
GO=1
|
||||
MODE="full"
|
||||
echo "✅ build-impacting change -> MODE=full (rebuild+restart)"
|
||||
elif [[ "${HAS_HOTPATCH}" == "1" ]]; then
|
||||
GO=1
|
||||
MODE="hotpatch"
|
||||
echo "✅ annotations/media change -> MODE=hotpatch"
|
||||
else
|
||||
GO=0
|
||||
MODE="skip"
|
||||
echo "ℹ️ no relevant change -> skip deploy"
|
||||
fi
|
||||
|
||||
echo "GO=${GO}" >> /tmp/deploy.env
|
||||
echo "MODE='${MODE}'" >> /tmp/deploy.env
|
||||
|
||||
- name: Toolchain sanity + resolve COMPOSE_PROJECT_NAME
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
# tools are prebaked in the image
|
||||
git --version
|
||||
docker version
|
||||
docker compose version
|
||||
python3 -c 'import yaml; print("PyYAML OK")'
|
||||
|
||||
# Reuse existing compose project name if containers already exist
|
||||
PROJ="$(docker inspect archicratie-web-blue --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
|
||||
if [[ -z "${PROJ:-}" ]]; then
|
||||
PROJ="$(docker inspect archicratie-web-green --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -z "${PROJ:-}" ]]; then PROJ="archicratie-web"; fi
|
||||
echo "COMPOSE_PROJECT_NAME='$PROJ'" >> /tmp/deploy.env
|
||||
echo "✅ Using COMPOSE_PROJECT_NAME=$PROJ"
|
||||
|
||||
# Assert target containers exist (hotpatch needs them)
|
||||
for c in archicratie-web-blue archicratie-web-green; do
|
||||
docker inspect "$c" >/dev/null 2>&1 || { echo "❌ missing container $c"; exit 5; }
|
||||
done
|
||||
|
||||
- name: Assert required vars (PUBLIC_GITEA_*) — only needed for MODE=full
|
||||
env:
|
||||
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
|
||||
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
|
||||
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ hotpatch mode -> vars not required"; exit 0; }
|
||||
|
||||
test -n "${PUBLIC_GITEA_BASE:-}" || { echo "❌ missing repo var PUBLIC_GITEA_BASE"; exit 2; }
|
||||
test -n "${PUBLIC_GITEA_OWNER:-}" || { echo "❌ missing repo var PUBLIC_GITEA_OWNER"; exit 2; }
|
||||
test -n "${PUBLIC_GITEA_REPO:-}" || { echo "❌ missing repo var PUBLIC_GITEA_REPO"; exit 2; }
|
||||
echo "✅ vars OK"
|
||||
|
||||
- name: Assert deploy files exist — only needed for MODE=full
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ hotpatch mode -> files not required"; exit 0; }
|
||||
|
||||
test -f docker-compose.yml
|
||||
test -f Dockerfile
|
||||
test -f nginx.conf
|
||||
echo "✅ deploy files OK"
|
||||
|
||||
- name: FULL — Build + deploy staging (blue) then warmup+smoke
|
||||
env:
|
||||
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
|
||||
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
|
||||
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ MODE=$MODE -> skip full rebuild"; exit 0; }
|
||||
|
||||
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
|
||||
|
||||
wait_url() {
|
||||
local url="$1"
|
||||
local label="$2"
|
||||
local tries="${3:-60}"
|
||||
for i in $(seq 1 "$tries"); do
|
||||
if curl -fsS --max-time 4 "$url" >/dev/null; then
|
||||
echo "✅ $label OK ($url)"
|
||||
return 0
|
||||
fi
|
||||
echo "… warmup $label ($i/$tries)"
|
||||
sleep 1
|
||||
done
|
||||
echo "❌ timeout $label ($url)"
|
||||
return 1
|
||||
}
|
||||
|
||||
TS="$(date -u +%Y%m%d-%H%M%S)"
|
||||
echo "TS='$TS'" >> /tmp/deploy.env
|
||||
docker image tag archicratie-web:blue "archicratie-web:blue.BAK.${TS}" || true
|
||||
docker image tag archicratie-web:green "archicratie-web:green.BAK.${TS}" || true
|
||||
|
||||
docker compose -p "$PROJ" -f docker-compose.yml build web_blue
|
||||
docker rm -f archicratie-web-blue || true
|
||||
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_blue
|
||||
|
||||
# warmup endpoints
|
||||
wait_url "http://127.0.0.1:8081/para-index.json" "blue para-index"
|
||||
wait_url "http://127.0.0.1:8081/annotations-index.json" "blue annotations-index"
|
||||
wait_url "http://127.0.0.1:8081/pagefind/pagefind.js" "blue pagefind.js"
|
||||
|
||||
CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
|
||||
echo "canonical(blue)=$CANON"
|
||||
echo "$CANON" | grep -q 'https://staging\.archicratie\.trans-hands\.synology\.me/' || {
|
||||
echo "❌ staging canonical mismatch"
|
||||
docker logs --tail 120 archicratie-web-blue || true
|
||||
exit 3
|
||||
}
|
||||
|
||||
echo "✅ staging OK"
|
||||
|
||||
- name: FULL — Build + deploy live (green) then warmup+smoke + rollback if needed
|
||||
env:
|
||||
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
|
||||
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
|
||||
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ MODE=$MODE -> skip full rebuild"; exit 0; }
|
||||
|
||||
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
|
||||
TS="${TS:-$(date -u +%Y%m%d-%H%M%S)}"
|
||||
|
||||
wait_url() {
|
||||
local url="$1"
|
||||
local label="$2"
|
||||
local tries="${3:-60}"
|
||||
for i in $(seq 1 "$tries"); do
|
||||
if curl -fsS --max-time 4 "$url" >/dev/null; then
|
||||
echo "✅ $label OK ($url)"
|
||||
return 0
|
||||
fi
|
||||
echo "… warmup $label ($i/$tries)"
|
||||
sleep 1
|
||||
done
|
||||
echo "❌ timeout $label ($url)"
|
||||
return 1
|
||||
}
|
||||
|
||||
rollback() {
|
||||
echo "⚠️ rollback green -> previous image tag (best effort)"
|
||||
docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green || true
|
||||
docker rm -f archicratie-web-green || true
|
||||
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green || true
|
||||
}
|
||||
|
||||
# build/restart green
|
||||
if ! docker compose -p "$PROJ" -f docker-compose.yml build web_green; then
|
||||
echo "❌ build green failed"; rollback; exit 4
|
||||
fi
|
||||
|
||||
docker rm -f archicratie-web-green || true
|
||||
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green
|
||||
|
||||
# warmup endpoints
|
||||
if ! wait_url "http://127.0.0.1:8082/para-index.json" "green para-index"; then rollback; exit 4; fi
|
||||
if ! wait_url "http://127.0.0.1:8082/annotations-index.json" "green annotations-index"; then rollback; exit 4; fi
|
||||
if ! wait_url "http://127.0.0.1:8082/pagefind/pagefind.js" "green pagefind.js"; then rollback; exit 4; fi
|
||||
|
||||
CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
|
||||
echo "canonical(green)=$CANON"
|
||||
echo "$CANON" | grep -q 'https://archicratie\.trans-hands\.synology\.me/' || {
|
||||
echo "❌ live canonical mismatch"
|
||||
docker logs --tail 120 archicratie-web-green || true
|
||||
rollback
|
||||
exit 4
|
||||
}
|
||||
|
||||
echo "✅ live OK"
|
||||
|
||||
- name: HOTPATCH — deep merge shards -> annotations-index + copy changed media into blue+green
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
python3 - <<'PY'
|
||||
import os, re, json, glob
|
||||
import yaml
|
||||
import datetime as dt
|
||||
|
||||
ROOT = os.getcwd()
|
||||
ANNO_ROOT = os.path.join(ROOT, "src", "annotations")
|
||||
|
||||
def is_obj(x): return isinstance(x, dict)
|
||||
def is_arr(x): return isinstance(x, list)
|
||||
|
||||
def iso_dt(x):
|
||||
if isinstance(x, dt.datetime):
|
||||
if x.tzinfo is None:
|
||||
return x.isoformat()
|
||||
return x.astimezone(dt.timezone.utc).isoformat().replace("+00:00","Z")
|
||||
if isinstance(x, dt.date):
|
||||
return x.isoformat()
|
||||
return None
|
||||
|
||||
def normalize(x):
|
||||
s = iso_dt(x)
|
||||
if s is not None: return s
|
||||
if isinstance(x, dict):
|
||||
return {str(k): normalize(v) for k, v in x.items()}
|
||||
if isinstance(x, list):
|
||||
return [normalize(v) for v in x]
|
||||
return x
|
||||
|
||||
def key_media(it): return str((it or {}).get("src",""))
|
||||
def key_ref(it):
|
||||
it = it or {}
|
||||
return "||".join([str(it.get("url","")), str(it.get("label","")), str(it.get("kind","")), str(it.get("citation",""))])
|
||||
def key_comment(it): return str((it or {}).get("text","")).strip()
|
||||
|
||||
def dedup_extend(dst_list, src_list, key_fn):
|
||||
seen = set(); out = []
|
||||
for x in (dst_list or []):
|
||||
x = normalize(x); k = key_fn(x)
|
||||
if k and k not in seen: seen.add(k); out.append(x)
|
||||
for x in (src_list or []):
|
||||
x = normalize(x); k = key_fn(x)
|
||||
if k and k not in seen: seen.add(k); out.append(x)
|
||||
return out
|
||||
|
||||
def deep_merge(dst, src):
|
||||
src = normalize(src)
|
||||
for k, v in (src or {}).items():
|
||||
if k in ("media","refs","comments_editorial") and is_arr(v):
|
||||
if k == "media": dst[k] = dedup_extend(dst.get(k, []), v, key_media)
|
||||
elif k == "refs": dst[k] = dedup_extend(dst.get(k, []), v, key_ref)
|
||||
else: dst[k] = dedup_extend(dst.get(k, []), v, key_comment)
|
||||
continue
|
||||
|
||||
if is_obj(v):
|
||||
if not is_obj(dst.get(k)): dst[k] = {}
|
||||
deep_merge(dst[k], v)
|
||||
continue
|
||||
|
||||
if is_arr(v):
|
||||
cur = dst.get(k, [])
|
||||
if not is_arr(cur): cur = []
|
||||
seen = set(); out = []
|
||||
for x in cur:
|
||||
x = normalize(x)
|
||||
s = json.dumps(x, sort_keys=True, ensure_ascii=False)
|
||||
if s not in seen: seen.add(s); out.append(x)
|
||||
for x in v:
|
||||
x = normalize(x)
|
||||
s = json.dumps(x, sort_keys=True, ensure_ascii=False)
|
||||
if s not in seen: seen.add(s); out.append(x)
|
||||
dst[k] = out
|
||||
continue
|
||||
|
||||
v = normalize(v)
|
||||
if k not in dst or dst.get(k) in (None, ""):
|
||||
dst[k] = v
|
||||
|
||||
def para_num(pid):
|
||||
m = re.match(r"^p-(\d+)-", str(pid))
|
||||
return int(m.group(1)) if m else 10**9
|
||||
|
||||
def sort_lists(entry):
|
||||
for k in ("media","refs","comments_editorial"):
|
||||
arr = entry.get(k)
|
||||
if not is_arr(arr): continue
|
||||
def ts(x):
|
||||
x = normalize(x)
|
||||
try:
|
||||
s = str((x or {}).get("ts",""))
|
||||
return dt.datetime.fromisoformat(s.replace("Z","+00:00")).timestamp() if s else 0
|
||||
except Exception:
|
||||
return 0
|
||||
arr = [normalize(x) for x in arr]
|
||||
arr.sort(key=lambda x: (ts(x), json.dumps(x, sort_keys=True, ensure_ascii=False)))
|
||||
entry[k] = arr
|
||||
|
||||
if not os.path.isdir(ANNO_ROOT):
|
||||
raise SystemExit(f"Missing annotations root: {ANNO_ROOT}")
|
||||
|
||||
pages = {}
|
||||
errors = []
|
||||
|
||||
files = sorted(glob.glob(os.path.join(ANNO_ROOT, "**", "*.yml"), recursive=True))
|
||||
for fp in files:
|
||||
try:
|
||||
with open(fp, "r", encoding="utf-8") as f:
|
||||
doc = yaml.safe_load(f) or {}
|
||||
doc = normalize(doc)
|
||||
if not isinstance(doc, dict) or doc.get("schema") != 1:
|
||||
continue
|
||||
|
||||
page = str(doc.get("page","")).strip().strip("/")
|
||||
paras = doc.get("paras") or {}
|
||||
if not page or not isinstance(paras, dict):
|
||||
continue
|
||||
|
||||
pg = pages.setdefault(page, {"paras": {}})
|
||||
for pid, entry in paras.items():
|
||||
pid = str(pid)
|
||||
if pid not in pg["paras"] or not isinstance(pg["paras"].get(pid), dict):
|
||||
pg["paras"][pid] = {}
|
||||
if isinstance(entry, dict):
|
||||
deep_merge(pg["paras"][pid], entry)
|
||||
sort_lists(pg["paras"][pid])
|
||||
|
||||
except Exception as e:
|
||||
errors.append({"file": os.path.relpath(fp, ROOT), "error": str(e)})
|
||||
|
||||
for page, obj in pages.items():
|
||||
keys = list((obj.get("paras") or {}).keys())
|
||||
keys.sort(key=lambda k: (para_num(k), k))
|
||||
obj["paras"] = {k: obj["paras"][k] for k in keys}
|
||||
|
||||
out = {
|
||||
"schema": 1,
|
||||
"generatedAt": dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc).isoformat().replace("+00:00","Z"),
|
||||
"pages": pages,
|
||||
"stats": {
|
||||
"pages": len(pages),
|
||||
"paras": sum(len(v.get("paras") or {}) for v in pages.values()),
|
||||
"errors": len(errors),
|
||||
},
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
with open("/tmp/annotations-index.json", "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, ensure_ascii=False)
|
||||
|
||||
print("OK: wrote /tmp/annotations-index.json pages=", out["stats"]["pages"], "paras=", out["stats"]["paras"], "errors=", out["stats"]["errors"])
|
||||
PY
|
||||
|
||||
# patch JSON into running containers
|
||||
for c in archicratie-web-blue archicratie-web-green; do
|
||||
echo "== patch annotations-index.json into $c =="
|
||||
docker cp /tmp/annotations-index.json "${c}:/usr/share/nginx/html/annotations-index.json"
|
||||
done
|
||||
|
||||
# copy changed media files into containers (so new media appears without rebuild)
|
||||
if [[ -s /tmp/changed.txt ]]; then
|
||||
while IFS= read -r f; do
|
||||
[[ -n "$f" ]] || continue
|
||||
if [[ "$f" == public/media/* ]]; then
|
||||
dest="/usr/share/nginx/html/${f#public/}" # => /usr/share/nginx/html/media/...
|
||||
for c in archicratie-web-blue archicratie-web-green; do
|
||||
echo "== copy media into $c: $f -> $dest =="
|
||||
docker exec "$c" sh -lc "mkdir -p \"$(dirname "$dest")\""
|
||||
docker cp "$f" "$c:$dest"
|
||||
done
|
||||
fi
|
||||
done < /tmp/changed.txt
|
||||
fi
|
||||
|
||||
# smoke after patch
|
||||
for p in 8081 8082; do
|
||||
echo "== smoke annotations-index on $p =="
|
||||
curl -fsS --max-time 6 "http://127.0.0.1:${p}/annotations-index.json" \
|
||||
| python3 -c 'import sys,json; j=json.load(sys.stdin); print("generatedAt:", j.get("generatedAt")); print("pages:", len(j.get("pages") or {})); print("paras:", j.get("stats",{}).get("paras"))'
|
||||
done
|
||||
|
||||
echo "✅ hotpatch done"
|
||||
|
||||
- name: Debug on failure (containers status/logs)
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "== docker ps =="
|
||||
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | sed -n '1,80p' || true
|
||||
for c in archicratie-web-blue archicratie-web-green; do
|
||||
echo "== logs $c (tail 200) =="
|
||||
docker logs --tail 200 "$c" || true
|
||||
done
|
||||
395
.gitea/workflows/proposer-apply-pr.yml
Normal file
@@ -0,0 +1,395 @@
|
||||
name: Proposer Apply (PR)
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue:
|
||||
description: "Issue number to apply (Proposer: correction/fact-check)"
|
||||
required: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
concurrency:
|
||||
group: proposer-apply-${{ github.event.issue.number || inputs.issue || 'manual' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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)
|
||||
env:
|
||||
INPUT_ISSUE: ${{ inputs.issue }}
|
||||
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);
|
||||
|
||||
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
|
||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||
}
|
||||
|
||||
const labelName =
|
||||
ev?.label?.name ||
|
||||
ev?.label ||
|
||||
"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)}`,
|
||||
`API_BASE=${sh(apiBase)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ context:"
|
||||
sed -n '1,120p' /tmp/proposer.env
|
||||
|
||||
- name: Gate on label state/approved
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
|
||||
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||
echo "SKIP=1" >> /tmp/proposer.env
|
||||
exit 0
|
||||
fi
|
||||
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
||||
|
||||
- name: Fetch issue + API-hard gate on (state/approved present + proposer type)
|
||||
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; }
|
||||
|
||||
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/proposer.env
|
||||
import fs from "node:fs";
|
||||
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
||||
const title = String(issue.title || "");
|
||||
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) : [];
|
||||
|
||||
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 hasApproved = labels.includes("state/approved");
|
||||
const proposer = new Set(["type/correction","type/fact-check"]);
|
||||
|
||||
const out = [];
|
||||
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
|
||||
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
|
||||
out.push(`HAS_APPROVED=${hasApproved ? "1":"0"}`);
|
||||
|
||||
if (!hasApproved) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("approved_not_present")}`);
|
||||
} else if (!type) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
|
||||
} else if (!proposer.has(type)) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("not_proposer:"+type)}`);
|
||||
}
|
||||
process.stdout.write(out.join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ proposer gating:"
|
||||
grep -E '^(ISSUE_TYPE|HAS_APPROVED|SKIP|SKIP_REASON)=' /tmp/proposer.env || true
|
||||
|
||||
- name: Comment issue if skipped
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
|
||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||
[[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0
|
||||
|
||||
REASON="${SKIP_REASON:-}"
|
||||
TYPE="${ISSUE_TYPE:-}"
|
||||
|
||||
if [[ "$REASON" == "approved_not_present" ]]; then
|
||||
MSG="ℹ️ Proposer Apply: skip — le label **state/approved** n'est pas présent sur le ticket au moment du run (gate API-hard)."
|
||||
elif [[ "$REASON" == "missing_type" ]]; then
|
||||
MSG="ℹ️ Proposer Apply: skip — champ **Type:** manquant/illisible. Attendu: type/correction ou type/fact-check."
|
||||
else
|
||||
MSG="ℹ️ Proposer Apply: skip — Type non-Proposer (${TYPE}). (Ce workflow ne traite que correction/fact-check.)"
|
||||
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" || true
|
||||
|
||||
- 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
|
||||
echo "✅ workspace:"
|
||||
ls -la | sed -n '1,120p'
|
||||
|
||||
- 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"
|
||||
ls -la "$APP_DIR" | sed -n '1,120p'
|
||||
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: NPM harden (reduce flakiness)
|
||||
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 (APP_DIR)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
cd "$APP_DIR"
|
||||
npm ci --no-audit --no-fund
|
||||
|
||||
- name: Build dist baseline (APP_DIR)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
cd "$APP_DIR"
|
||||
npm run build
|
||||
|
||||
- name: Apply ticket (alias + commit) 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 }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
git config user.name "${BOT_GIT_NAME:-archicratie-bot}"
|
||||
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
|
||||
|
||||
START_SHA="$(git rev-parse HEAD)"
|
||||
TS="$(date -u +%Y%m%d-%H%M%S)"
|
||||
BR="bot/proposer-${ISSUE_NUMBER}-${TS}"
|
||||
echo "BRANCH=$BR" >> /tmp/proposer.env
|
||||
git checkout -b "$BR"
|
||||
|
||||
export GITEA_OWNER="$OWNER"
|
||||
export GITEA_REPO="$REPO"
|
||||
export FORGE_BASE="$API_BASE"
|
||||
|
||||
LOG="/tmp/proposer-apply.log"
|
||||
set +e
|
||||
(cd "$APP_DIR" && node scripts/apply-ticket.mjs "$ISSUE_NUMBER" --alias --commit) >"$LOG" 2>&1
|
||||
RC=$?
|
||||
set -e
|
||||
|
||||
echo "APPLY_RC=$RC" >> /tmp/proposer.env
|
||||
|
||||
echo "== apply log (tail) =="
|
||||
tail -n 200 "$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: 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; }
|
||||
[[ "${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 issue
|
||||
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
|
||||
[[ "${NOOP:-0}" == "0" ]] || exit 0
|
||||
[[ -n "${BRANCH:-}" ]] || { echo "ℹ️ BRANCH unset -> skip PR"; exit 0; }
|
||||
|
||||
PR_TITLE="proposer: apply ticket #${ISSUE_NUMBER}"
|
||||
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA:-unknown}\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 Proposer créée pour 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"
|
||||
|
||||
- name: Finalize (fail job if apply failed)
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
RC="${APPLY_RC:-0}"
|
||||
if [[ "$RC" != "0" ]]; then
|
||||
echo "❌ apply failed (rc=$RC)"
|
||||
exit "$RC"
|
||||
fi
|
||||
echo "✅ apply 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"
|
||||
|
||||
7
.gitignore
vendored
@@ -3,6 +3,10 @@
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# dev-only
|
||||
public/_auth/whoami
|
||||
public/_auth/whoami/*
|
||||
|
||||
# --- local backups ---
|
||||
*.bak
|
||||
*.bak.*
|
||||
@@ -21,3 +25,6 @@ dist/
|
||||
# local backups
|
||||
Dockerfile.bak.*
|
||||
public/favicon_io.zip
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
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
|
||||
7
bridge/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM node:22-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install --omit=dev
|
||||
COPY server.mjs ./
|
||||
EXPOSE 8787
|
||||
CMD ["node","server.mjs"]
|
||||
15
bridge/docker-compose-bridge.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
issue_bridge:
|
||||
build: ./bridge
|
||||
environment:
|
||||
GITEA_API_BASE: "http://gitea:3000"
|
||||
GITEA_TOKEN: "${GITEA_TOKEN}"
|
||||
GITEA_OWNER: "Archicratia"
|
||||
GITEA_REPO: "archicratie-edition"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
|
||||
networks:
|
||||
internal:
|
||||
external: true
|
||||
10
bridge/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "issue-bridge",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"express": "^4.19.2",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
}
|
||||
}
|
||||
|
||||
89
bridge/server.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
import express from "express";
|
||||
import multer from "multer";
|
||||
|
||||
const app = express();
|
||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } }); // 25 MB
|
||||
|
||||
const {
|
||||
GITEA_API_BASE, // ex: http://gitea:3000 (ou https://forge.tld)
|
||||
GITEA_TOKEN, // PAT du bot
|
||||
GITEA_OWNER, // owner/org
|
||||
GITEA_REPO // repo
|
||||
} = process.env;
|
||||
|
||||
function mustEnv(name) {
|
||||
if (!process.env[name]) throw new Error(`Missing env ${name}`);
|
||||
}
|
||||
["GITEA_API_BASE","GITEA_TOKEN","GITEA_OWNER","GITEA_REPO"].forEach(mustEnv);
|
||||
|
||||
function isEditor(req) {
|
||||
// Adapte selon tes headers Authelia. Souvent Remote-Groups / Remote-User.
|
||||
const groups = String(req.header("Remote-Groups") || req.header("X-Remote-Groups") || "");
|
||||
return groups.split(/[,\s]+/).includes("editors");
|
||||
}
|
||||
|
||||
async function giteaFetch(path, init = {}) {
|
||||
const url = String(GITEA_API_BASE).replace(/\/+$/, "") + path;
|
||||
const headers = new Headers(init.headers || {});
|
||||
headers.set("Authorization", `token ${GITEA_TOKEN}`);
|
||||
return fetch(url, { ...init, headers });
|
||||
}
|
||||
|
||||
app.get("/health", (_req, res) => res.json({ ok: true }));
|
||||
|
||||
app.post("/media", upload.single("file"), async (req, res) => {
|
||||
try {
|
||||
if (!isEditor(req)) return res.status(403).json({ ok: false, error: "forbidden" });
|
||||
|
||||
const file = req.file;
|
||||
const title = String(req.body.title || "").trim();
|
||||
const body = String(req.body.body || "").trim();
|
||||
const suggestedName = String(req.body.suggestedName || "").trim();
|
||||
|
||||
if (!file) return res.status(400).json({ ok: false, error: "missing_file" });
|
||||
if (!title) return res.status(400).json({ ok: false, error: "missing_title" });
|
||||
if (!body) return res.status(400).json({ ok: false, error: "missing_body" });
|
||||
|
||||
// 1) Create issue
|
||||
const r1 = await giteaFetch(`/api/v1/repos/${encodeURIComponent(GITEA_OWNER)}/${encodeURIComponent(GITEA_REPO)}/issues`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title, body })
|
||||
});
|
||||
|
||||
if (!r1.ok) {
|
||||
const t = await r1.text().catch(() => "");
|
||||
return res.status(502).json({ ok: false, step: "create_issue", status: r1.status, detail: t.slice(0, 2000) });
|
||||
}
|
||||
|
||||
const issue = await r1.json();
|
||||
const index = issue?.number ?? issue?.index;
|
||||
const issueUrl = issue?.html_url;
|
||||
|
||||
if (!index) return res.status(502).json({ ok: false, step: "create_issue", error: "missing_issue_index" });
|
||||
|
||||
// 2) Upload attachment (multipart field name = "attachment") :contentReference[oaicite:1]{index=1}
|
||||
const fd = new FormData();
|
||||
fd.append("attachment", new Blob([file.buffer], { type: file.mimetype || "application/octet-stream" }), file.originalname);
|
||||
|
||||
const q = suggestedName ? `?name=${encodeURIComponent(suggestedName)}` : "";
|
||||
const r2 = await giteaFetch(`/api/v1/repos/${encodeURIComponent(GITEA_OWNER)}/${encodeURIComponent(GITEA_REPO)}/issues/${encodeURIComponent(String(index))}/assets${q}`, {
|
||||
method: "POST",
|
||||
body: fd
|
||||
});
|
||||
|
||||
if (!r2.ok) {
|
||||
const t = await r2.text().catch(() => "");
|
||||
return res.status(502).json({ ok: false, step: "upload_asset", status: r2.status, detail: t.slice(0, 2000), issueUrl });
|
||||
}
|
||||
|
||||
const asset = await r2.json().catch(() => ({}));
|
||||
return res.json({ ok: true, issueUrl, issueIndex: index, asset });
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(8787, "0.0.0.0", () => {
|
||||
console.log("issue-bridge listening on :8787");
|
||||
});
|
||||
@@ -5,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
@@ -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.
|
||||
|
||||
@@ -202,4 +202,33 @@ docker compose logs --tail=200 web_blue
|
||||
docker compose logs --tail=200 web_green
|
||||
|
||||
# Si tu veux suivre en live :
|
||||
docker compose logs -f web_green
|
||||
docker compose logs -f web_green
|
||||
|
||||
|
||||
## Historique synthétique (2026-03-03) — Stabilisation CI/CD “zéro surprise”
|
||||
|
||||
### Problème initial observé
|
||||
- Déploiement parfois lancé en “hotpatch” alors qu’un rebuild était nécessaire.
|
||||
- Sur merge commits, la détection de fichiers modifiés pouvait être ambiguë.
|
||||
- Résultat : besoin de `force=1` manuel pour éviter des incohérences.
|
||||
|
||||
### Correctif appliqué
|
||||
- Gate CI rendu **merge-proof** :
|
||||
- lecture de `BEFORE` et `AFTER` depuis `event.json`
|
||||
- calcul des fichiers modifiés via `git diff --name-only BEFORE AFTER`
|
||||
|
||||
- Politique de décision stabilisée :
|
||||
- FULL auto dès qu’un changement impacte build/runtime (content/pages/scripts/anchors/etc.)
|
||||
- HOTPATCH auto uniquement pour annotations/media
|
||||
|
||||
### Preuves
|
||||
- Test A (touch src/content) :
|
||||
- Gate flags: HAS_FULL=1 HAS_HOTPATCH=0 → MODE=full
|
||||
- Test B (touch src/annotations) :
|
||||
- Gate flags: HAS_FULL=0 HAS_HOTPATCH=1 → MODE=hotpatch
|
||||
|
||||
### Audit post-déploiement (preuves côté NAS)
|
||||
- 8081 + 8082 répondent HTTP 200
|
||||
- `/para-index.json` + `/annotations-index.json` OK
|
||||
- Aliases injectés visibles dans HTML via `.para-alias` quand alias présent
|
||||
|
||||
|
||||
176
docs/START-HERE.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# START-HERE — Archicratie / Édition Web (v2)
|
||||
> Onboarding + exploitation “nickel chrome” (DEV → Gitea → CI → Release → Blue/Green → Edge/SSO)
|
||||
|
||||
## 0) TL;DR (la règle d’or)
|
||||
- **Gitea = source canonique**.
|
||||
- **main est protégé** : toute modification passe par **branche → PR → CI → merge**.
|
||||
- **Le NAS n’est pas la source** : si un hotfix est fait sur NAS, on **backporte** via PR immédiatement.
|
||||
- **Le site est statique Astro** : la prod sert du HTML (nginx), l’accès est contrôlé au niveau reverse-proxy (Traefik + Authelia).
|
||||
|
||||
## 1) Architecture mentale (ultra simple)
|
||||
- **DEV (Mac Studio)** : édition + tests + commit + push
|
||||
- **Gitea** : dépôt canon + PR + CI (CI.yaml)
|
||||
- **NAS (DS220+)** : déploiement “blue/green”
|
||||
- `web_blue` (staging upstream) → `127.0.0.1:8081`
|
||||
- `web_green` (live upstream) → `127.0.0.1:8082`
|
||||
- **Edge (Traefik)** : route les hosts
|
||||
- `staging.archicratie...` → 8081
|
||||
- `archicratie...` → 8082
|
||||
- **Authelia** devant, via middleware `chain-auth@file`
|
||||
|
||||
## 2) Répertoires & conventions (repo)
|
||||
### 2.1 Contenu canon (édition)
|
||||
- `src/content/**` : contenu MD / MDX canon (Astro content collections)
|
||||
- `src/pages/**` : routes Astro (index, [...slug], etc.)
|
||||
- `src/components/**` : composants UI (SiteNav, TOC, SidePanel, etc.)
|
||||
- `src/layouts/**` : layouts (EditionLayout, SiteLayout)
|
||||
- `src/styles/**` : CSS global
|
||||
|
||||
### 2.2 Annotations (pré-Édition “tickets”)
|
||||
- `src/annotations/<workKey>/<slug>.yml`
|
||||
- Exemple : `src/annotations/archicrat-ia/prologue.yml`
|
||||
- Objectif : stocker “Références / Médias / Commentaires” par page et par paragraphe (`p-...`).
|
||||
|
||||
### 2.3 Scripts (tooling / build)
|
||||
- `scripts/inject-anchor-aliases.mjs` : injection aliases dans dist
|
||||
- `scripts/dedupe-ids-dist.mjs` : retire IDs dupliqués dans dist
|
||||
- `scripts/build-para-index.mjs` : index paragraphes (postbuild / predev)
|
||||
- `scripts/build-annotations-index.mjs` : index annotations (postbuild / predev)
|
||||
- `scripts/check-anchors.mjs` : contrat stabilité d’ancres (CI)
|
||||
- `scripts/check-annotations*.mjs` : sanity YAML + médias
|
||||
|
||||
> Important : les scripts sont **partie intégrante** de la stabilité (IDs/ancres/indexation).
|
||||
> On évite “la magie” : tout est scripté + vérifié.
|
||||
|
||||
## 3) Workflow Git “pro” (main protégé)
|
||||
### 3.1 Cycle standard (toute modif)
|
||||
en bash :
|
||||
|
||||
git checkout main
|
||||
git pull --ff-only
|
||||
|
||||
BR="chore/xxx-$(date +%Y%m%d)"
|
||||
git checkout -b "$BR"
|
||||
|
||||
# dev…
|
||||
npm i
|
||||
npm run build
|
||||
npm run test:anchors
|
||||
|
||||
git add -A
|
||||
git commit -m "xxx: description claire"
|
||||
git push -u origin "$BR"
|
||||
|
||||
### 3.2 PR vers main
|
||||
|
||||
Ouvrir PR dans Gitea
|
||||
|
||||
CI doit être verte
|
||||
|
||||
Merge PR → main
|
||||
|
||||
### 3.3 Cas spécial : hotfix prod (NAS)
|
||||
|
||||
On peut faire un hotfix “urgence” en prod/staging si nécessaire…
|
||||
|
||||
MAIS : l’état final doit revenir dans Gitea : branche → PR → CI → merge.
|
||||
|
||||
## 4) Déploiement (NAS) — principe
|
||||
### 4.1 Release pack
|
||||
|
||||
On génère un pack “reproductible” (source + config + scripts) puis on déploie.
|
||||
|
||||
### 4.2 Blue/Green
|
||||
|
||||
web_blue = staging upstream (8081)
|
||||
|
||||
web_green = live upstream (8082)
|
||||
|
||||
Edge Traefik sélectionne quel host pointe vers quel upstream.
|
||||
|
||||
## 5) Check-list “≤ 10 commandes” (happy path complet)
|
||||
### 5.1 DEV (Mac)
|
||||
|
||||
git checkout main && git pull --ff-only
|
||||
git checkout -b chore/my-change-$(date +%Y%m%d)
|
||||
|
||||
npm i
|
||||
rm -rf .astro node_modules/.vite dist
|
||||
npm run build
|
||||
npm run test:anchors
|
||||
npm run dev
|
||||
|
||||
### 5.2 Push + PR
|
||||
|
||||
git add -A
|
||||
git commit -m "chore: my change"
|
||||
git push -u origin chore/my-change-YYYYMMDD
|
||||
# ouvrir PR dans Gitea
|
||||
|
||||
### 5.3 Déploiement NAS (résumé)
|
||||
|
||||
Voir docs/runbooks/DEPLOY-BLUE-GREEN.md.
|
||||
|
||||
## 6) Problèmes “classiques” + diagnostic rapide
|
||||
### 6.1 “Le staging ne ressemble pas au local”
|
||||
|
||||
# Comparer upstream direct 8081 vs 8082 :
|
||||
|
||||
curl -sS http://127.0.0.1:8081/ | head -n 2
|
||||
curl -sS http://127.0.0.1:8082/ | head -n 2
|
||||
|
||||
# Vérifier quel routeur edge répond (header diag) :
|
||||
|
||||
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
|
||||
| grep -iE 'HTTP/|location:|x-archi-router'
|
||||
|
||||
# Lire docs/runbooks/EDGE-TRAEFIK.md.
|
||||
|
||||
### 6.2 Canonical incorrect (localhost en prod)
|
||||
|
||||
Cause racine : site dans Astro = PUBLIC_SITE non injecté au build.
|
||||
|
||||
Fix canonique : voir docs/runbooks/ENV-PUBLIC_SITE.md.
|
||||
|
||||
Test :
|
||||
|
||||
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -1
|
||||
|
||||
### 6.3 Contrat “anchors” en échec après migration d’URL
|
||||
|
||||
Quand on déplace des routes (ex: /archicratie/archicrat-ia/* → /archicrat-ia/*), le test d’ancres peut échouer même si les IDs n’ont pas changé, car les pages ont changé de chemin.
|
||||
|
||||
# Procédure safe :
|
||||
|
||||
Backup baseline :
|
||||
|
||||
cp -a tests/anchors-baseline.json /tmp/anchors-baseline.json.bak.$(date +%F-%H%M%S)
|
||||
|
||||
Mettre à jour les clés (chemins) sans toucher aux IDs :
|
||||
|
||||
node - <<'NODE'
|
||||
import fs from 'fs';
|
||||
const p='tests/anchors-baseline.json';
|
||||
const j=JSON.parse(fs.readFileSync(p,'utf8'));
|
||||
const out={};
|
||||
for (const [k,v] of Object.entries(j)) {
|
||||
const nk = k.replace(/^archicratie\/archicrat-ia\//, 'archicrat-ia/');
|
||||
out[nk]=v;
|
||||
}
|
||||
fs.writeFileSync(p, JSON.stringify(out,null,2)+'\n');
|
||||
console.log('updated keys:', Object.keys(j).length, '->', Object.keys(out).length);
|
||||
NODE
|
||||
|
||||
Re-run :
|
||||
|
||||
npm run test:anchors
|
||||
|
||||
## 7) Ce que l’étape 9 doit faire (orientation)
|
||||
|
||||
Stabiliser le pipeline “tickets → YAML annotations”
|
||||
|
||||
Formaliser la spec YAML + merge + anti-doublon (voir docs/EDITORIAL-ANNOTATIONS-SPEC.md)
|
||||
|
||||
Durcir l’onboarding (ce START-HERE + runbooks)
|
||||
|
||||
Éviter les régressions par tests (anchors / annotations / smoke)
|
||||
@@ -0,0 +1,427 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="900"
|
||||
viewBox="0 0 1600 900"
|
||||
version="1.1"
|
||||
id="svg49"
|
||||
sodipodi:docname="archicratie-web-edition-blue-green-runbook-verbatim-v2.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview49"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="726.17247"
|
||||
inkscape:cy="401.21029"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg49" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<!-- Fond clair lisible partout -->
|
||||
<linearGradient
|
||||
id="bg"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ffffff"
|
||||
id="stop1" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#f1f5f9"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<!-- Flèches -->
|
||||
<marker
|
||||
id="arrow"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#334155"
|
||||
id="path2" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowA"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#2563eb"
|
||||
id="path3" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowG"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#059669"
|
||||
id="path4" />
|
||||
</marker>
|
||||
<!-- Styles SANS variables CSS (compat max) -->
|
||||
<style
|
||||
id="style4"><![CDATA[
|
||||
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
|
||||
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
|
||||
|
||||
/* Cadres lisibles */
|
||||
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
|
||||
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
|
||||
|
||||
/* Pills */
|
||||
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
|
||||
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
|
||||
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
|
||||
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
|
||||
|
||||
/* Traits / flèches */
|
||||
.line{stroke:#334155;stroke-width:1.4;fill:none}
|
||||
.dash{stroke-dasharray:6 6}
|
||||
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
|
||||
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
|
||||
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
|
||||
]]></style>
|
||||
</defs>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="1600"
|
||||
height="900"
|
||||
fill="url(#bg)"
|
||||
id="rect4" />
|
||||
<text
|
||||
x="40"
|
||||
y="56"
|
||||
class="title"
|
||||
id="text4">Archicratie — Runbook Blue/Green (v2, verbatim)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="84"
|
||||
class="subtitle"
|
||||
id="text5">Mise à jour 2026-02-20 — release-pack → releases/<ts>/app → current → docker compose web_blue/web_green</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="130"
|
||||
width="500"
|
||||
height="240"
|
||||
class="box"
|
||||
id="rect5" />
|
||||
<text
|
||||
x="58"
|
||||
y="160"
|
||||
class="h"
|
||||
id="text6">0) Pré-requis</text>
|
||||
<text
|
||||
x="58"
|
||||
y="184"
|
||||
class="t"
|
||||
id="text7">main protégé → travail via branches + PR</text>
|
||||
<text
|
||||
x="58"
|
||||
y="202"
|
||||
class="t"
|
||||
id="text8">CI doit rester source de vérité</text>
|
||||
<text
|
||||
x="58"
|
||||
y="220"
|
||||
class="t"
|
||||
id="text9">Éviter d'éditer une release en prod (hotfix = exception)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="238"
|
||||
class="s"
|
||||
id="text10">Si hotfix: on le re-synchronise ensuite dans Git (cf. étape 5)</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="400"
|
||||
width="500"
|
||||
height="260"
|
||||
class="box2"
|
||||
id="rect10" />
|
||||
<text
|
||||
x="58"
|
||||
y="430"
|
||||
class="h"
|
||||
id="text11">1) Préparer une release (atelier DEV)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="454"
|
||||
class="mono"
|
||||
id="text12">npm ci && npm run build</text>
|
||||
<text
|
||||
x="58"
|
||||
y="472"
|
||||
class="mono"
|
||||
id="text13">release-pack.sh → tarball/artefact</text>
|
||||
<text
|
||||
x="58"
|
||||
y="490"
|
||||
class="mono"
|
||||
id="text14">inclut dist/ + pagefind + indexes + build stamp</text>
|
||||
<text
|
||||
x="58"
|
||||
y="508"
|
||||
class="t"
|
||||
id="text15">ouvrir PR → merge → CI</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="690"
|
||||
width="500"
|
||||
height="170"
|
||||
class="box"
|
||||
id="rect15" />
|
||||
<text
|
||||
x="58"
|
||||
y="720"
|
||||
class="h"
|
||||
id="text16">2) Déposer sur NAS</text>
|
||||
<text
|
||||
x="58"
|
||||
y="744"
|
||||
class="mono"
|
||||
id="text17">/volume2/docker/archicratie-web/releases/<ts>/app</text>
|
||||
<text
|
||||
x="58"
|
||||
y="762"
|
||||
class="mono"
|
||||
id="text18">current → pointe vers la release active</text>
|
||||
<text
|
||||
x="58"
|
||||
y="780"
|
||||
class="t"
|
||||
id="text19">build context docker = current OU release/app (selon compose)</text>
|
||||
<rect
|
||||
x="600"
|
||||
y="130"
|
||||
width="520"
|
||||
height="210"
|
||||
class="box"
|
||||
id="rect19" />
|
||||
<text
|
||||
x="618"
|
||||
y="160"
|
||||
class="h"
|
||||
id="text20">3) Build images</text>
|
||||
<text
|
||||
x="618"
|
||||
y="184"
|
||||
class="mono"
|
||||
id="text21">sudo env DOCKER_API_VERSION=1.43 \</text>
|
||||
<text
|
||||
x="618"
|
||||
y="202"
|
||||
class="mono"
|
||||
id="text22">docker compose -f docker-compose.yml build --no-cache \</text>
|
||||
<text
|
||||
x="618"
|
||||
y="220"
|
||||
class="mono"
|
||||
id="text23"> web_blue web_green</text>
|
||||
<text
|
||||
x="618"
|
||||
y="238"
|
||||
class="s"
|
||||
id="text24">les 2 images doivent builder OK</text>
|
||||
<rect
|
||||
x="639.93951"
|
||||
y="378.7897"
|
||||
width="480.06052"
|
||||
height="241.21028"
|
||||
class="box2"
|
||||
id="rect24" />
|
||||
<text
|
||||
x="658"
|
||||
y="410"
|
||||
class="h"
|
||||
id="text25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">4) Switch trafic (blue ↔ green)</text>
|
||||
<text
|
||||
x="658"
|
||||
y="434"
|
||||
class="t"
|
||||
id="text26"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">déterminer couleur active (proxy / conf)</text>
|
||||
<text
|
||||
x="658"
|
||||
y="452"
|
||||
class="t"
|
||||
id="text27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">mettre à jour routing vers l'autre couleur</text>
|
||||
<text
|
||||
x="658"
|
||||
y="470"
|
||||
class="t"
|
||||
id="text28"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">reload proxy, vérifier 200/302</text>
|
||||
<text
|
||||
x="658"
|
||||
y="488"
|
||||
class="mono"
|
||||
id="text29"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">curl -sSI -H 'Host: staging.*' http://127.0.0.1:18080/</text>
|
||||
<text
|
||||
x="658"
|
||||
y="506"
|
||||
class="s"
|
||||
id="text30"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">rollback = revenir à l'ancienne couleur</text>
|
||||
<rect
|
||||
x="600"
|
||||
y="660"
|
||||
width="520"
|
||||
height="210"
|
||||
class="box"
|
||||
id="rect30" />
|
||||
<text
|
||||
x="618"
|
||||
y="690"
|
||||
class="h"
|
||||
id="text31">5) Hotfix de release → re-synchroniser Git (step8)</text>
|
||||
<text
|
||||
x="618"
|
||||
y="714"
|
||||
class="t"
|
||||
id="text32">A) NAS: find src -mtime -3 → liste fichiers</text>
|
||||
<text
|
||||
x="618"
|
||||
y="732"
|
||||
class="t"
|
||||
id="text33">B) NAS: tar -czf /tmp/hotfix.tgz -T liste</text>
|
||||
<text
|
||||
x="618"
|
||||
y="750"
|
||||
class="t"
|
||||
id="text34">C) sha256 + manifest, puis scp vers Mac</text>
|
||||
<text
|
||||
x="618"
|
||||
y="768"
|
||||
class="t"
|
||||
id="text35">D) Mac: tar -xzf → rsync --checksum vers repo</text>
|
||||
<text
|
||||
x="618"
|
||||
y="786"
|
||||
class="t"
|
||||
id="text36">E) commit sur branche dédiée → push → PR vers main</text>
|
||||
<rect
|
||||
x="1180"
|
||||
y="160"
|
||||
width="380"
|
||||
height="520"
|
||||
class="box2"
|
||||
id="rect36" />
|
||||
<text
|
||||
x="1198"
|
||||
y="190"
|
||||
class="h"
|
||||
id="text37">Arborescence NAS (rappel)</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="214"
|
||||
class="mono"
|
||||
id="text38">/volume2/docker/archicratie-web/</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="232"
|
||||
class="mono"
|
||||
id="text39"> releases/</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="250"
|
||||
class="mono"
|
||||
id="text40"> 20260219-103222/</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="268"
|
||||
class="mono"
|
||||
id="text41"> app/ (ctx build)</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="286"
|
||||
class="mono"
|
||||
id="text42"> current -> releases/…/app</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="304"
|
||||
class="mono"
|
||||
id="text43"> compose/ docker-compose.yml</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="322"
|
||||
class="s"
|
||||
id="text44">compose.expanded.yml t'indique le build.context effectif</text>
|
||||
<path
|
||||
d="M 540,520 H 642.36006"
|
||||
class="arrowA"
|
||||
id="path44"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<text
|
||||
x="545"
|
||||
y="500"
|
||||
class="s"
|
||||
id="text45">→ NAS + build</text>
|
||||
<path
|
||||
d="M860 340 C860 360 860 360 860 380"
|
||||
class="arrowA"
|
||||
id="path45" />
|
||||
<text
|
||||
x="875"
|
||||
y="365"
|
||||
class="s"
|
||||
id="text46">images OK</text>
|
||||
<path
|
||||
d="M860 620 C860 640 860 640 860 660"
|
||||
class="arrowG"
|
||||
id="path46" />
|
||||
<text
|
||||
x="875"
|
||||
y="645"
|
||||
class="s"
|
||||
id="text47">si hotfix</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="20"
|
||||
width="1520"
|
||||
height="80"
|
||||
class="box2"
|
||||
id="rect47" />
|
||||
<text
|
||||
x="58"
|
||||
y="50"
|
||||
class="h"
|
||||
id="text48">Règle d'or</text>
|
||||
<text
|
||||
x="58"
|
||||
y="74"
|
||||
class="t"
|
||||
id="text49">La release doit être reproductible depuis Git. Toute modif manuelle en prod doit finir: (a) re-sync dans une branche, (b) PR, (c) merge, (d) prochaine release propre.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,551 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="1020"
|
||||
viewBox="0 0 1600 1020"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="archicratie-web-edition-blue-green-runbook-verbatim.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
inkscape:export-filename="out/archicratie-web-edition-blue-green-runbook-verbatim.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0606602"
|
||||
inkscape:cx="675.05126"
|
||||
inkscape:cy="300.28467"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="222"
|
||||
inkscape:window-y="74"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<style
|
||||
id="style1"><![CDATA[
|
||||
.bg { fill: #fff; }
|
||||
.outer { fill: #fff; stroke: #111; stroke-width: 3; rx: 18; }
|
||||
.box { fill: #fff; stroke: #111; stroke-width: 2; rx: 14; }
|
||||
.ok { fill: #eafff1; stroke: #1a7f37; stroke-width: 2; rx: 14; }
|
||||
.warn { fill: #fff0f0; stroke: #b42318; stroke-width: 2; rx: 14; }
|
||||
.title { font: 800 40px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
|
||||
.subtitle { font: 500 16px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #333; }
|
||||
.h2 { font: 800 24px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
|
||||
.h3 { font: 800 20px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
|
||||
.txt { font: 500 16px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
|
||||
.small { font: 500 12px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #333; }
|
||||
.mono { font: 500 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill: #111; }
|
||||
.arrow { stroke: #111; stroke-width: 2; fill: none; marker-end: url(#arrow); }
|
||||
.arrowThin { stroke: #111; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }
|
||||
.cap { stroke-linecap: round; stroke-linejoin: round; }
|
||||
]]></style>
|
||||
<marker
|
||||
id="arrow"
|
||||
viewBox="0 0 10 10"
|
||||
refX="9"
|
||||
refY="5"
|
||||
markerWidth="8"
|
||||
markerHeight="8"
|
||||
orient="auto-start-reverse">
|
||||
<path
|
||||
d="M 0 0 L 10 5 L 0 10 z"
|
||||
fill="#111"
|
||||
id="path1" />
|
||||
</marker>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<!-- Title -->
|
||||
<text
|
||||
x="40"
|
||||
y="55"
|
||||
class="title"
|
||||
id="text1">Archicratie – Web Edition : Blue/Green Runbook visuel (VERBATIM)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="88"
|
||||
class="subtitle"
|
||||
id="text2">Cible : déployer une nouvelle version sur le slot inactif (8081/8082), basculer via Traefik (dynamic/20-archicratie-backend.yml), vérifier (smoke tests), rollback en 30s si besoin.</text>
|
||||
<!-- Outer frame -->
|
||||
<rect
|
||||
x="30"
|
||||
y="110"
|
||||
width="1540"
|
||||
height="870"
|
||||
class="outer"
|
||||
id="rect2" />
|
||||
<!-- Invariants box -->
|
||||
<rect
|
||||
x="60"
|
||||
y="140"
|
||||
width="1480"
|
||||
height="130"
|
||||
class="box"
|
||||
id="rect3" />
|
||||
<text
|
||||
x="90"
|
||||
y="175"
|
||||
class="h2"
|
||||
id="text3">Invariants (ce qui évite de casser la prod)</text>
|
||||
<text
|
||||
x="90"
|
||||
y="198"
|
||||
class="txt"
|
||||
id="text4">• Les 2 slots existent en parallèle : archicratie-web-blue = 127.0.0.1:8081 et archicratie-web-green = 127.0.0.1:8082.</text>
|
||||
<text
|
||||
x="90"
|
||||
y="220"
|
||||
class="txt"
|
||||
id="text5">• Traefik edge écoute :18080 et choisit le slot LIVE via /volume2/docker/edge/config/dynamic/20-archicratie-backend.yml.</text>
|
||||
<text
|
||||
x="90"
|
||||
y="242"
|
||||
class="txt"
|
||||
id="text7">• Une seule cible active dans Traefik (pas de load-balance non déterministe). Rollback = remettre l’URL précédente dans le même fichier.</text>
|
||||
<!-- RIGUEUR ABSOLUE (ajout non destructif) -->
|
||||
<rect
|
||||
x="64"
|
||||
y="269"
|
||||
width="1480"
|
||||
height="30"
|
||||
rx="12"
|
||||
class="warn"
|
||||
id="rect-rigueur" />
|
||||
<text
|
||||
x="84"
|
||||
y="289"
|
||||
class="txt"
|
||||
id="text-rigueur"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">• RIGUEUR ABSOLUE : STAGING = slot INACTIF (opposé au LIVE). LIVE = 20-archicratie-backend.yml ; STAGING = 21-archicratie-staging.yml.</text>
|
||||
<!-- Step columns -->
|
||||
<!-- Column 1 -->
|
||||
<rect
|
||||
x="60"
|
||||
y="300"
|
||||
width="470"
|
||||
height="610"
|
||||
class="box"
|
||||
id="rect4" />
|
||||
<text
|
||||
x="90"
|
||||
y="335"
|
||||
class="h2"
|
||||
id="text8">Étape 1 — Build & déployer</text>
|
||||
<text
|
||||
x="90"
|
||||
y="365"
|
||||
class="h3"
|
||||
id="text9">But</text>
|
||||
<text
|
||||
x="90"
|
||||
y="387"
|
||||
class="txt"
|
||||
id="text10">Mettre la nouvelle version sur le slot inactif</text>
|
||||
<text
|
||||
x="90"
|
||||
y="409"
|
||||
class="txt"
|
||||
id="text11">(sans toucher au slot LIVE actuel).</text>
|
||||
<!-- Où box -->
|
||||
<rect
|
||||
x="90"
|
||||
y="435"
|
||||
width="410"
|
||||
height="210"
|
||||
class="box"
|
||||
id="rect5" />
|
||||
<text
|
||||
x="110"
|
||||
y="465"
|
||||
class="h3"
|
||||
id="text12">Où</text>
|
||||
<text
|
||||
x="110"
|
||||
y="490"
|
||||
class="mono"
|
||||
id="text13">/volume2/docker/archicratie-web/current/</text>
|
||||
<text
|
||||
x="110"
|
||||
y="515"
|
||||
class="txt"
|
||||
id="text14">Compose : docker-compose.yml</text>
|
||||
<text
|
||||
x="110"
|
||||
y="545"
|
||||
class="txt"
|
||||
id="text15">Slots :</text>
|
||||
<text
|
||||
x="140"
|
||||
y="568"
|
||||
class="mono"
|
||||
id="text16">web_blue → 127.0.0.1:8081</text>
|
||||
<text
|
||||
x="140"
|
||||
y="590"
|
||||
class="mono"
|
||||
id="text17">web_green → 127.0.0.1:8082</text>
|
||||
<text
|
||||
x="110"
|
||||
y="622"
|
||||
class="small"
|
||||
id="text18">Le build injecte aussi PUBLIC_GITEA_* via build args</text>
|
||||
<text
|
||||
x="110"
|
||||
y="640"
|
||||
class="small"
|
||||
id="text19">(déjà dans ton compose).</text>
|
||||
<!-- Commands box -->
|
||||
<rect
|
||||
x="90"
|
||||
y="665"
|
||||
width="410"
|
||||
height="225"
|
||||
class="box"
|
||||
id="rect6" />
|
||||
<text
|
||||
x="110"
|
||||
y="695"
|
||||
class="h3"
|
||||
id="text20">Commandes (safe)</text>
|
||||
<text
|
||||
x="110"
|
||||
y="720"
|
||||
class="txt"
|
||||
id="text21">1) Choisir le slot cible (inactif)</text>
|
||||
<text
|
||||
x="100"
|
||||
y="742"
|
||||
class="small"
|
||||
id="text22"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Astuce : le LIVE = ce que pointe Traefik dans 20-archicratie-backend.yml</text>
|
||||
<text
|
||||
x="110"
|
||||
y="770"
|
||||
class="txt"
|
||||
id="text23">2) Build + redémarrer uniquement ce slot</text>
|
||||
<text
|
||||
x="130"
|
||||
y="794"
|
||||
class="mono"
|
||||
id="text24">cd /volume2/docker/archicratie-web/current</text>
|
||||
<text
|
||||
x="130"
|
||||
y="814"
|
||||
class="mono"
|
||||
id="text25">sudo docker compose build web_green</text>
|
||||
<text
|
||||
x="130"
|
||||
y="834"
|
||||
class="mono"
|
||||
id="text26">sudo docker compose up -d --no-deps web_green</text>
|
||||
<text
|
||||
x="110"
|
||||
y="862"
|
||||
class="small"
|
||||
id="text27">Remplace web_green par web_blue selon la cible.</text>
|
||||
<text
|
||||
x="110"
|
||||
y="880"
|
||||
class="small"
|
||||
id="text28">Ne pas toucher l’autre service.</text>
|
||||
<!-- Arrow to column 2 -->
|
||||
<path
|
||||
d="M 530 605 L 560 605"
|
||||
class="arrow cap"
|
||||
id="path6" />
|
||||
<!-- Column 2 -->
|
||||
<rect
|
||||
x="560"
|
||||
y="300"
|
||||
width="470"
|
||||
height="610"
|
||||
class="box"
|
||||
id="rect7" />
|
||||
<text
|
||||
x="590"
|
||||
y="335"
|
||||
class="h2"
|
||||
id="text29">Étape 2 — Switch Traefik (LIVE)</text>
|
||||
<text
|
||||
x="590"
|
||||
y="365"
|
||||
class="h3"
|
||||
id="text30">But</text>
|
||||
<text
|
||||
x="590"
|
||||
y="387"
|
||||
class="txt"
|
||||
id="text31">Basculer le LIVE en modifiant 1 fichier</text>
|
||||
<text
|
||||
x="590"
|
||||
y="409"
|
||||
class="txt"
|
||||
id="text32">et laisser Traefik recharger automatiquement.</text>
|
||||
<!-- Canon file box -->
|
||||
<rect
|
||||
x="565.48694"
|
||||
y="433.11438"
|
||||
width="458.08331"
|
||||
height="176.31372"
|
||||
class="box"
|
||||
id="rect8"
|
||||
ry="0" />
|
||||
<text
|
||||
x="610"
|
||||
y="465"
|
||||
class="h3"
|
||||
id="text33">Fichier canonique (LIVE switch)</text>
|
||||
<text
|
||||
x="572"
|
||||
y="490"
|
||||
class="mono"
|
||||
id="text34"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/volume2/docker/edge/config/dynamic/20-archicratie-backend.yml</text>
|
||||
<text
|
||||
x="610"
|
||||
y="520"
|
||||
class="txt"
|
||||
id="text35">Contient :</text>
|
||||
<text
|
||||
x="588"
|
||||
y="545"
|
||||
class="mono"
|
||||
id="text36"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http.services.archicratie_web.loadBalancer.servers[0].url</text>
|
||||
<text
|
||||
x="610"
|
||||
y="575"
|
||||
class="txt"
|
||||
id="text37">Ex : http://127.0.0.1:8082 (green)</text>
|
||||
<text
|
||||
x="610"
|
||||
y="597"
|
||||
class="txt"
|
||||
id="text38">ou http://127.0.0.1:8081 (blue)</text>
|
||||
<!-- Procedure warn box -->
|
||||
<rect
|
||||
x="567.37256"
|
||||
y="621.88562"
|
||||
width="454.31201"
|
||||
height="279.4281"
|
||||
class="warn"
|
||||
id="rect9" />
|
||||
<text
|
||||
x="610"
|
||||
y="644"
|
||||
class="h3"
|
||||
id="text39"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:20px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Procédure (anti-casse)</text>
|
||||
<text
|
||||
x="576"
|
||||
y="668"
|
||||
class="txt"
|
||||
id="text40"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">1) Backup horodaté du fichier</text>
|
||||
<text
|
||||
x="591"
|
||||
y="690"
|
||||
class="mono"
|
||||
id="text41"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">cd /volume2/docker/edge/config/dynamic</text>
|
||||
<text
|
||||
x="577"
|
||||
y="712"
|
||||
class="mono"
|
||||
id="text42"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2"
|
||||
x="577"
|
||||
y="712">sudo cp 20-archicratie-backend.yml</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3"
|
||||
x="577"
|
||||
y="727">20-archicratie-backend.yml.bak.$(date +%F-%H%M%S)</tspan></text>
|
||||
<text
|
||||
x="576"
|
||||
y="752"
|
||||
class="txt"
|
||||
id="text43"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">2) Éditer l’URL (un seul backend)</text>
|
||||
<text
|
||||
x="591"
|
||||
y="774"
|
||||
class="mono"
|
||||
id="text44"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">sudo vi 20-archicratie-backend.yml</text>
|
||||
<text
|
||||
x="591"
|
||||
y="788"
|
||||
class="small"
|
||||
id="text47"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Changer uniquement la valeur url : 8081 ↔ 8082</text>
|
||||
<text
|
||||
x="571"
|
||||
y="810"
|
||||
class="txt"
|
||||
id="text45bis"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4"
|
||||
x="571"
|
||||
y="810">2bis) Mettre à jour 21-archicratie-staging.yml sur l’autre port</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5"
|
||||
x="571"
|
||||
y="830">(opposé au LIVE)</tspan></text>
|
||||
<text
|
||||
x="571"
|
||||
y="856"
|
||||
class="txt"
|
||||
id="text45"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">3) Traefik recharge (watch=true)</text>
|
||||
<text
|
||||
x="591"
|
||||
y="878"
|
||||
class="small"
|
||||
id="text46"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan71"
|
||||
x="591"
|
||||
y="878">Pas de restart requis si provider file watch=true</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan72"
|
||||
x="591"
|
||||
y="893">(ton traefik.yml).</tspan></text>
|
||||
<!-- Arrow to column 3 -->
|
||||
<path
|
||||
d="M 1030 605 L 1060 605"
|
||||
class="arrow cap"
|
||||
id="path7" />
|
||||
<!-- Column 3 -->
|
||||
<rect
|
||||
x="1060"
|
||||
y="300"
|
||||
width="480"
|
||||
height="610"
|
||||
class="box"
|
||||
id="rect10" />
|
||||
<text
|
||||
x="1090"
|
||||
y="335"
|
||||
class="h2"
|
||||
id="text48">Étape 3 — Smoke tests</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="365"
|
||||
class="h3"
|
||||
id="text49">But</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="387"
|
||||
class="txt"
|
||||
id="text50">Prouver que le nouveau LIVE répond,</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="409"
|
||||
class="txt"
|
||||
id="text51">et que l’auth (Authelia/whoami) est OK.</text>
|
||||
<!-- Slot direct ok box -->
|
||||
<rect
|
||||
x="1090"
|
||||
y="435"
|
||||
width="420"
|
||||
height="185"
|
||||
class="ok"
|
||||
id="rect11" />
|
||||
<text
|
||||
x="1110"
|
||||
y="465"
|
||||
class="h3"
|
||||
id="text52">Tests “slot direct” (preuve build)</text>
|
||||
<text
|
||||
x="1110"
|
||||
y="490"
|
||||
class="txt"
|
||||
id="text53">Le slot construit doit répondre en 200 :</text>
|
||||
<text
|
||||
x="1130"
|
||||
y="515"
|
||||
class="mono"
|
||||
id="text54">curl -sS -I http://127.0.0.1:8081/ | head -n 12</text>
|
||||
<text
|
||||
x="1130"
|
||||
y="540"
|
||||
class="mono"
|
||||
id="text55">curl -sS -I http://127.0.0.1:8082/ | head -n 12</text>
|
||||
<text
|
||||
x="1110"
|
||||
y="570"
|
||||
class="small"
|
||||
id="text56">L’un des deux peut rester l’ancien LIVE.</text>
|
||||
<text
|
||||
x="1110"
|
||||
y="590"
|
||||
class="small"
|
||||
id="text57">L’objectif est que le slot cible soit OK.</text>
|
||||
<!-- Edge tests box -->
|
||||
<rect
|
||||
x="1090"
|
||||
y="638.4342"
|
||||
width="420"
|
||||
height="251.56581"
|
||||
class="box"
|
||||
id="rect54" />
|
||||
<text
|
||||
x="1110"
|
||||
y="675"
|
||||
class="h3"
|
||||
id="text57b"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:20px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Tests “edge” (preuve routage + auth)</text>
|
||||
<text
|
||||
x="1110"
|
||||
y="700"
|
||||
class="txt"
|
||||
id="text57c"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Host rules : tester AVEC Host header :</text>
|
||||
<text
|
||||
x="1130"
|
||||
y="724"
|
||||
class="mono"
|
||||
id="text57d"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan57d1"
|
||||
x="1130"
|
||||
y="724">curl -sS -I -H 'Host:</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan57d2"
|
||||
x="1130"
|
||||
y="739">archicratie.trans-hands.synology.me'</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="1130"
|
||||
y="754"
|
||||
id="tspan57d3">http://127.0.0.1:18080/ | head -n 20</tspan></text>
|
||||
<text
|
||||
x="1130"
|
||||
y="780"
|
||||
class="mono"
|
||||
id="text58"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
|
||||
id="tspan1" /></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,437 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="900"
|
||||
viewBox="0 0 1600 900"
|
||||
version="1.1"
|
||||
id="svg43"
|
||||
sodipodi:docname="archicratie-web-edition-edge-routing-verbatim-v2.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview43"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="424.81089"
|
||||
inkscape:cy="464.14523"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg43" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<!-- Fond clair lisible partout -->
|
||||
<linearGradient
|
||||
id="bg"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ffffff"
|
||||
id="stop1" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#f1f5f9"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<!-- Flèches -->
|
||||
<marker
|
||||
id="arrow"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#334155"
|
||||
id="path2" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowA"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#2563eb"
|
||||
id="path3" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowG"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#059669"
|
||||
id="path4" />
|
||||
</marker>
|
||||
<!-- Styles SANS variables CSS (compat max) -->
|
||||
<style
|
||||
id="style4"><![CDATA[
|
||||
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
|
||||
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
|
||||
|
||||
/* Cadres lisibles */
|
||||
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
|
||||
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
|
||||
|
||||
/* Pills */
|
||||
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
|
||||
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
|
||||
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
|
||||
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
|
||||
|
||||
/* Traits / flèches */
|
||||
.line{stroke:#334155;stroke-width:1.4;fill:none}
|
||||
.dash{stroke-dasharray:6 6}
|
||||
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
|
||||
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
|
||||
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
|
||||
]]></style>
|
||||
</defs>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="1600"
|
||||
height="900"
|
||||
fill="url(#bg)"
|
||||
id="rect4" />
|
||||
<text
|
||||
x="40"
|
||||
y="56"
|
||||
class="title"
|
||||
id="text4">Archicratie — Edge routing (v2, verbatim)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="84"
|
||||
class="subtitle"
|
||||
id="text5">Mise à jour 2026-02-20 — Host routing + Authelia + Blue/Green web_*</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="140"
|
||||
width="420"
|
||||
height="180"
|
||||
class="box"
|
||||
id="rect5" />
|
||||
<text
|
||||
x="58"
|
||||
y="170"
|
||||
class="h"
|
||||
id="text6">Client (navigateur)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="194"
|
||||
class="t"
|
||||
id="text7">https://archicratie.* / https://staging.archicratie.*</text>
|
||||
<text
|
||||
x="58"
|
||||
y="212"
|
||||
class="t"
|
||||
id="text8">cookies authelia_session</text>
|
||||
<text
|
||||
x="58"
|
||||
y="230"
|
||||
class="s"
|
||||
id="text9">HEAD/GET → 302 si non auth</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="320"
|
||||
width="110"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect9" />
|
||||
<text
|
||||
x="74"
|
||||
y="339"
|
||||
class="mono"
|
||||
id="text10">HTTPS 443</text>
|
||||
<rect
|
||||
x="520"
|
||||
y="120"
|
||||
width="520"
|
||||
height="260"
|
||||
class="box"
|
||||
id="rect10" />
|
||||
<text
|
||||
x="538"
|
||||
y="150"
|
||||
class="h"
|
||||
id="text11">Reverse-proxy (Nginx / DSM)</text>
|
||||
<text
|
||||
x="538"
|
||||
y="174"
|
||||
class="t"
|
||||
id="text12">Routage par Host</text>
|
||||
<text
|
||||
x="538"
|
||||
y="192"
|
||||
class="t"
|
||||
id="text13">auth_request → Authelia</text>
|
||||
<text
|
||||
x="538"
|
||||
y="210"
|
||||
class="t"
|
||||
id="text14">proxy_pass → service web_blue ou web_green</text>
|
||||
<text
|
||||
x="538"
|
||||
y="228"
|
||||
class="t"
|
||||
id="text15">headers: X-Forwarded-* + Host</text>
|
||||
<text
|
||||
x="538"
|
||||
y="246"
|
||||
class="s"
|
||||
id="text16">en local: curl -H 'Host: ...' http://127.0.0.1:18080/</text>
|
||||
<rect
|
||||
x="540"
|
||||
y="320"
|
||||
width="142"
|
||||
height="28"
|
||||
class="chip"
|
||||
id="rect16" />
|
||||
<text
|
||||
x="554"
|
||||
y="339"
|
||||
class="mono"
|
||||
id="text17">auth_request</text>
|
||||
<rect
|
||||
x="520"
|
||||
y="420"
|
||||
width="520"
|
||||
height="210"
|
||||
class="box2"
|
||||
id="rect17" />
|
||||
<text
|
||||
x="538"
|
||||
y="450"
|
||||
class="h"
|
||||
id="text18">Auth stack</text>
|
||||
<text
|
||||
x="538"
|
||||
y="474"
|
||||
class="t"
|
||||
id="text19">Authelia (portal login)</text>
|
||||
<text
|
||||
x="538"
|
||||
y="492"
|
||||
class="t"
|
||||
id="text20">LLDAP (backend LDAP)</text>
|
||||
<text
|
||||
x="538"
|
||||
y="510"
|
||||
class="t"
|
||||
id="text21">Redis (sessions / storage)</text>
|
||||
<text
|
||||
x="538"
|
||||
y="528"
|
||||
class="s"
|
||||
id="text22">auth.* domain</text>
|
||||
<rect
|
||||
x="540"
|
||||
y="600"
|
||||
width="250"
|
||||
height="28"
|
||||
class="chipW"
|
||||
id="rect22" />
|
||||
<text
|
||||
x="554"
|
||||
y="619"
|
||||
class="mono"
|
||||
id="text23">302 → auth.*?rd=...</text>
|
||||
<rect
|
||||
x="1175.0378"
|
||||
y="237.57942"
|
||||
width="384.96219"
|
||||
height="162.42058"
|
||||
class="box"
|
||||
id="rect23" />
|
||||
<text
|
||||
x="1198"
|
||||
y="270"
|
||||
class="h"
|
||||
id="text24"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Service web_blue (container)</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="294"
|
||||
class="mono"
|
||||
id="text25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">nginx static (dist/)</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="312"
|
||||
class="mono"
|
||||
id="text26"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">/pagefind/*, /assets/*</text>
|
||||
<rect
|
||||
x="1172.6172"
|
||||
y="417.57944"
|
||||
width="387.38275"
|
||||
height="162.42058"
|
||||
class="box"
|
||||
id="rect26" />
|
||||
<text
|
||||
x="1198"
|
||||
y="450"
|
||||
class="h"
|
||||
id="text27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Service web_green (container)</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="474"
|
||||
class="mono"
|
||||
id="text28"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">nginx static (dist/)</text>
|
||||
<text
|
||||
x="1198"
|
||||
y="492"
|
||||
class="mono"
|
||||
id="text29"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">build identique, couleur swap</text>
|
||||
<rect
|
||||
x="1120"
|
||||
y="600"
|
||||
width="230"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect29" />
|
||||
<text
|
||||
x="1134"
|
||||
y="619"
|
||||
class="mono"
|
||||
id="text30"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">une seule couleur active</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="410"
|
||||
width="420"
|
||||
height="220"
|
||||
class="box2"
|
||||
id="rect30" />
|
||||
<text
|
||||
x="58"
|
||||
y="440"
|
||||
class="h"
|
||||
id="text31">Atelier DEV (local)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="464"
|
||||
class="mono"
|
||||
id="text32">astro dev : http://localhost:4321</text>
|
||||
<text
|
||||
x="58"
|
||||
y="482"
|
||||
class="mono"
|
||||
id="text33">pas d'authelia</text>
|
||||
<text
|
||||
x="58"
|
||||
y="500"
|
||||
class="mono"
|
||||
id="text34">predev génère:</text>
|
||||
<text
|
||||
x="58"
|
||||
y="518"
|
||||
class="mono"
|
||||
id="text35"> /annotations-index.json</text>
|
||||
<text
|
||||
x="58"
|
||||
y="536"
|
||||
class="mono"
|
||||
id="text36"> /para-index.json</text>
|
||||
<text
|
||||
x="58"
|
||||
y="554"
|
||||
class="s"
|
||||
id="text37">404 = index manquant (relancer predev/dev)</text>
|
||||
<path
|
||||
d="M460 230 C500 230 500 230 520 230"
|
||||
class="arrow"
|
||||
id="path37" />
|
||||
<text
|
||||
x="465"
|
||||
y="210"
|
||||
class="s"
|
||||
id="text38"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
|
||||
style="font-size:14.6667px"
|
||||
id="tspan45">requête</tspan></text>
|
||||
<path
|
||||
d="M780 380 C780 410 780 410 780 420"
|
||||
class="arrow"
|
||||
id="path38" />
|
||||
<text
|
||||
x="795"
|
||||
y="410"
|
||||
class="s"
|
||||
id="text39">auth_request</text>
|
||||
<path
|
||||
d="m 1040,332 h 132.6172"
|
||||
class="arrowA"
|
||||
id="path39"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
d="m 1040,464 131.407,2.42057"
|
||||
class="arrowA"
|
||||
id="path40"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<text
|
||||
x="1045"
|
||||
y="317"
|
||||
class="s"
|
||||
id="text40"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
|
||||
style="font-size:14.6667px"
|
||||
id="tspan44">proxy_pass (active)</tspan></text>
|
||||
<path
|
||||
d="M780 630 C780 700 340 700 250 630"
|
||||
class="arrow"
|
||||
id="path41" />
|
||||
<text
|
||||
x="462.36005"
|
||||
y="707.89716"
|
||||
class="s"
|
||||
id="text41"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
|
||||
id="tspan43"
|
||||
style="font-size:14.6667px">callback + cookie</tspan></text>
|
||||
<rect
|
||||
x="40"
|
||||
y="820"
|
||||
width="1520"
|
||||
height="60"
|
||||
class="box2"
|
||||
id="rect41" />
|
||||
<text
|
||||
x="58"
|
||||
y="850"
|
||||
class="h"
|
||||
id="text42">Note importante (debug)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="874"
|
||||
class="s"
|
||||
id="text43">Si tu testes via loopback (127.0.0.1:18080), la directive Host détermine la vhost. Sans Host correct, tu peux tomber sur une autre conf.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
324
docs/diagrams/archicratie-web-edition-edge-routing-verbatim.svg
Normal file
@@ -0,0 +1,324 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="820"
|
||||
viewBox="0 0 1600 820"
|
||||
version="1.1"
|
||||
id="svg36"
|
||||
sodipodi:docname="archicratie-web-edition-edge-routing-verbatim.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
inkscape:export-filename="out/archicratie-web-edition-edge-routing-verbatim.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview36"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="665.65809"
|
||||
inkscape:cy="354.00908"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg36" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<marker
|
||||
id="arrow"
|
||||
viewBox="0 0 10 10"
|
||||
refX="9.5"
|
||||
refY="5"
|
||||
markerWidth="8"
|
||||
markerHeight="8"
|
||||
orient="auto-start-reverse">
|
||||
<path
|
||||
d="M 0 0 L 10 5 L 0 10 z"
|
||||
fill="#222"
|
||||
id="path1" />
|
||||
</marker>
|
||||
<style
|
||||
id="style1">
|
||||
.title { font: 700 22px sans-serif; fill:#111; }
|
||||
.small { font: 12px sans-serif; fill:#111; }
|
||||
.h2 { font: 700 16px sans-serif; fill:#111; }
|
||||
.txt { font: 13px sans-serif; fill:#111; }
|
||||
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
|
||||
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
|
||||
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
|
||||
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
|
||||
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
|
||||
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<text
|
||||
x="40"
|
||||
y="45"
|
||||
class="title"
|
||||
id="text1">Edge Traefik (verbatim) — routers Host(...) + middlewares + services</text>
|
||||
<text
|
||||
x="40"
|
||||
y="75"
|
||||
class="small"
|
||||
id="text2">Source : /volume2/docker/edge/config/dynamic/10-core.yml + 20-archicratie-backend.yml + 21-archicratie-staging.yml + 30-lldap-ui.yml</text>
|
||||
<rect
|
||||
x="35"
|
||||
y="110"
|
||||
width="1530"
|
||||
height="670"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect2" />
|
||||
<text
|
||||
x="60"
|
||||
y="145"
|
||||
class="h2"
|
||||
id="text3">Traefik : entryPoint web = :18080 — provider file (dynamic/) watch=true</text>
|
||||
<!-- Middlewares -->
|
||||
<rect
|
||||
x="60"
|
||||
y="185"
|
||||
width="601.60364"
|
||||
height="196.36914"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect3" />
|
||||
<text
|
||||
x="80"
|
||||
y="215"
|
||||
class="h2"
|
||||
id="text4">Middlewares (10-core.yml)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="242"
|
||||
class="txt"
|
||||
id="text5">sanitize-remote : purge Remote-* + force X-Forwarded-Proto/Port</text>
|
||||
<text
|
||||
x="80"
|
||||
y="264"
|
||||
class="txt"
|
||||
id="text6">authelia : forwardAuth → <tspan
|
||||
class="mono"
|
||||
id="tspan5">http://127.0.0.1:9091/api/authz/forward-auth</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="286"
|
||||
class="txt"
|
||||
id="text7">chain-auth : [sanitize-remote, authelia]</text>
|
||||
<!-- Routers -->
|
||||
<rect
|
||||
x="60"
|
||||
y="415"
|
||||
width="823.08624"
|
||||
height="205.02269"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect7" />
|
||||
<text
|
||||
x="80"
|
||||
y="445"
|
||||
class="h2"
|
||||
id="text8">Routers</text>
|
||||
<text
|
||||
x="80"
|
||||
y="472"
|
||||
class="mono"
|
||||
id="text9">archicratie</text>
|
||||
<text
|
||||
x="200"
|
||||
y="472"
|
||||
class="txt"
|
||||
id="text10">Host(archicratie.trans-hands.synology.me) + chain-auth → service archicratie_web</text>
|
||||
<text
|
||||
x="80"
|
||||
y="498"
|
||||
class="mono"
|
||||
id="text11">archicratie-authinfo</text>
|
||||
<text
|
||||
x="290"
|
||||
y="498"
|
||||
class="txt"
|
||||
id="text12">Host(archicratie…) PathPrefix(/_auth/whoami) + chain-auth → whoami</text>
|
||||
<text
|
||||
x="80"
|
||||
y="524"
|
||||
class="mono"
|
||||
id="text13">gitea</text>
|
||||
<text
|
||||
x="200"
|
||||
y="524"
|
||||
class="txt"
|
||||
id="text14">Host(gitea.archicratie.trans-hands.synology.me) + sanitize-remote → gitea_web</text>
|
||||
<text
|
||||
x="80"
|
||||
y="550"
|
||||
class="mono"
|
||||
id="text15">archicratie-staging</text>
|
||||
<text
|
||||
x="290"
|
||||
y="550"
|
||||
class="txt"
|
||||
id="text16">Host(staging.archicratie.trans-hands.synology.me) + chain-auth → archicratie_blue</text>
|
||||
<text
|
||||
x="80"
|
||||
y="576"
|
||||
class="mono"
|
||||
id="text17">lldap-ui</text>
|
||||
<text
|
||||
x="200"
|
||||
y="576"
|
||||
class="txt"
|
||||
id="text18">Host(lldap.archicratie.trans-hands.synology.me) + chain-auth → lldap_ui</text>
|
||||
<rect
|
||||
x="925.50684"
|
||||
y="181.36914"
|
||||
width="614.49316"
|
||||
height="553.63086"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect18" />
|
||||
<text
|
||||
x="985"
|
||||
y="215"
|
||||
class="h2"
|
||||
id="text19"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Services (loadBalancer → url)</text>
|
||||
<text
|
||||
x="985"
|
||||
y="250"
|
||||
class="mono"
|
||||
id="text20"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">whoami</text>
|
||||
<text
|
||||
x="1120"
|
||||
y="250"
|
||||
class="txt"
|
||||
id="text21"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ <tspan
|
||||
class="mono"
|
||||
id="tspan20"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:18081</tspan> (edge-whoami)</text>
|
||||
<text
|
||||
x="985"
|
||||
y="285"
|
||||
class="mono"
|
||||
id="text22"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea_web</text>
|
||||
<text
|
||||
x="1120"
|
||||
y="285"
|
||||
class="txt"
|
||||
id="text23"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ <tspan
|
||||
class="mono"
|
||||
id="tspan22"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:3000</tspan> (Gitea)</text>
|
||||
<text
|
||||
x="985"
|
||||
y="320"
|
||||
class="mono"
|
||||
id="text24"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">archicratie_web</text>
|
||||
<text
|
||||
x="1120"
|
||||
y="320"
|
||||
class="txt"
|
||||
id="text25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ défini par <tspan
|
||||
class="mono"
|
||||
id="tspan24"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">20-archicratie-backend.yml</tspan></text>
|
||||
<text
|
||||
x="1140"
|
||||
y="345"
|
||||
class="txt"
|
||||
id="text26"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• actuel : <tspan
|
||||
class="mono"
|
||||
id="tspan25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:8082</tspan> (green)</text>
|
||||
<text
|
||||
x="985"
|
||||
y="390"
|
||||
class="mono"
|
||||
id="text27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">archicratie_blue</text>
|
||||
<text
|
||||
x="1170"
|
||||
y="390"
|
||||
class="txt"
|
||||
id="text28"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ <tspan
|
||||
class="mono"
|
||||
id="tspan27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:8081</tspan> (staging)</text>
|
||||
<text
|
||||
x="985"
|
||||
y="435"
|
||||
class="mono"
|
||||
id="text29"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">lldap_ui</text>
|
||||
<text
|
||||
x="1120"
|
||||
y="435"
|
||||
class="txt"
|
||||
id="text30"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ <tspan
|
||||
class="mono"
|
||||
id="tspan29"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:17170</tspan> (LLDAP UI)</text>
|
||||
<rect
|
||||
x="954.1377"
|
||||
y="493.94855"
|
||||
width="560.8623"
|
||||
height="216.05144"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect30" />
|
||||
<text
|
||||
x="975"
|
||||
y="530"
|
||||
class="h2"
|
||||
id="text31"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Interprétation debug (safe)</text>
|
||||
<text
|
||||
x="975"
|
||||
y="555"
|
||||
class="txt"
|
||||
id="text32"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si tu testes sans Host header sur :18080 → 404 (normal)</text>
|
||||
<text
|
||||
x="975"
|
||||
y="577"
|
||||
class="txt"
|
||||
id="text33"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si archicratie → 302 auth.* : Authelia forward-auth OK</text>
|
||||
<text
|
||||
x="975"
|
||||
y="599"
|
||||
class="txt"
|
||||
id="text34"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si /_auth/whoami → 302 auth.* : gate OK (non-auth)</text>
|
||||
<text
|
||||
x="975"
|
||||
y="621"
|
||||
class="txt"
|
||||
id="text35"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Pour basculer blue/green : modifier 20-archicratie-backend.yml (8081 ↔ 8082)</text>
|
||||
<text
|
||||
x="975"
|
||||
y="643"
|
||||
class="small"
|
||||
id="text36"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">But : une seule cible active (évite load-balance non déterministe).</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
870
docs/diagrams/archicratie-web-edition-git-ci-workflow-v1.svg
Normal file
@@ -0,0 +1,870 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1500"
|
||||
height="940"
|
||||
viewBox="0 0 1500 940"
|
||||
role="img"
|
||||
aria-label="Workflow Git CI - main protégé, PR, CI, release-pack, déploiement blue/green"
|
||||
version="1.1"
|
||||
id="svg93"
|
||||
sodipodi:docname="archicratie-web-edition-git-ci-workflow-v1.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview93"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.88133333"
|
||||
inkscape:cx="253.02572"
|
||||
inkscape:cy="536.11952"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg93" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<style
|
||||
id="style1">
|
||||
/* ✅ Version “Inkscape-safe” : pas de var(), pas de rgba() */
|
||||
.canvasBg { fill:#f8fafc; stroke:#e2e8f0; stroke-width:1; }
|
||||
|
||||
.title { font:800 26px/1.2 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; }
|
||||
.subtitle { font:600 14px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
|
||||
.laneTitle { font:800 14px/1 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; letter-spacing:.2px; }
|
||||
.laneNote { font:600 12px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
|
||||
.boxTitle { font:800 14px/1.2 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; }
|
||||
.boxText { font:600 12px/1.35 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
|
||||
.mono { font:700 11px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; fill:#334155; }
|
||||
|
||||
.lane { fill:#f1f5f9; fill-opacity:.70; stroke:#cbd5e1; stroke-width:1; }
|
||||
.laneAlt { fill:#e2e8f0; fill-opacity:.55; stroke:#cbd5e1; stroke-width:1; }
|
||||
|
||||
.box { fill:#ffffff; fill-opacity:.92; stroke:#94a3b8; stroke-width:1.4; }
|
||||
.boxAlt { fill:#f8fafc; fill-opacity:.92; stroke:#94a3b8; stroke-width:1.4; }
|
||||
|
||||
.tag { font:800 10px/1 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; }
|
||||
.tagOk { fill:#16a34a; }
|
||||
.tagWarn { fill:#f59e0b; }
|
||||
.tagInfo { fill:#2563eb; }
|
||||
.tagDanger { fill:#dc2626; }
|
||||
|
||||
.arrow { stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#arrowHead); }
|
||||
.arrowSoft { stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#arrowHeadSoft); }
|
||||
.dashed { stroke-dasharray:7 6; }
|
||||
|
||||
.callout { fill:#ffffff; fill-opacity:.70; stroke:#cbd5e1; stroke-width:1.1; }
|
||||
.small { font:600 11px/1.35 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
|
||||
</style>
|
||||
<marker
|
||||
id="arrowHead"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="5"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L10,5 L0,10 Z"
|
||||
fill="#64748b"
|
||||
id="path1" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowHeadSoft"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="5"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L10,5 L0,10 Z"
|
||||
fill="#94a3b8"
|
||||
id="path2" />
|
||||
</marker>
|
||||
</defs>
|
||||
<!-- ✅ Fond explicite + bordure douce -->
|
||||
<rect
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="1499"
|
||||
height="939"
|
||||
class="canvasBg"
|
||||
rx="26"
|
||||
id="rect2" />
|
||||
<!-- Header -->
|
||||
<text
|
||||
x="44"
|
||||
y="54"
|
||||
class="title"
|
||||
id="text2">Archicratie — Workflow Git “pro” (main protégé) + CI + Release + Blue/Green</text>
|
||||
<text
|
||||
x="44"
|
||||
y="82"
|
||||
class="subtitle"
|
||||
id="text3">
|
||||
Objectif : partir d’un hotfix appliqué (si besoin), le remettre proprement sous Git (branche → PR → CI → merge),
|
||||
puis produire une release packagée et déployer sans régression.
|
||||
</text>
|
||||
<!-- Lanes -->
|
||||
<rect
|
||||
x="40"
|
||||
y="115"
|
||||
width="440"
|
||||
height="770"
|
||||
class="lane"
|
||||
rx="18"
|
||||
id="rect3" />
|
||||
<rect
|
||||
x="520"
|
||||
y="115"
|
||||
width="430"
|
||||
height="770"
|
||||
class="laneAlt"
|
||||
rx="18"
|
||||
id="rect4" />
|
||||
<rect
|
||||
x="980"
|
||||
y="115"
|
||||
width="250"
|
||||
height="770"
|
||||
class="lane"
|
||||
rx="18"
|
||||
id="rect5" />
|
||||
<rect
|
||||
x="1250"
|
||||
y="115"
|
||||
width="210"
|
||||
height="770"
|
||||
class="laneAlt"
|
||||
rx="18"
|
||||
id="rect6" />
|
||||
<text
|
||||
x="60"
|
||||
y="142"
|
||||
class="laneTitle"
|
||||
id="text6"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Atelier DEV (Mac Studio)</text>
|
||||
<text
|
||||
x="60"
|
||||
y="164"
|
||||
class="laneNote"
|
||||
id="text7"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.4;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Travail local, build, commit, push de branche</text>
|
||||
<text
|
||||
x="540"
|
||||
y="148"
|
||||
class="laneTitle"
|
||||
id="text8">Gitea (remote)</text>
|
||||
<text
|
||||
x="540"
|
||||
y="170"
|
||||
class="laneNote"
|
||||
id="text9">main verrouillé · PR obligatoire · historique canon</text>
|
||||
<text
|
||||
x="1000"
|
||||
y="148"
|
||||
class="laneTitle"
|
||||
id="text10">CI (CI.yaml)</text>
|
||||
<text
|
||||
x="1000"
|
||||
y="170"
|
||||
class="laneNote"
|
||||
id="text11">build checks · gate de merge</text>
|
||||
<text
|
||||
x="1270"
|
||||
y="148"
|
||||
class="laneTitle"
|
||||
id="text12">NAS (Prod)</text>
|
||||
<text
|
||||
x="1270"
|
||||
y="170"
|
||||
class="laneNote"
|
||||
id="text13">release-pack + blue/green</text>
|
||||
<!-- Boxes: Mac -->
|
||||
<rect
|
||||
x="70"
|
||||
y="170"
|
||||
width="380"
|
||||
height="105"
|
||||
class="box"
|
||||
rx="16"
|
||||
id="rect13" />
|
||||
<text
|
||||
x="92"
|
||||
y="198"
|
||||
class="boxTitle"
|
||||
id="text14"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">1) Se baser sur main (canon)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="220"
|
||||
class="boxText"
|
||||
id="text15"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Synchroniser le dépôt local sur le dernier état validé.</text>
|
||||
<text
|
||||
x="92"
|
||||
y="242"
|
||||
class="mono"
|
||||
id="text16"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git checkout main git pull --ff-only</text>
|
||||
<text
|
||||
x="92"
|
||||
y="264"
|
||||
class="small"
|
||||
id="text17"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
class="tag tagInfo"
|
||||
id="tspan16"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">INFO</tspan> main est protégé : pas de commit direct.</text>
|
||||
<rect
|
||||
x="70"
|
||||
y="300"
|
||||
width="380"
|
||||
height="125"
|
||||
class="boxAlt"
|
||||
rx="16"
|
||||
id="rect17" />
|
||||
<text
|
||||
x="92"
|
||||
y="328"
|
||||
class="boxTitle"
|
||||
id="text18"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">2) Créer une branche dédiée “hotfix sync”</text>
|
||||
<text
|
||||
x="92"
|
||||
y="350"
|
||||
class="boxText"
|
||||
id="text19"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Nom explicite + date. Toute la synchro se fait ici.</text>
|
||||
<text
|
||||
x="92"
|
||||
y="372"
|
||||
class="mono"
|
||||
id="text20"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git checkout -b chore/step8-sync-hotfix-YYYYMMDD</text>
|
||||
<text
|
||||
x="92"
|
||||
y="394"
|
||||
class="small"
|
||||
id="text21"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan94"
|
||||
x="92"
|
||||
y="394"><tspan
|
||||
class="tag tagWarn"
|
||||
id="tspan20"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Optionnel : appliquer un pack hotfix (tar/sha/rsync)</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan95"
|
||||
x="92"
|
||||
y="408.85001">si prod a bougé.</tspan></text>
|
||||
<rect
|
||||
x="70"
|
||||
y="455"
|
||||
width="380"
|
||||
height="160"
|
||||
class="box"
|
||||
rx="16"
|
||||
id="rect21" />
|
||||
<text
|
||||
x="92"
|
||||
y="483"
|
||||
class="boxTitle"
|
||||
id="text22"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">3) Appliquer les changements vérifier</text>
|
||||
<text
|
||||
x="72"
|
||||
y="505"
|
||||
class="boxText"
|
||||
id="text23"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Copier/merge les fichiers (rsync/checksum), puis tester build/dev.</text>
|
||||
<text
|
||||
x="92"
|
||||
y="527"
|
||||
class="mono"
|
||||
id="text24"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">rm -rf .astro node_modules/.vite</text>
|
||||
<text
|
||||
x="92"
|
||||
y="548"
|
||||
class="mono"
|
||||
id="text25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm i npm run build</text>
|
||||
<text
|
||||
x="92"
|
||||
y="569"
|
||||
class="mono"
|
||||
id="text26"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm run dev</text>
|
||||
<text
|
||||
x="92"
|
||||
y="593"
|
||||
class="small"
|
||||
id="text27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
class="tag tagOk"
|
||||
id="tspan26"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">OK</tspan> On ne push que si build + postbuild passent.</text>
|
||||
<rect
|
||||
x="70"
|
||||
y="640"
|
||||
width="380"
|
||||
height="145"
|
||||
class="boxAlt"
|
||||
rx="16"
|
||||
id="rect27" />
|
||||
<text
|
||||
x="92"
|
||||
y="668"
|
||||
class="boxTitle"
|
||||
id="text28"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">4) Commit propre + diff lisible</text>
|
||||
<text
|
||||
x="92"
|
||||
y="690"
|
||||
class="boxText"
|
||||
id="text29"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Inspecter, puis commiter en message clair (hotfix étape X).</text>
|
||||
<text
|
||||
x="92"
|
||||
y="712"
|
||||
class="mono"
|
||||
id="text30"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git status git diff</text>
|
||||
<text
|
||||
x="92"
|
||||
y="733"
|
||||
class="mono"
|
||||
id="text31"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan96"
|
||||
x="92"
|
||||
y="733">git add -A git commit -m "step8: sync hotfix</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan97"
|
||||
x="92"
|
||||
y="747.84998">(SidePanel/reading)"</tspan></text>
|
||||
<text
|
||||
x="92"
|
||||
y="770"
|
||||
class="small"
|
||||
id="text32"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
class="tag tagInfo"
|
||||
id="tspan31"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">TIP</tspan> Garder le commit “gros” mais unique si c’est un backport prod.</text>
|
||||
<rect
|
||||
x="70"
|
||||
y="805"
|
||||
width="380"
|
||||
height="65"
|
||||
class="box"
|
||||
rx="16"
|
||||
id="rect32" />
|
||||
<text
|
||||
x="92"
|
||||
y="833"
|
||||
class="boxTitle"
|
||||
id="text33"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">5) Push de branche vers Gitea</text>
|
||||
<text
|
||||
x="92"
|
||||
y="855"
|
||||
class="mono"
|
||||
id="text34"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git push -u origin chore/step8-sync-hotfix-YYYYMMDD</text>
|
||||
<!-- Boxes: Gitea -->
|
||||
<rect
|
||||
x="536.38428"
|
||||
y="241.13464"
|
||||
width="396.0968"
|
||||
height="123.86536"
|
||||
class="box"
|
||||
rx="16"
|
||||
id="rect34" />
|
||||
<text
|
||||
x="572"
|
||||
y="268"
|
||||
class="boxTitle"
|
||||
id="text35">6) Ouvrir une PR vers main</text>
|
||||
<text
|
||||
x="554"
|
||||
y="290"
|
||||
class="boxText"
|
||||
id="text36"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">main protégé → PR obligatoire. Décrire : “backport hotfix prod”.</text>
|
||||
<text
|
||||
x="554"
|
||||
y="312"
|
||||
class="small"
|
||||
id="text37"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
class="tag tagWarn"
|
||||
id="tspan36"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Ajouter contexte : fichiers touchés, risque, checks attendus.</text>
|
||||
<text
|
||||
x="572"
|
||||
y="334"
|
||||
class="small"
|
||||
id="text38"><tspan
|
||||
class="tag tagInfo"
|
||||
id="tspan37">INFO</tspan> La PR déclenche CI.yaml (pipeline de validation).</text>
|
||||
<rect
|
||||
x="538.65356"
|
||||
y="396.13464"
|
||||
width="389.28894"
|
||||
height="135"
|
||||
class="boxAlt"
|
||||
rx="16"
|
||||
id="rect38" />
|
||||
<text
|
||||
x="572"
|
||||
y="423"
|
||||
class="boxTitle"
|
||||
id="text39">7) Review + décisions</text>
|
||||
<text
|
||||
x="552"
|
||||
y="445"
|
||||
class="boxText"
|
||||
id="text40"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Lecture diff, vérif logique, pas de secrets, pas de régressions UI.</text>
|
||||
<text
|
||||
x="572"
|
||||
y="468"
|
||||
class="small"
|
||||
id="text41"><tspan
|
||||
class="tag tagDanger"
|
||||
id="tspan40">STOP</tspan> Si CI rouge : corriger sur la branche, push → CI relancé.</text>
|
||||
<text
|
||||
x="572"
|
||||
y="490"
|
||||
class="small"
|
||||
id="text42"><tspan
|
||||
class="tag tagOk"
|
||||
id="tspan41">OK</tspan> Si CI vert + review OK : merge autorisé.</text>
|
||||
<rect
|
||||
x="537.51892"
|
||||
y="548.65356"
|
||||
width="390.42358"
|
||||
height="138.82753"
|
||||
class="box"
|
||||
rx="16"
|
||||
id="rect42" />
|
||||
<text
|
||||
x="572"
|
||||
y="588"
|
||||
class="boxTitle"
|
||||
id="text43">8) Merge PR → main (canon)</text>
|
||||
<text
|
||||
x="572"
|
||||
y="610"
|
||||
class="boxText"
|
||||
id="text44"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan107"
|
||||
x="572"
|
||||
y="610">main devient l’unique source officielle.</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan108"
|
||||
x="572"
|
||||
y="626.20001">La prod se recale dessus.</tspan></text>
|
||||
<text
|
||||
x="572"
|
||||
y="646"
|
||||
class="mono"
|
||||
id="text45"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">Merge (UI Gitea) → origin/main updated</text>
|
||||
<text
|
||||
x="572"
|
||||
y="668"
|
||||
class="small"
|
||||
id="text46"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
class="tag tagInfo"
|
||||
id="tspan45"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">INFO</tspan> Optionnel : tagger une release (vX.Y / date).</text>
|
||||
<rect
|
||||
x="550"
|
||||
y="705"
|
||||
width="370"
|
||||
height="145"
|
||||
class="boxAlt"
|
||||
rx="16"
|
||||
id="rect46" />
|
||||
<text
|
||||
x="572"
|
||||
y="733"
|
||||
class="boxTitle"
|
||||
id="text47">9) Préparer une release packagée</text>
|
||||
<text
|
||||
x="572"
|
||||
y="755"
|
||||
class="boxText"
|
||||
id="text48"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan105"
|
||||
x="572"
|
||||
y="755">Générer un paquet de release reproductible</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan106"
|
||||
x="572"
|
||||
y="771.20001">(sources + scripts + config).</tspan></text>
|
||||
<text
|
||||
x="572"
|
||||
y="791"
|
||||
class="mono"
|
||||
id="text49"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">./release-pack.sh</text>
|
||||
<text
|
||||
x="572"
|
||||
y="813"
|
||||
class="small"
|
||||
id="text50"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
class="tag tagWarn"
|
||||
id="tspan49"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Le pack sert au déploiement sur NAS (blue/green).</text>
|
||||
<text
|
||||
x="572"
|
||||
y="835"
|
||||
class="small"
|
||||
id="text51"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
class="tag tagInfo"
|
||||
id="tspan50"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">TIP</tspan> Conserver checksum + manifest (traçabilité).</text>
|
||||
<!-- Boxes: CI -->
|
||||
<rect
|
||||
x="1000"
|
||||
y="260"
|
||||
width="210"
|
||||
height="160"
|
||||
class="box"
|
||||
rx="16"
|
||||
id="rect51" />
|
||||
<text
|
||||
x="1018"
|
||||
y="288"
|
||||
class="boxTitle"
|
||||
id="text52">CI : checks</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="310"
|
||||
class="boxText"
|
||||
id="text53">npm ci</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="330"
|
||||
class="boxText"
|
||||
id="text54">astro build</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="350"
|
||||
class="boxText"
|
||||
id="text55">postbuild scripts</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="370"
|
||||
class="boxText"
|
||||
id="text56">pagefind</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="394"
|
||||
class="small"
|
||||
id="text57"><tspan
|
||||
class="tag tagOk"
|
||||
id="tspan56">PASS</tspan> → merge autorisé</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="414"
|
||||
class="small"
|
||||
id="text58"><tspan
|
||||
class="tag tagDanger"
|
||||
id="tspan57">FAIL</tspan> → corriger branche</text>
|
||||
<rect
|
||||
x="1000"
|
||||
y="450"
|
||||
width="210"
|
||||
height="105"
|
||||
class="boxAlt"
|
||||
rx="16"
|
||||
id="rect58" />
|
||||
<text
|
||||
x="1018"
|
||||
y="478"
|
||||
class="boxTitle"
|
||||
id="text59">Artefacts</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="500"
|
||||
class="boxText"
|
||||
id="text60">Logs + traces</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="520"
|
||||
class="boxText"
|
||||
id="text61">Optionnel : build artefact</text>
|
||||
<text
|
||||
x="1018"
|
||||
y="540"
|
||||
class="small"
|
||||
id="text62"><tspan
|
||||
class="tag tagInfo"
|
||||
id="tspan61">INFO</tspan> Sert au diagnostic rapide.</text>
|
||||
<!-- Boxes: NAS -->
|
||||
<rect
|
||||
x="1270"
|
||||
y="260"
|
||||
width="170"
|
||||
height="155"
|
||||
class="box"
|
||||
rx="16"
|
||||
id="rect62" />
|
||||
<text
|
||||
x="1288"
|
||||
y="288"
|
||||
class="boxTitle"
|
||||
id="text63">Déploiement</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="312"
|
||||
class="boxText"
|
||||
id="text64">Importer release</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="332"
|
||||
class="boxText"
|
||||
id="text65">docker build</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="352"
|
||||
class="boxText"
|
||||
id="text66">web_blue / web_green</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="376"
|
||||
class="boxText"
|
||||
id="text67">switch proxy</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="398"
|
||||
class="small"
|
||||
id="text68"><tspan
|
||||
class="tag tagOk"
|
||||
id="tspan67">OK</tspan> rollback possible</text>
|
||||
<rect
|
||||
x="1270"
|
||||
y="450"
|
||||
width="170"
|
||||
height="140"
|
||||
class="boxAlt"
|
||||
rx="16"
|
||||
id="rect68" />
|
||||
<text
|
||||
x="1288"
|
||||
y="478"
|
||||
class="boxTitle"
|
||||
id="text69">Runbook</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="500"
|
||||
class="boxText"
|
||||
id="text70">healthchecks</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="520"
|
||||
class="boxText"
|
||||
id="text71">logs</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="540"
|
||||
class="boxText"
|
||||
id="text72">validation UI</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="566"
|
||||
class="small"
|
||||
id="text73"><tspan
|
||||
class="tag tagWarn"
|
||||
id="tspan72">MANUEL</tspan> staging d’abord</text>
|
||||
<rect
|
||||
x="1270"
|
||||
y="640"
|
||||
width="170"
|
||||
height="210"
|
||||
class="box"
|
||||
rx="16"
|
||||
id="rect73" />
|
||||
<text
|
||||
x="1288"
|
||||
y="668"
|
||||
class="boxTitle"
|
||||
id="text74">Hotfix prod</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="690"
|
||||
class="boxText"
|
||||
id="text75">À éviter si possible</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="710"
|
||||
class="boxText"
|
||||
id="text76">Si nécessaire :</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="732"
|
||||
class="boxText"
|
||||
id="text77">pack (tar+sha)</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="754"
|
||||
class="boxText"
|
||||
id="text78">→ rapatrier DEV</text>
|
||||
<text
|
||||
x="1288"
|
||||
y="778"
|
||||
class="small"
|
||||
id="text79"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan109"
|
||||
x="1288"
|
||||
y="778"><tspan
|
||||
style="fill:#ff0000"
|
||||
id="tspan111">RISK</tspan> Toujours backporter</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan110"
|
||||
x="1288"
|
||||
y="792.84998">via PR.</tspan></text>
|
||||
<!-- Callout -->
|
||||
<rect
|
||||
x="988.19214"
|
||||
y="665.28741"
|
||||
width="231.68678"
|
||||
height="196.73224"
|
||||
class="callout"
|
||||
rx="14"
|
||||
id="rect79" />
|
||||
<text
|
||||
x="999"
|
||||
y="701"
|
||||
class="boxTitle"
|
||||
id="text80"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Règle d’or</text>
|
||||
<text
|
||||
x="999"
|
||||
y="725"
|
||||
class="boxText"
|
||||
id="text81"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan98"
|
||||
x="999"
|
||||
y="725">Le NAS n’est pas le dépôt source.</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan99"
|
||||
x="999"
|
||||
y="741.20001">Même si un hotfix a été fait en prod,</tspan></text>
|
||||
<text
|
||||
x="999"
|
||||
y="760"
|
||||
class="boxText"
|
||||
id="text82"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan100"
|
||||
x="999"
|
||||
y="760">l’état final “vrai” doit être : branche</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan101"
|
||||
x="999"
|
||||
y="776.20001">→ PR → CI → merge main → release</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="999"
|
||||
y="792.87439"
|
||||
id="tspan102">→ deploy.</tspan></text>
|
||||
<text
|
||||
x="999"
|
||||
y="812"
|
||||
class="small"
|
||||
id="text83"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan103"
|
||||
x="999"
|
||||
y="812"><tspan
|
||||
class="tag tagOk"
|
||||
id="tspan82"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">BUT</tspan> Passation étape 9 = base Git propre</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan104"
|
||||
x="999"
|
||||
y="826.84998">+ reproductible.</tspan></text>
|
||||
<!-- Arrows -->
|
||||
<path
|
||||
class="arrow"
|
||||
d="m 450,222.34493 c 50,0 46.38427,58.78971 86.38427,78.78971"
|
||||
id="path83"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="m 450,835 c 50,0 60,-15 100,-55"
|
||||
id="path84" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M 931.75492,299.19062 C 995.29501,300.15129 924.67474,339.65204 1000,340"
|
||||
id="path85"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
class="arrowSoft dashed"
|
||||
d="M 888.63843,365 C 909.06203,422.26929 925.80938,435.71104 1000,500"
|
||||
id="path86"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M1210 340 C1240 340, 1245 340, 1270 340"
|
||||
id="path87" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M920 620 C980 620, 1000 620, 1080 620"
|
||||
id="path88" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="m 920,620 c 60,0 311.3313,0.2118 347.7307,-236.67171"
|
||||
id="path89"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
class="arrowSoft dashed"
|
||||
d="m 1269.652,672.90469 c -83.9636,0.6354 -155.1134,-27.51891 -348.51736,-27.76853"
|
||||
id="path90"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<!-- Footnote -->
|
||||
<text
|
||||
x="44"
|
||||
y="916"
|
||||
class="subtitle"
|
||||
id="text93">
|
||||
Légende : <tspan
|
||||
class="tag tagInfo"
|
||||
id="tspan90">INFO</tspan> invariant / contexte · <tspan
|
||||
class="tag tagWarn"
|
||||
id="tspan91">MANUEL</tspan> action humaine ·
|
||||
<tspan
|
||||
class="tag tagOk"
|
||||
id="tspan92">OK</tspan> attendu · <tspan
|
||||
class="tag tagDanger"
|
||||
id="tspan93">STOP</tspan> bloquant.
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 29 KiB |
537
docs/diagrams/archicratie-web-edition-global-verbatim-v2.svg
Normal file
@@ -0,0 +1,537 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="900"
|
||||
viewBox="0 0 1600 900"
|
||||
version="1.1"
|
||||
id="svg57"
|
||||
sodipodi:docname="archicratie-web-edition-global-verbatim-v2.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview57"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="540.99849"
|
||||
inkscape:cy="369.74281"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg57" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<!-- Fond clair lisible partout -->
|
||||
<linearGradient
|
||||
id="bg"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ffffff"
|
||||
id="stop1" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#f1f5f9"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<!-- Flèches -->
|
||||
<marker
|
||||
id="arrow"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#334155"
|
||||
id="path2" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowA"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#2563eb"
|
||||
id="path3" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowG"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#059669"
|
||||
id="path4" />
|
||||
</marker>
|
||||
<!-- Styles SANS variables CSS (compat max) -->
|
||||
<style
|
||||
id="style4"><![CDATA[
|
||||
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
|
||||
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
|
||||
|
||||
/* Cadres lisibles */
|
||||
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
|
||||
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
|
||||
|
||||
/* Pills */
|
||||
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
|
||||
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
|
||||
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
|
||||
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
|
||||
|
||||
/* Traits / flèches */
|
||||
.line{stroke:#334155;stroke-width:1.4;fill:none}
|
||||
.dash{stroke-dasharray:6 6}
|
||||
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
|
||||
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
|
||||
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
|
||||
]]></style>
|
||||
</defs>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="1600"
|
||||
height="900"
|
||||
fill="url(#bg)"
|
||||
id="rect4" />
|
||||
<text
|
||||
x="40"
|
||||
y="56"
|
||||
class="title"
|
||||
id="text4">Archicratie — Vue globale (v2, verbatim)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="84"
|
||||
class="subtitle"
|
||||
id="text5">Étape 8 (hotfix UI + sync Git) — mise à jour 2026-02-20 — Astro static + Pagefind + Authelia + Blue/Green</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="120"
|
||||
width="470"
|
||||
height="290"
|
||||
class="box"
|
||||
id="rect5" />
|
||||
<text
|
||||
x="58"
|
||||
y="150"
|
||||
class="h"
|
||||
id="text6">Atelier DEV (Mac Studio)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="174"
|
||||
class="mono"
|
||||
id="text7">repo git (branches, PR vers main)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="192"
|
||||
class="mono"
|
||||
id="text8">npm run dev → http://localhost:4321</text>
|
||||
<text
|
||||
x="58"
|
||||
y="210"
|
||||
class="mono"
|
||||
id="text9">npm run build → dist/ (static)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="228"
|
||||
class="mono"
|
||||
id="text10">postbuild:</text>
|
||||
<text
|
||||
x="58"
|
||||
y="246"
|
||||
class="mono"
|
||||
id="text11"> inject-anchor-aliases.mjs</text>
|
||||
<text
|
||||
x="58"
|
||||
y="264"
|
||||
class="mono"
|
||||
id="text12"> dedupe-ids-dist.mjs</text>
|
||||
<text
|
||||
x="58"
|
||||
y="282"
|
||||
class="mono"
|
||||
id="text13"> build-para-index.mjs → dist/para-index.json</text>
|
||||
<text
|
||||
x="58"
|
||||
y="300"
|
||||
class="mono"
|
||||
id="text14"> build-annotations-index.mjs → dist/annotations-index.json</text>
|
||||
<text
|
||||
x="58"
|
||||
y="318"
|
||||
class="mono"
|
||||
id="text15"> pagefind → dist/pagefind/</text>
|
||||
<text
|
||||
x="58"
|
||||
y="336"
|
||||
class="s"
|
||||
id="text16">predev: build public/para-index.json + public/annotations-index.json</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="420"
|
||||
width="206"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect16" />
|
||||
<text
|
||||
x="74"
|
||||
y="439"
|
||||
class="mono"
|
||||
id="text17">dist/ + pagefind + indexes</text>
|
||||
<rect
|
||||
x="300"
|
||||
y="420"
|
||||
width="198"
|
||||
height="28"
|
||||
class="chip"
|
||||
id="rect17" />
|
||||
<text
|
||||
x="314"
|
||||
y="439"
|
||||
class="mono"
|
||||
id="text18"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">public/*-index.json</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="470"
|
||||
width="470"
|
||||
height="330"
|
||||
class="box2"
|
||||
id="rect18" />
|
||||
<text
|
||||
x="58"
|
||||
y="500"
|
||||
class="h"
|
||||
id="text19">UI Lecture / Édition (dans le site)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="524"
|
||||
class="t"
|
||||
id="text20">EditionLayout.astro (globals + meta)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="542"
|
||||
class="t"
|
||||
id="text21">SidePanel.astro (reading-follow + annotations + propose)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="560"
|
||||
class="t"
|
||||
id="text22">LevelToggle.astro (Niveaux)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="578"
|
||||
class="t"
|
||||
id="text23">global.css (UX lecture + TOC-local sync)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="596"
|
||||
class="s"
|
||||
id="text24">SidePanel consomme para-index + annotations-index</text>
|
||||
<text
|
||||
x="58"
|
||||
y="614"
|
||||
class="s"
|
||||
id="text25">ProposeModal ouvre une issue Gitea (direct ou via bridge)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="632"
|
||||
class="mono"
|
||||
id="text26">env publics: PUBLIC_GITEA_* + PUBLIC_ISSUE_BRIDGE_PATH</text>
|
||||
<rect
|
||||
x="673.92584"
|
||||
y="119.57942"
|
||||
width="344.13016"
|
||||
height="250"
|
||||
class="box"
|
||||
id="rect26" />
|
||||
<text
|
||||
x="723"
|
||||
y="180"
|
||||
class="h"
|
||||
id="text27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Gitea (sur NAS) — source of truth</text>
|
||||
<text
|
||||
x="723"
|
||||
y="204"
|
||||
class="t"
|
||||
id="text28"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">main protégé (push direct interdit)</text>
|
||||
<text
|
||||
x="723"
|
||||
y="222"
|
||||
class="t"
|
||||
id="text29"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">branches de travail → PR → merge</text>
|
||||
<text
|
||||
x="723"
|
||||
y="240"
|
||||
class="t"
|
||||
id="text30"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">CI (workflow) : build + checks + artefacts</text>
|
||||
<text
|
||||
x="723"
|
||||
y="258"
|
||||
class="t"
|
||||
id="text31"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">issues + labels (tickets)</text>
|
||||
<rect
|
||||
x="745"
|
||||
y="320"
|
||||
width="126"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect31" />
|
||||
<text
|
||||
x="759"
|
||||
y="339"
|
||||
class="mono"
|
||||
id="text32"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PR → CI.yaml</text>
|
||||
<rect
|
||||
x="565"
|
||||
y="430"
|
||||
width="470"
|
||||
height="160"
|
||||
class="box2"
|
||||
id="rect32" />
|
||||
<text
|
||||
x="583"
|
||||
y="460"
|
||||
class="h"
|
||||
id="text33">Hotfix de release → re-sync Git (méthode step8)</text>
|
||||
<text
|
||||
x="583"
|
||||
y="484"
|
||||
class="t"
|
||||
id="text34">1) lister fichiers modifiés sur NAS</text>
|
||||
<text
|
||||
x="583"
|
||||
y="502"
|
||||
class="t"
|
||||
id="text35">2) tar + sha256 → transfert</text>
|
||||
<text
|
||||
x="583"
|
||||
y="520"
|
||||
class="t"
|
||||
id="text36">3) rsync --checksum vers repo local</text>
|
||||
<text
|
||||
x="583"
|
||||
y="538"
|
||||
class="t"
|
||||
id="text37">4) commit sur branche dédiée + push + PR</text>
|
||||
<rect
|
||||
x="1183.1921"
|
||||
y="122.42058"
|
||||
width="376.80786"
|
||||
height="247.57942"
|
||||
class="box"
|
||||
id="rect37" />
|
||||
<text
|
||||
x="1228"
|
||||
y="150"
|
||||
class="h"
|
||||
id="text38"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">NAS DS220+ — runtime Blue/Green</text>
|
||||
<text
|
||||
x="1228"
|
||||
y="174"
|
||||
class="mono"
|
||||
id="text39"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">/volume2/docker/archicratie-web/</text>
|
||||
<text
|
||||
x="1228"
|
||||
y="192"
|
||||
class="mono"
|
||||
id="text40"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">releases/<timestamp>/app (build context)</text>
|
||||
<text
|
||||
x="1228"
|
||||
y="210"
|
||||
class="mono"
|
||||
id="text41"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">current → release active</text>
|
||||
<text
|
||||
x="1228"
|
||||
y="228"
|
||||
class="mono"
|
||||
id="text42"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">docker compose: web_blue / web_green</text>
|
||||
<text
|
||||
x="1228"
|
||||
y="246"
|
||||
class="s"
|
||||
id="text43"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">une seule couleur sert le trafic (reverse-proxy)</text>
|
||||
<rect
|
||||
x="1210"
|
||||
y="310"
|
||||
width="322"
|
||||
height="28"
|
||||
class="chipW"
|
||||
id="rect43" />
|
||||
<text
|
||||
x="1224"
|
||||
y="329"
|
||||
class="mono"
|
||||
id="text44"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">docker compose build --no-cache web_*</text>
|
||||
<rect
|
||||
x="1184.4025"
|
||||
y="393.94858"
|
||||
width="375.59756"
|
||||
height="246.05144"
|
||||
class="box2"
|
||||
id="rect44" />
|
||||
<text
|
||||
x="1208"
|
||||
y="430"
|
||||
class="h"
|
||||
id="text45"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Edge & Auth</text>
|
||||
<text
|
||||
x="1208"
|
||||
y="454"
|
||||
class="t"
|
||||
id="text46"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Reverse-proxy (Nginx) protège le site</text>
|
||||
<text
|
||||
x="1208"
|
||||
y="472"
|
||||
class="t"
|
||||
id="text47"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Authelia (SSO) + LLDAP (LDAP) + Redis</text>
|
||||
<text
|
||||
x="1208"
|
||||
y="490"
|
||||
class="t"
|
||||
id="text48"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">302 vers auth.* si non authentifié</text>
|
||||
<text
|
||||
x="1208"
|
||||
y="508"
|
||||
class="t"
|
||||
id="text49"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Host header: staging.* / archicratie.*</text>
|
||||
<text
|
||||
x="1208"
|
||||
y="526"
|
||||
class="s"
|
||||
id="text50"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">/_auth/whoami utilisé en check côté client (peut 404 en local)</text>
|
||||
<path
|
||||
d="m 510,260 163.92587,-1.21029"
|
||||
class="arrowA"
|
||||
id="path50"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<text
|
||||
x="514"
|
||||
y="245"
|
||||
class="s"
|
||||
id="text51"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
|
||||
style="font-size:14.6667px"
|
||||
id="tspan57">git push (branche) + PR</tspan></text>
|
||||
<path
|
||||
d="m 1016.8457,240 h 166.3464"
|
||||
class="arrowA"
|
||||
id="path51"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<text
|
||||
x="1025.3177"
|
||||
y="206.9062"
|
||||
class="s"
|
||||
id="text52"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan58"
|
||||
x="1025.3177"
|
||||
y="206.9062"
|
||||
style="font-size:14.6667px">CI/artefact</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan59"
|
||||
x="1025.3177"
|
||||
y="226.70625"
|
||||
style="font-size:14.6667px">→ déploiement release</tspan></text>
|
||||
<path
|
||||
d="M1420 650 C1480 650 1520 650 1560 650"
|
||||
class="arrow"
|
||||
id="path52" />
|
||||
<text
|
||||
x="1420"
|
||||
y="632"
|
||||
class="s"
|
||||
id="text53">HTTPS → navigateur</text>
|
||||
<path
|
||||
d="M 510,640.25719 C 540,640.25719 540,520 565,520"
|
||||
class="arrow"
|
||||
id="path53"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<text
|
||||
x="534.84113"
|
||||
y="620.20575"
|
||||
class="s"
|
||||
id="text54"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
|
||||
style="font-size:14.6667px"
|
||||
id="tspan60">Propose → issue</tspan></text>
|
||||
<path
|
||||
d="m 1181.9818,520 -145.7715,-1.21029"
|
||||
class="arrowG"
|
||||
id="path54"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
d="M565 520 C520 520 520 520 510 520"
|
||||
class="arrowG"
|
||||
id="path55" />
|
||||
<text
|
||||
x="800"
|
||||
y="505"
|
||||
class="s"
|
||||
id="text55">tar+sha256 → scp → rsync --checksum</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="820"
|
||||
width="1520"
|
||||
height="60"
|
||||
class="box2"
|
||||
id="rect55" />
|
||||
<text
|
||||
x="58"
|
||||
y="850"
|
||||
class="h"
|
||||
id="text56">Légende</text>
|
||||
<text
|
||||
x="58"
|
||||
y="874"
|
||||
class="s"
|
||||
id="text57"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
|
||||
style="font-size:14.6667px"
|
||||
id="tspan61">Bleu = flux Git/CI · Vert = flux de re-sync hotfix · Orange = build runtime · Le reste = navigation/HTTP</tspan></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
828
docs/diagrams/archicratie-web-edition-global-verbatim.svg
Normal file
@@ -0,0 +1,828 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="1020"
|
||||
viewBox="0 0 1600 1020"
|
||||
version="1.1"
|
||||
id="svg80"
|
||||
sodipodi:docname="archicratie-web-edition-global-verbatim.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
inkscape:export-filename="out/archicratie-web-edition-global-verbatim.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview80"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="594.25113"
|
||||
inkscape:cy="481.08926"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg80" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<marker
|
||||
id="arrow"
|
||||
viewBox="0 0 10 10"
|
||||
refX="9.5"
|
||||
refY="5"
|
||||
markerWidth="8"
|
||||
markerHeight="8"
|
||||
orient="auto-start-reverse">
|
||||
<path
|
||||
d="M 0 0 L 10 5 L 0 10 z"
|
||||
fill="#222"
|
||||
id="path1" />
|
||||
</marker>
|
||||
<style
|
||||
id="style1">
|
||||
.title { font: 700 22px sans-serif; fill:#111; }
|
||||
.small { font: 12px sans-serif; fill:#111; }
|
||||
.h2 { font: 700 16px sans-serif; fill:#111; }
|
||||
.h3 { font: 700 14px sans-serif; fill:#111; }
|
||||
.txt { font: 13px sans-serif; fill:#111; }
|
||||
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
|
||||
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
|
||||
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
|
||||
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
|
||||
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
|
||||
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<!-- Header -->
|
||||
<text
|
||||
x="40"
|
||||
y="45"
|
||||
class="title"
|
||||
id="text1">Archicratie – Web Edition : schéma global VERBATIM (Mac Studio ↔ NAS Synology DS220+)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="75"
|
||||
class="small"
|
||||
id="text2">Factuel (capturé sur ton NAS) : DSM (TLS) → Traefik :18080 (file provider) → routers Host(...) → (Authelia forward-auth) → backends (blue/green). Gitea via Traefik sans chain-auth.</text>
|
||||
<!-- LOCAL -->
|
||||
<rect
|
||||
x="35"
|
||||
y="110"
|
||||
width="520"
|
||||
height="880"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect2" />
|
||||
<text
|
||||
x="60"
|
||||
y="145"
|
||||
class="h2"
|
||||
id="text3">LOCAL — Mac Studio (atelier)</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="175"
|
||||
width="470"
|
||||
height="110"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect3" />
|
||||
<text
|
||||
x="80"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text4">Repo site (Astro)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="232"
|
||||
class="txt"
|
||||
id="text5">• build statique → dist/</text>
|
||||
<text
|
||||
x="80"
|
||||
y="254"
|
||||
class="txt"
|
||||
id="text6">• postbuild : inject aliases + dedupe IDs + indexes + pagefind</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="305"
|
||||
width="470"
|
||||
height="115"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect6" />
|
||||
<text
|
||||
x="80"
|
||||
y="335"
|
||||
class="h2"
|
||||
id="text7">Tooling (scripts/)</text>
|
||||
<text
|
||||
x="66"
|
||||
y="360"
|
||||
class="txt"
|
||||
id="text8"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/inject-anchor-aliases.mjs</text>
|
||||
<text
|
||||
x="66"
|
||||
y="382"
|
||||
class="txt"
|
||||
id="text9"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/apply-ticket.mjs --alias</text>
|
||||
<text
|
||||
x="66"
|
||||
y="404"
|
||||
class="txt"
|
||||
id="text10"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/check-anchor-aliases.mjs + verify-anchor-aliases-in-dist.mjs</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="445"
|
||||
width="470"
|
||||
height="105"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect10" />
|
||||
<text
|
||||
x="80"
|
||||
y="475"
|
||||
class="h2"
|
||||
id="text11">Déploiement (release pack)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="500"
|
||||
class="txt"
|
||||
id="text12">• build Docker avec ARG/ENV : PUBLIC_GITEA_BASE/OWNER/REPO</text>
|
||||
<text
|
||||
x="80"
|
||||
y="522"
|
||||
class="txt"
|
||||
id="text13">• pousse/maj sur NAS (containers web_blue/web_green)</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="569"
|
||||
width="470"
|
||||
height="165"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect13" />
|
||||
<text
|
||||
x="80"
|
||||
y="605"
|
||||
class="h2"
|
||||
id="text14">Repères “vrais” côté site</text>
|
||||
<text
|
||||
x="80"
|
||||
y="632"
|
||||
class="txt"
|
||||
id="text15">• whoami runtime : <tspan
|
||||
class="mono"
|
||||
id="tspan14">/_auth/whoami</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="654"
|
||||
class="txt"
|
||||
id="text16">• variables injectées : <tspan
|
||||
class="mono"
|
||||
id="tspan15">PUBLIC_GITEA_BASE/OWNER/REPO</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="676"
|
||||
class="txt"
|
||||
id="text17">• anchors canon : <tspan
|
||||
class="mono"
|
||||
id="tspan16">src/anchors/anchor-aliases.json</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="698"
|
||||
class="txt"
|
||||
id="text18">• injection build-time : <tspan
|
||||
class="mono"
|
||||
id="tspan17">scripts/inject-anchor-aliases.mjs</tspan></text>
|
||||
<!-- NAS -->
|
||||
<rect
|
||||
x="590"
|
||||
y="110"
|
||||
width="975"
|
||||
height="880"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect18" />
|
||||
<text
|
||||
x="615"
|
||||
y="145"
|
||||
class="h2"
|
||||
id="text19">DISTANT — NAS Synology DS220+ (DSM + Container Manager)</text>
|
||||
<!-- Users -->
|
||||
<rect
|
||||
x="615"
|
||||
y="175"
|
||||
width="270"
|
||||
height="90"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect19" />
|
||||
<text
|
||||
x="635"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text20">Utilisateurs</text>
|
||||
<text
|
||||
x="635"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text21">• Web (public)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="252"
|
||||
class="txt"
|
||||
id="text22">• Éditeurs (groupe LDAP)</text>
|
||||
<!-- DSM RP -->
|
||||
<rect
|
||||
x="905"
|
||||
y="175"
|
||||
width="630"
|
||||
height="120"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect22" />
|
||||
<text
|
||||
x="930"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text23">DSM Reverse Proxy (TLS terminé ici)</text>
|
||||
<text
|
||||
x="930"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text24">• Host archicratie.trans-hands.synology.me → 127.0.0.1:18080</text>
|
||||
<text
|
||||
x="930"
|
||||
y="252"
|
||||
class="txt"
|
||||
id="text25">• Host gitea.archicratie.trans-hands.synology.me → 127.0.0.1:18080</text>
|
||||
<text
|
||||
x="930"
|
||||
y="274"
|
||||
class="txt"
|
||||
id="text26">• (idem staging.*, lldap.* si routés via Traefik)</text>
|
||||
<!-- Edge Traefik -->
|
||||
<rect
|
||||
x="1012.7156"
|
||||
y="321.2103"
|
||||
width="522.28442"
|
||||
height="148.78972"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect26" />
|
||||
<text
|
||||
x="1050"
|
||||
y="350"
|
||||
class="h2"
|
||||
id="text27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">edge-traefik (traefik:v2.11) — network_mode: host</text>
|
||||
<text
|
||||
x="1050"
|
||||
y="375"
|
||||
class="txt"
|
||||
id="text28"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• entryPoint web : <tspan
|
||||
class="mono"
|
||||
id="tspan27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">:18080</tspan></text>
|
||||
<text
|
||||
x="1050"
|
||||
y="397"
|
||||
class="txt"
|
||||
id="text29"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• provider file : <tspan
|
||||
class="mono"
|
||||
id="tspan28"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/etc/traefik/dynamic</tspan> (watch: true)</text>
|
||||
<text
|
||||
x="1050"
|
||||
y="419"
|
||||
class="txt"
|
||||
id="text30"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Host rules (routers) + middlewares (chain-auth / sanitize-remote)</text>
|
||||
<text
|
||||
x="1050"
|
||||
y="441"
|
||||
class="small"
|
||||
id="text31"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">Tes 404 initiaux venaient d’un test sans Host: les routers utilisent Host(...)</text>
|
||||
<!-- Dynamic files -->
|
||||
<rect
|
||||
x="615"
|
||||
y="290"
|
||||
width="270"
|
||||
height="180"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect31" />
|
||||
<text
|
||||
x="623"
|
||||
y="320"
|
||||
class="h2"
|
||||
id="text32"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Fichiers dynamiques (edge)</text>
|
||||
<text
|
||||
x="623"
|
||||
y="345"
|
||||
class="mono"
|
||||
id="text33"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/volume2/docker/edge/config/dynamic/</text>
|
||||
<text
|
||||
x="623"
|
||||
y="368"
|
||||
class="txt"
|
||||
id="text34"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 10-core.yml (routers + chain-auth)</text>
|
||||
<text
|
||||
x="623"
|
||||
y="390"
|
||||
class="txt"
|
||||
id="text35"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 20-archicratie-backend.yml (slot actif)</text>
|
||||
<text
|
||||
x="623"
|
||||
y="412"
|
||||
class="txt"
|
||||
id="text36"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan88"
|
||||
x="623"
|
||||
y="412">• 21-archicratie-staging.yml</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan89"
|
||||
x="623"
|
||||
y="428.25">(staging→8081)</tspan></text>
|
||||
<text
|
||||
x="623"
|
||||
y="448"
|
||||
class="txt"
|
||||
id="text37"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 30-lldap-ui.yml (lldap UI)</text>
|
||||
<!-- Auth stack -->
|
||||
<rect
|
||||
x="615"
|
||||
y="495"
|
||||
width="376.80786"
|
||||
height="258.41147"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect37" />
|
||||
<text
|
||||
x="635"
|
||||
y="525"
|
||||
class="h2"
|
||||
id="text38">Auth stack (auth)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="550"
|
||||
class="txt"
|
||||
id="text39">auth-authelia (authelia:4.39.13) — host</text>
|
||||
<text
|
||||
x="635"
|
||||
y="572"
|
||||
class="txt"
|
||||
id="text40"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan92"
|
||||
x="635"
|
||||
y="572">• forward-auth :</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan93"
|
||||
x="635"
|
||||
y="588.25">http://127.0.0.1:9091/api/authz/forward-auth</tspan></text>
|
||||
<text
|
||||
x="635"
|
||||
y="614"
|
||||
class="txt"
|
||||
id="text41"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">auth-lldap (lldap:stable)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="640"
|
||||
class="txt"
|
||||
id="text42"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• LDAP : <tspan
|
||||
class="mono"
|
||||
id="tspan41"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:3890</tspan> • UI : <tspan
|
||||
class="mono"
|
||||
id="tspan42"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:17170</tspan></text>
|
||||
<text
|
||||
x="635"
|
||||
y="662"
|
||||
class="txt"
|
||||
id="text43"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">auth-redis (redis:7-alpine)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="684"
|
||||
class="txt"
|
||||
id="text44"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• exposé : <tspan
|
||||
class="mono"
|
||||
id="tspan43"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:6380</tspan></text>
|
||||
<text
|
||||
x="635"
|
||||
y="708"
|
||||
class="small"
|
||||
id="text45"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan94"
|
||||
x="635"
|
||||
y="708">Traefik injecte Remote-* via forward-auth,</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan95"
|
||||
x="635"
|
||||
y="723">et purge l’entrée (sanitize-remote).</tspan></text>
|
||||
<!-- Whoami service -->
|
||||
<rect
|
||||
x="1110"
|
||||
y="495"
|
||||
width="365.69592"
|
||||
height="117.57942"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect45" />
|
||||
<text
|
||||
x="1135"
|
||||
y="525"
|
||||
class="h2"
|
||||
id="text46">edge-whoami (traefik/whoami)</text>
|
||||
<text
|
||||
x="1135"
|
||||
y="550"
|
||||
class="txt"
|
||||
id="text47">• exposé : <tspan
|
||||
class="mono"
|
||||
id="tspan46">127.0.0.1:18081 → 80</tspan></text>
|
||||
<text
|
||||
x="1135"
|
||||
y="572"
|
||||
class="txt"
|
||||
id="text48">• router Traefik : <tspan
|
||||
class="mono"
|
||||
id="tspan47">PathPrefix('/_auth/whoami')</tspan></text>
|
||||
<text
|
||||
x="1135"
|
||||
y="594"
|
||||
class="txt"
|
||||
id="text49">• protégé par <tspan
|
||||
class="mono"
|
||||
id="tspan48">chain-auth</tspan> (302 login si non auth)</text>
|
||||
<!-- Web blue/green -->
|
||||
<rect
|
||||
x="1047.9349"
|
||||
y="639.94855"
|
||||
width="224.96217"
|
||||
height="116.11195"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect49" />
|
||||
<text
|
||||
x="1070"
|
||||
y="670"
|
||||
class="h2"
|
||||
id="text50"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">archicratie-web-blue</text>
|
||||
<text
|
||||
x="1070"
|
||||
y="695"
|
||||
class="txt"
|
||||
id="text51"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 127.0.0.1:8081 → 80</text>
|
||||
<text
|
||||
x="1070"
|
||||
y="717"
|
||||
class="txt"
|
||||
id="text52"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Nginx sert dist/</text>
|
||||
<text
|
||||
x="1070"
|
||||
y="739"
|
||||
class="small"
|
||||
id="text53"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">slot blue (staging cible 8081)</text>
|
||||
<rect
|
||||
x="1295"
|
||||
y="640"
|
||||
width="240.69592"
|
||||
height="116.11195"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect53" />
|
||||
<text
|
||||
x="1320"
|
||||
y="670"
|
||||
class="h2"
|
||||
id="text54"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">archicratie-web-green</text>
|
||||
<text
|
||||
x="1320"
|
||||
y="695"
|
||||
class="txt"
|
||||
id="text55"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 127.0.0.1:8082 → 80</text>
|
||||
<text
|
||||
x="1320"
|
||||
y="717"
|
||||
class="txt"
|
||||
id="text56"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Nginx sert dist/</text>
|
||||
<text
|
||||
x="1320"
|
||||
y="739"
|
||||
class="small"
|
||||
id="text57"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">slot green (backend actuel)</text>
|
||||
<rect
|
||||
x="1156.7399"
|
||||
y="778.21478"
|
||||
width="374.62918"
|
||||
height="105.4161"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect57" />
|
||||
<text
|
||||
x="1190.2118"
|
||||
y="797.89716"
|
||||
class="txt"
|
||||
id="text58"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
id="tspan96"
|
||||
x="1190.2118"
|
||||
y="797.89716"
|
||||
sodipodi:role="line">Bascule blue/green (Traefik) :</tspan><tspan
|
||||
x="1190.2118"
|
||||
y="814.14716"
|
||||
id="tspan104"
|
||||
sodipodi:role="line">modifier <tspan
|
||||
class="mono"
|
||||
id="tspan57"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">dynamic/20-archicratie-backend.yml</tspan></tspan><tspan
|
||||
id="tspan97"
|
||||
x="1190.2118"
|
||||
y="830.39716"
|
||||
sodipodi:role="line">→ url 8081/8082 (un seul backend actif)</tspan></text>
|
||||
<text
|
||||
x="1202.3751"
|
||||
y="858.05145"
|
||||
class="small"
|
||||
id="text59"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan101"
|
||||
x="1202.3751"
|
||||
y="858.05145">Actuellement (d’après ton dump) :<tspan
|
||||
class="mono"
|
||||
id="tspan58"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"></tspan></tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan102"
|
||||
x="1202.3751"
|
||||
y="873.05145"><tspan
|
||||
class="mono"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"
|
||||
id="tspan103">archicratie_web → http://127.0.0.1:8082</tspan></tspan></text>
|
||||
<!-- Gitea + Runner -->
|
||||
<rect
|
||||
x="615"
|
||||
y="790"
|
||||
width="440.12103"
|
||||
height="180.57489"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect59" />
|
||||
<text
|
||||
x="635"
|
||||
y="820"
|
||||
class="h2"
|
||||
id="text60"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Gitea (actuel)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="845"
|
||||
class="txt"
|
||||
id="text61"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• conteneur : <tspan
|
||||
class="mono"
|
||||
id="tspan60"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea-old-2026-02-09-105211</tspan></text>
|
||||
<text
|
||||
x="635"
|
||||
y="867"
|
||||
class="txt"
|
||||
id="text62"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• port : <tspan
|
||||
class="mono"
|
||||
id="tspan61"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">0.0.0.0:3000</tspan> (Traefik route aussi vers 127.0.0.1:3000)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="889"
|
||||
class="txt"
|
||||
id="text63"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan90"
|
||||
x="635"
|
||||
y="889">• router Traefik : Host(gitea.archicratie...)</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan91"
|
||||
x="635"
|
||||
y="905.25">+ middleware sanitize-remote (pas chain-auth)</tspan></text>
|
||||
<text
|
||||
x="635"
|
||||
y="929"
|
||||
class="small"
|
||||
id="text64"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan99"
|
||||
x="635"
|
||||
y="929">“Proposer” dépend de PUBLIC_GITEA_* corrects</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan100"
|
||||
x="635"
|
||||
y="944">(owner casse sensible).</tspan></text>
|
||||
<rect
|
||||
x="1160"
|
||||
y="895"
|
||||
width="375"
|
||||
height="85"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect64" />
|
||||
<text
|
||||
x="1185"
|
||||
y="925"
|
||||
class="h2"
|
||||
id="text65"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">gitea-act-runner</text>
|
||||
<text
|
||||
x="1185"
|
||||
y="950"
|
||||
class="txt"
|
||||
id="text66"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• image : <tspan
|
||||
class="mono"
|
||||
id="tspan65"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea/act_runner:0.2.11</tspan></text>
|
||||
<text
|
||||
x="1185"
|
||||
y="972"
|
||||
class="small"
|
||||
id="text67"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">CI : labels / checks (anchors, aliases, etc.).</text>
|
||||
<!-- Connections -->
|
||||
<path
|
||||
d="M 885 220 L 905 220"
|
||||
class="line"
|
||||
id="path67" />
|
||||
<!-- Users -> DSM -->
|
||||
<path
|
||||
d="M 1220 295 L 1220 320"
|
||||
class="line"
|
||||
id="path68" />
|
||||
<!-- DSM -> Traefik -->
|
||||
<!-- Traefik -> auth (forward auth) -->
|
||||
<path
|
||||
d="m 1005,420 -45,75"
|
||||
class="dash"
|
||||
id="path69" />
|
||||
<text
|
||||
x="120.96539"
|
||||
y="1057.8674"
|
||||
class="small"
|
||||
id="text69"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"
|
||||
transform="rotate(-57.013356)">forward-auth</text>
|
||||
<!-- Traefik -> whoami -->
|
||||
<path
|
||||
d="M 1220 470 L 1220 495"
|
||||
class="line"
|
||||
id="path70" />
|
||||
<!-- Traefik -> web service -->
|
||||
<path
|
||||
d="m 1082.9955,470 -0.3782,168.78971"
|
||||
class="dash"
|
||||
id="path71"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
d="m 1500,470 -0.416,170"
|
||||
class="dash"
|
||||
id="path72"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<!-- Traefik -> gitea -->
|
||||
<path
|
||||
d="m 1034.826,471.21029 1.3616,315.67322"
|
||||
class="dash"
|
||||
id="path73"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<!-- Gitea -> runner -->
|
||||
<path
|
||||
d="m 1060,890 98.7897,59.52345"
|
||||
class="line"
|
||||
id="path74"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<!-- Local -> NAS (release/deploy) -->
|
||||
<path
|
||||
d="M 530 505 L 615 505"
|
||||
class="line"
|
||||
id="path75" />
|
||||
<!-- Legend -->
|
||||
<rect
|
||||
x="61.210289"
|
||||
y="750.47656"
|
||||
width="467.57938"
|
||||
height="215.31776"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect75" />
|
||||
<text
|
||||
x="80"
|
||||
y="777"
|
||||
class="h2"
|
||||
id="text75"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Lecture (opérationnelle)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="798"
|
||||
class="txt"
|
||||
id="text76"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan86"
|
||||
x="80"
|
||||
y="798">1) Web public : DSM → Traefik :18080 → Host(archicratie...)</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan87"
|
||||
x="80"
|
||||
y="814.40051">→ chain-auth → backend (8081/8082)</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="836"
|
||||
class="txt"
|
||||
id="text77"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan84"
|
||||
x="80"
|
||||
y="836">2) Gate éditeurs : site appelle /_auth/whoami</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan85"
|
||||
x="80"
|
||||
y="852.25">→ Traefik route vers edge-whoami (protégé)</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="874"
|
||||
class="txt"
|
||||
id="text78"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan82"
|
||||
x="80"
|
||||
y="874">3) Gitea : Host(gitea...) → Traefik → 127.0.0.1:3000</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan83"
|
||||
x="80"
|
||||
y="890.40051">(sanitize-remote)</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="912"
|
||||
class="txt"
|
||||
id="text79"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan80"
|
||||
x="80"
|
||||
y="912">4) Blue/green : changer <tspan
|
||||
class="mono"
|
||||
id="tspan78"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">dynamic/20-archicratie-backend.yml</tspan></tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan81"
|
||||
x="80"
|
||||
y="928.25">(un seul backend actif)</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="950"
|
||||
class="small"
|
||||
id="text80"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">NB : un test sans Host sur :18080 renvoie 404 (normal, Host rules).</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 26 KiB |
409
docs/diagrams/archicratie-web-edition-machine-editoriale-v2.svg
Normal file
@@ -0,0 +1,409 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="900"
|
||||
viewBox="0 0 1600 900"
|
||||
version="1.1"
|
||||
id="svg43"
|
||||
sodipodi:docname="archicratie-web-edition-machine-editoriale-v2.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview43"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="1108.6233"
|
||||
inkscape:cy="435.09834"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg43" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<!-- Fond clair lisible partout -->
|
||||
<linearGradient
|
||||
id="bg"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ffffff"
|
||||
id="stop1" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#f1f5f9"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<!-- Flèches -->
|
||||
<marker
|
||||
id="arrow"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#334155"
|
||||
id="path2" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowA"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#2563eb"
|
||||
id="path3" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowG"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#059669"
|
||||
id="path4" />
|
||||
</marker>
|
||||
<!-- Styles SANS variables CSS (compat max) -->
|
||||
<style
|
||||
id="style4"><![CDATA[
|
||||
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
|
||||
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
|
||||
|
||||
/* Cadres lisibles */
|
||||
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
|
||||
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
|
||||
|
||||
/* Pills */
|
||||
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
|
||||
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
|
||||
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
|
||||
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
|
||||
|
||||
/* Traits / flèches */
|
||||
.line{stroke:#334155;stroke-width:1.4;fill:none}
|
||||
.dash{stroke-dasharray:6 6}
|
||||
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
|
||||
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
|
||||
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
|
||||
]]></style>
|
||||
</defs>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="1600"
|
||||
height="900"
|
||||
fill="url(#bg)"
|
||||
id="rect4" />
|
||||
<text
|
||||
x="40"
|
||||
y="56"
|
||||
class="title"
|
||||
id="text4">Archicratie — Machine éditoriale (v2)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="84"
|
||||
class="subtitle"
|
||||
id="text5">De la source au site (lecture + annotations + propositions) — 2026-02-20</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="140"
|
||||
width="460"
|
||||
height="256.62631"
|
||||
class="box"
|
||||
id="rect5" />
|
||||
<text
|
||||
x="118"
|
||||
y="230"
|
||||
class="h"
|
||||
id="text6"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Sources (repo)</text>
|
||||
<text
|
||||
x="118"
|
||||
y="254"
|
||||
class="t"
|
||||
id="text7"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Contenu : src/content/** (MD/MDX)</text>
|
||||
<text
|
||||
x="118"
|
||||
y="272"
|
||||
class="t"
|
||||
id="text8"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Annotations : src/annotations/** (YAML)</text>
|
||||
<text
|
||||
x="118"
|
||||
y="290"
|
||||
class="t"
|
||||
id="text9"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI : src/layouts + src/components + global.css</text>
|
||||
<text
|
||||
x="118"
|
||||
y="308"
|
||||
class="s"
|
||||
id="text10"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Plugin paragraph-ids ajoute des ids stables sur paragraphes</text>
|
||||
<rect
|
||||
x="560"
|
||||
y="140"
|
||||
width="500"
|
||||
height="260"
|
||||
class="box"
|
||||
id="rect10" />
|
||||
<text
|
||||
x="698"
|
||||
y="230"
|
||||
class="h"
|
||||
id="text11"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Build (Astro static)</text>
|
||||
<text
|
||||
x="698"
|
||||
y="254"
|
||||
class="t"
|
||||
id="text12"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">astro build → dist/**/index.html</text>
|
||||
<text
|
||||
x="698"
|
||||
y="272"
|
||||
class="t"
|
||||
id="text13"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">meta Pagefind: edition/level/status/version</text>
|
||||
<text
|
||||
x="698"
|
||||
y="290"
|
||||
class="t"
|
||||
id="text14"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Layout : EditionLayout + SiteLayout</text>
|
||||
<text
|
||||
x="698"
|
||||
y="308"
|
||||
class="s"
|
||||
id="text15"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">data-pagefind-body = zone indexée</text>
|
||||
<rect
|
||||
x="560"
|
||||
y="430"
|
||||
width="497.57944"
|
||||
height="249.74281"
|
||||
class="box2"
|
||||
id="rect15" />
|
||||
<text
|
||||
x="658"
|
||||
y="520"
|
||||
class="h"
|
||||
id="text16"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Postbuild (qualité + recherche + indexes)</text>
|
||||
<text
|
||||
x="658"
|
||||
y="544"
|
||||
class="t"
|
||||
id="text17"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Aliases d'ancres (backward compat)</text>
|
||||
<text
|
||||
x="658"
|
||||
y="562"
|
||||
class="t"
|
||||
id="text18"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Dédoublonnage d'IDs (anti-régression)</text>
|
||||
<text
|
||||
x="658"
|
||||
y="580"
|
||||
class="t"
|
||||
id="text19"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Index des paragraphes (para-index)</text>
|
||||
<text
|
||||
x="658"
|
||||
y="598"
|
||||
class="t"
|
||||
id="text20"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Index des annotations (annotations-index)</text>
|
||||
<text
|
||||
x="658"
|
||||
y="616"
|
||||
class="t"
|
||||
id="text21"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Pagefind (recherche full-text)</text>
|
||||
<rect
|
||||
x="1120"
|
||||
y="140"
|
||||
width="436.36914"
|
||||
height="259.36459"
|
||||
class="box"
|
||||
id="rect21" />
|
||||
<text
|
||||
x="1238"
|
||||
y="210"
|
||||
class="h"
|
||||
id="text22"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Artefacts (dist/)</text>
|
||||
<text
|
||||
x="1238"
|
||||
y="234"
|
||||
class="mono"
|
||||
id="text23"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">HTML statique + assets</text>
|
||||
<text
|
||||
x="1238"
|
||||
y="252"
|
||||
class="mono"
|
||||
id="text24"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/pagefind/**</text>
|
||||
<text
|
||||
x="1238"
|
||||
y="270"
|
||||
class="mono"
|
||||
id="text25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/para-index.json</text>
|
||||
<text
|
||||
x="1238"
|
||||
y="288"
|
||||
class="mono"
|
||||
id="text26"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/annotations-index.json</text>
|
||||
<text
|
||||
x="1238"
|
||||
y="306"
|
||||
class="s"
|
||||
id="text27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">(en dev) recopiés dans public/*-index.json</text>
|
||||
<rect
|
||||
x="1120"
|
||||
y="430"
|
||||
width="436.36917"
|
||||
height="249.48566"
|
||||
class="box2"
|
||||
id="rect27" />
|
||||
<text
|
||||
x="1178"
|
||||
y="520"
|
||||
class="h"
|
||||
id="text28"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Runtime navigateur (lecture)</text>
|
||||
<text
|
||||
x="1178"
|
||||
y="544"
|
||||
class="t"
|
||||
id="text29"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">LocalToc sync (H2/H3)</text>
|
||||
<text
|
||||
x="1178"
|
||||
y="562"
|
||||
class="t"
|
||||
id="text30"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">banner-follow + reading-follow__inner</text>
|
||||
<text
|
||||
x="1178"
|
||||
y="580"
|
||||
class="t"
|
||||
id="text31"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">SidePanel: niveaux + annotations + propose</text>
|
||||
<text
|
||||
x="1178"
|
||||
y="598"
|
||||
class="s"
|
||||
id="text32"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Comportement lecture: H2/H3 unifiés (plus d’accordéon gênant)</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="430"
|
||||
width="460"
|
||||
height="250"
|
||||
class="box2"
|
||||
id="rect32" />
|
||||
<text
|
||||
x="138"
|
||||
y="524"
|
||||
class="h"
|
||||
id="text33"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Flux “Proposer” (tickets)</text>
|
||||
<text
|
||||
x="138"
|
||||
y="548"
|
||||
class="t"
|
||||
id="text34"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI collecte: page + paragraphe + type + message</text>
|
||||
<text
|
||||
x="138"
|
||||
y="566"
|
||||
class="t"
|
||||
id="text35"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Création d'issue Gitea (labels)</text>
|
||||
<text
|
||||
x="138"
|
||||
y="584"
|
||||
class="t"
|
||||
id="text36"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Lien retour: issue → page + id</text>
|
||||
<text
|
||||
x="138"
|
||||
y="602"
|
||||
class="s"
|
||||
id="text37"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Option: bridge same-origin pour éviter CORS/auth</text>
|
||||
<path
|
||||
d="M500 250 C530 250 530 250 560 250"
|
||||
class="arrowA"
|
||||
id="path37" />
|
||||
<path
|
||||
d="M810 400 C810 420 810 420 810 430"
|
||||
class="arrowA"
|
||||
id="path38" />
|
||||
<path
|
||||
d="M1060 250 C1090 250 1090 250 1120 250"
|
||||
class="arrowA"
|
||||
id="path39" />
|
||||
<path
|
||||
d="M 1338.7897,398.15432 1340,430"
|
||||
class="arrow"
|
||||
id="path40"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
d="M500 540 C620 540 620 520 560 520"
|
||||
class="arrow"
|
||||
id="path41" />
|
||||
<text
|
||||
x="520"
|
||||
y="525"
|
||||
class="s"
|
||||
id="text41">issues</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="820"
|
||||
width="1520"
|
||||
height="60"
|
||||
class="box2"
|
||||
id="rect41" />
|
||||
<text
|
||||
x="58"
|
||||
y="850"
|
||||
class="h"
|
||||
id="text42">Conseil de maintenance</text>
|
||||
<text
|
||||
x="58"
|
||||
y="874"
|
||||
class="s"
|
||||
id="text43">Toute évolution UI/indices doit rester déterministe : build identique sur Mac, CI, et NAS. En cas de hotfix, re-sync via PR.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
596
docs/diagrams/archicratie-web-edition-machine-editoriale-v3.svg
Normal file
@@ -0,0 +1,596 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1500"
|
||||
height="940"
|
||||
viewBox="0 0 1500 940"
|
||||
role="img"
|
||||
aria-label="Archicratie — Machine éditoriale (synthèse) : DEV/PROD, indices, postbuild, proposer/bridge"
|
||||
version="1.1"
|
||||
id="svg71"
|
||||
sodipodi:docname="archicratie-web-edition-machine-editoriale-v3.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview71"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.88133333"
|
||||
inkscape:cx="794.25113"
|
||||
inkscape:cy="350.03782"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg71" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<style
|
||||
id="style1">
|
||||
/* Inkscape-safe: pas de CSS variables, fond explicite */
|
||||
.title{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:26px; font-weight:800; fill:#0f172a;}
|
||||
.sub{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:600; fill:#475569;}
|
||||
.laneT{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:800; fill:#0f172a; letter-spacing:.2px;}
|
||||
.laneN{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:600; fill:#475569;}
|
||||
.h{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:800; fill:#0f172a;}
|
||||
.p{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:600; fill:#475569;}
|
||||
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; font-size:11px; font-weight:700; fill:#334155;}
|
||||
.small{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:11px; font-weight:600; fill:#475569;}
|
||||
|
||||
.canvas{fill:#f8fafc; stroke:#e2e8f0; stroke-width:1;}
|
||||
.lane{fill:#f1f5f9; stroke:#cbd5e1; stroke-width:1;}
|
||||
.laneAlt{fill:#eef2f7; stroke:#cbd5e1; stroke-width:1;}
|
||||
|
||||
.box{fill:#ffffff; stroke:#94a3b8; stroke-width:1.4;}
|
||||
.boxAlt{fill:#f8fafc; stroke:#94a3b8; stroke-width:1.4;}
|
||||
.call{fill:#ffffff; fill-opacity:.72; stroke:#cbd5e1; stroke-width:1.1;}
|
||||
|
||||
.arrow{stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#ah);}
|
||||
.arrowSoft{stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#ahSoft);}
|
||||
.dash{stroke-dasharray:7 6;}
|
||||
</style>
|
||||
<marker
|
||||
id="ah"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="5"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L10,5 L0,10 Z"
|
||||
fill="#64748b"
|
||||
id="path1" />
|
||||
</marker>
|
||||
<marker
|
||||
id="ahSoft"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="5"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L10,5 L0,10 Z"
|
||||
fill="#94a3b8"
|
||||
id="path2" />
|
||||
</marker>
|
||||
</defs>
|
||||
<!-- Fond explicite -->
|
||||
<rect
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="1499"
|
||||
height="939"
|
||||
rx="26"
|
||||
class="canvas"
|
||||
id="rect2" />
|
||||
<!-- Titre -->
|
||||
<text
|
||||
x="44"
|
||||
y="54"
|
||||
class="title"
|
||||
id="text2">Archicratie — Machine éditoriale (synthèse “exploitation + onboarding”)</text>
|
||||
<text
|
||||
x="44"
|
||||
y="82"
|
||||
class="sub"
|
||||
id="text3">Inclut : DEV vs PROD (indices), ordre exact du postbuild, et fork “Proposer” (direct vs bridge same-origin).</text>
|
||||
<!-- Lanes -->
|
||||
<rect
|
||||
x="40"
|
||||
y="115"
|
||||
width="390"
|
||||
height="770"
|
||||
rx="18"
|
||||
class="lane"
|
||||
id="rect3" />
|
||||
<rect
|
||||
x="450"
|
||||
y="115"
|
||||
width="390"
|
||||
height="770"
|
||||
rx="18"
|
||||
class="laneAlt"
|
||||
id="rect4" />
|
||||
<rect
|
||||
x="860"
|
||||
y="115"
|
||||
width="330"
|
||||
height="770"
|
||||
rx="18"
|
||||
class="lane"
|
||||
id="rect5" />
|
||||
<rect
|
||||
x="1210"
|
||||
y="115"
|
||||
width="250"
|
||||
height="770"
|
||||
rx="18"
|
||||
class="laneAlt"
|
||||
id="rect6" />
|
||||
<text
|
||||
x="60"
|
||||
y="148"
|
||||
class="laneT"
|
||||
id="text6">1) Sources & vérité éditoriale</text>
|
||||
<text
|
||||
x="60"
|
||||
y="170"
|
||||
class="laneN"
|
||||
id="text7">Ce qui est versionné (canon) et transforme l’édition</text>
|
||||
<text
|
||||
x="470"
|
||||
y="148"
|
||||
class="laneT"
|
||||
id="text8">2) Build Astro (static)</text>
|
||||
<text
|
||||
x="470"
|
||||
y="170"
|
||||
class="laneN"
|
||||
id="text9">Rendu HTML + UI d’édition (EditionLayout)</text>
|
||||
<text
|
||||
x="880"
|
||||
y="148"
|
||||
class="laneT"
|
||||
id="text10">3) Postbuild (ordre exact)</text>
|
||||
<text
|
||||
x="880"
|
||||
y="170"
|
||||
class="laneN"
|
||||
id="text11">Anti-régressions + index + recherche</text>
|
||||
<text
|
||||
x="1230"
|
||||
y="148"
|
||||
class="laneT"
|
||||
id="text12">4) Runtime & feedback</text>
|
||||
<text
|
||||
x="1230"
|
||||
y="170"
|
||||
class="laneN"
|
||||
id="text13">DEV (public) / PROD (dist) + Proposer</text>
|
||||
<!-- Lane 1 -->
|
||||
<rect
|
||||
x="70"
|
||||
y="210"
|
||||
width="330"
|
||||
height="135"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect13" />
|
||||
<text
|
||||
x="92"
|
||||
y="238"
|
||||
class="h"
|
||||
id="text14">Sources amont (traçabilité)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="260"
|
||||
class="p"
|
||||
id="text15">Fichiers “sources/” (docx/pdf) + historiques.</text>
|
||||
<text
|
||||
x="92"
|
||||
y="282"
|
||||
class="mono"
|
||||
id="text16">sources/** (non servi tel quel)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="304"
|
||||
class="small"
|
||||
id="text17">→ import/pipeline vers le contenu canon.</text>
|
||||
<rect
|
||||
x="70"
|
||||
y="365"
|
||||
width="330"
|
||||
height="190"
|
||||
rx="16"
|
||||
class="boxAlt"
|
||||
id="rect17" />
|
||||
<text
|
||||
x="92"
|
||||
y="393"
|
||||
class="h"
|
||||
id="text18">Contenu canon (site)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="415"
|
||||
class="p"
|
||||
id="text19">Pages : MD/MDX (Astro content)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="437"
|
||||
class="mono"
|
||||
id="text20">src/content/**</text>
|
||||
<text
|
||||
x="92"
|
||||
y="461"
|
||||
class="p"
|
||||
id="text21">Annotations : YAML</text>
|
||||
<text
|
||||
x="92"
|
||||
y="483"
|
||||
class="mono"
|
||||
id="text22">src/annotations/**</text>
|
||||
<text
|
||||
x="78"
|
||||
y="507"
|
||||
class="small"
|
||||
id="text23">Ces deux entrées alimentent l’UI (SidePanel, highlights, etc.).</text>
|
||||
<rect
|
||||
x="70"
|
||||
y="575"
|
||||
width="330"
|
||||
height="140"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect23" />
|
||||
<text
|
||||
x="92"
|
||||
y="603"
|
||||
class="h"
|
||||
id="text24">Scripts d’import / qualité</text>
|
||||
<text
|
||||
x="92"
|
||||
y="625"
|
||||
class="p"
|
||||
id="text25">Import DOCX, contrôle d’IDs, aliases, etc.</text>
|
||||
<text
|
||||
x="92"
|
||||
y="647"
|
||||
class="mono"
|
||||
id="text26">scripts/*.mjs</text>
|
||||
<text
|
||||
x="86"
|
||||
y="671"
|
||||
class="small"
|
||||
id="text27">Objectif : build reproductible + pas de régression d’ancres.</text>
|
||||
<!-- Lane 2 -->
|
||||
<rect
|
||||
x="480"
|
||||
y="210"
|
||||
width="330"
|
||||
height="170"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect27" />
|
||||
<text
|
||||
x="502"
|
||||
y="238"
|
||||
class="h"
|
||||
id="text28">EditionLayout (UI d’édition)</text>
|
||||
<text
|
||||
x="502"
|
||||
y="260"
|
||||
class="p"
|
||||
id="text29"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan71"
|
||||
x="502"
|
||||
y="260">SiteNav + TOC global + TOC local + reading-follow</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan72"
|
||||
x="502"
|
||||
y="275">+ SidePanel.</tspan></text>
|
||||
<text
|
||||
x="508"
|
||||
y="300"
|
||||
class="mono"
|
||||
id="text30">src/layouts/EditionLayout.astro</text>
|
||||
<text
|
||||
x="508"
|
||||
y="322"
|
||||
class="mono"
|
||||
id="text31">src/components/SidePanel.astro</text>
|
||||
<text
|
||||
x="508"
|
||||
y="344"
|
||||
class="small"
|
||||
id="text32"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan73"
|
||||
x="508"
|
||||
y="344">Globals boot (flags/env)</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan74"
|
||||
x="508"
|
||||
y="357.75">+ interactions “Propos / Réfs / Illus / Com”.</tspan></text>
|
||||
<rect
|
||||
x="480"
|
||||
y="400"
|
||||
width="330"
|
||||
height="160"
|
||||
rx="16"
|
||||
class="boxAlt"
|
||||
id="rect32" />
|
||||
<text
|
||||
x="502"
|
||||
y="428"
|
||||
class="h"
|
||||
id="text33">Build statique</text>
|
||||
<text
|
||||
x="502"
|
||||
y="450"
|
||||
class="p"
|
||||
id="text34">Astro génère HTML & assets.</text>
|
||||
<text
|
||||
x="502"
|
||||
y="472"
|
||||
class="mono"
|
||||
id="text35">npm run build → astro build</text>
|
||||
<text
|
||||
x="502"
|
||||
y="496"
|
||||
class="p"
|
||||
id="text36">Sortie :</text>
|
||||
<text
|
||||
x="502"
|
||||
y="518"
|
||||
class="mono"
|
||||
id="text37">dist/**</text>
|
||||
<!-- Lane 3 -->
|
||||
<rect
|
||||
x="890"
|
||||
y="210"
|
||||
width="270"
|
||||
height="265"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect37" />
|
||||
<text
|
||||
x="912"
|
||||
y="238"
|
||||
class="h"
|
||||
id="text38">Postbuild : ordre exact (fixe)</text>
|
||||
<text
|
||||
x="912"
|
||||
y="264"
|
||||
class="mono"
|
||||
id="text39">1) inject-anchor-aliases.mjs</text>
|
||||
<text
|
||||
x="912"
|
||||
y="286"
|
||||
class="mono"
|
||||
id="text40">2) dedupe-ids-dist.mjs</text>
|
||||
<text
|
||||
x="912"
|
||||
y="308"
|
||||
class="mono"
|
||||
id="text41">3) build-para-index.mjs</text>
|
||||
<text
|
||||
x="912"
|
||||
y="330"
|
||||
class="mono"
|
||||
id="text42">4) build-annotations-index.mjs</text>
|
||||
<text
|
||||
x="912"
|
||||
y="352"
|
||||
class="mono"
|
||||
id="text43">5) pagefind</text>
|
||||
<text
|
||||
x="912"
|
||||
y="380"
|
||||
class="p"
|
||||
id="text44">Sorties PROD :</text>
|
||||
<text
|
||||
x="912"
|
||||
y="402"
|
||||
class="mono"
|
||||
id="text45">dist/para-index.json</text>
|
||||
<text
|
||||
x="912"
|
||||
y="424"
|
||||
class="mono"
|
||||
id="text46">dist/annotations-index.json</text>
|
||||
<text
|
||||
x="912"
|
||||
y="446"
|
||||
class="mono"
|
||||
id="text47">dist/pagefind/**</text>
|
||||
<!-- Lane 4 -->
|
||||
<rect
|
||||
x="1230"
|
||||
y="210"
|
||||
width="210"
|
||||
height="200"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect47" />
|
||||
<text
|
||||
x="1252"
|
||||
y="238"
|
||||
class="h"
|
||||
id="text48">DEV vs PROD : indices</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="262"
|
||||
class="p"
|
||||
id="text49">PROD (statique) lit dans :</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="284"
|
||||
class="mono"
|
||||
id="text50">dist/*.json</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="308"
|
||||
class="p"
|
||||
id="text51">DEV (astro dev) sert depuis :</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="330"
|
||||
class="mono"
|
||||
id="text52">public/*.json</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="356"
|
||||
class="small"
|
||||
id="text53"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan75"
|
||||
x="1252"
|
||||
y="356">DEV : predev copie/génère</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan76"
|
||||
x="1252"
|
||||
y="369.75">les index pour éviter les 404.</tspan></text>
|
||||
<rect
|
||||
x="1230"
|
||||
y="430"
|
||||
width="210"
|
||||
height="215"
|
||||
rx="16"
|
||||
class="boxAlt"
|
||||
id="rect53" />
|
||||
<text
|
||||
x="1252"
|
||||
y="458"
|
||||
class="h"
|
||||
id="text54">“Proposer” : 2 modes</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="482"
|
||||
class="p"
|
||||
id="text55">A) Direct client → Gitea API</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="504"
|
||||
class="small"
|
||||
id="text56">⚠️ CORS/auth/token (déconseillé)</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="536"
|
||||
class="p"
|
||||
id="text57">B) Bridge same-origin (reco)</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="558"
|
||||
class="mono"
|
||||
id="text58">PUBLIC_ISSUE_BRIDGE_PATH</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="582"
|
||||
class="small"
|
||||
id="text59"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan77"
|
||||
x="1252"
|
||||
y="582">UI → bridge → Gitea</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan78"
|
||||
x="1252"
|
||||
y="596.18488">(secrets côté serveur)</tspan></text>
|
||||
<rect
|
||||
x="1230"
|
||||
y="665"
|
||||
width="210"
|
||||
height="120"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect59" />
|
||||
<text
|
||||
x="1252"
|
||||
y="693"
|
||||
class="h"
|
||||
id="text60">Gitea (Issues)</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="717"
|
||||
class="p"
|
||||
id="text61">Création + suivi</text>
|
||||
<text
|
||||
x="1252"
|
||||
y="739"
|
||||
class="mono"
|
||||
id="text62">/issues/new</text>
|
||||
<text
|
||||
x="1232"
|
||||
y="763"
|
||||
class="small"
|
||||
id="text63">Labels/assignee selon règles d’équipe.</text>
|
||||
<!-- Callout (rappel pro) -->
|
||||
<rect
|
||||
x="470"
|
||||
y="590"
|
||||
width="720"
|
||||
height="120"
|
||||
rx="14"
|
||||
class="call"
|
||||
id="rect63" />
|
||||
<text
|
||||
x="490"
|
||||
y="618"
|
||||
class="h"
|
||||
id="text64">Rappel “pro” (anti régression)</text>
|
||||
<text
|
||||
x="490"
|
||||
y="642"
|
||||
class="p"
|
||||
id="text65">• L’ordre du postbuild ne doit pas changer sans raison : il garantit ancres stables + index cohérents.</text>
|
||||
<text
|
||||
x="490"
|
||||
y="664"
|
||||
class="p"
|
||||
id="text66">• DEV sert des index dans <tspan
|
||||
class="mono"
|
||||
id="tspan65">public/</tspan> ; PROD lit dans <tspan
|
||||
class="mono"
|
||||
id="tspan66">dist/</tspan>.</text>
|
||||
<text
|
||||
x="490"
|
||||
y="686"
|
||||
class="p"
|
||||
id="text67">• Pour “Proposer”, préférer le bridge same-origin : pas de token côté navigateur.</text>
|
||||
<!-- Arrows -->
|
||||
<path
|
||||
class="arrow"
|
||||
d="M400 460 C430 460, 450 450, 480 480"
|
||||
id="path67" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M810 480 C840 480, 860 470, 890 430"
|
||||
id="path68" />
|
||||
<path
|
||||
class="arrowSoft dash"
|
||||
d="M1030 460 C1120 500, 1180 520, 1230 330"
|
||||
id="path69" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M1180 590 C1210 590, 1220 590, 1230 540"
|
||||
id="path70" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M1340 645 C1340 660, 1340 660, 1340 665"
|
||||
id="path71" />
|
||||
<!-- Foot -->
|
||||
<text
|
||||
x="44"
|
||||
y="916"
|
||||
class="sub"
|
||||
id="text71">Astuce : si Inkscape affichait “noir”, c’était très souvent des CSS variables. Ici : couleurs explicites + fond explicite.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,510 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="900"
|
||||
viewBox="0 0 1600 900"
|
||||
version="1.1"
|
||||
id="svg53"
|
||||
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim-v2.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview53"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="655.97579"
|
||||
inkscape:cy="478.66868"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg53" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<!-- Fond clair lisible partout -->
|
||||
<linearGradient
|
||||
id="bg"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ffffff"
|
||||
id="stop1" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#f1f5f9"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<!-- Flèches -->
|
||||
<marker
|
||||
id="arrow"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#334155"
|
||||
id="path2" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowA"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#2563eb"
|
||||
id="path3" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowG"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="3"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L9,3 L0,6 Z"
|
||||
fill="#059669"
|
||||
id="path4" />
|
||||
</marker>
|
||||
<!-- Styles SANS variables CSS (compat max) -->
|
||||
<style
|
||||
id="style4"><![CDATA[
|
||||
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
|
||||
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
|
||||
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
|
||||
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
|
||||
|
||||
/* Cadres lisibles */
|
||||
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
|
||||
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
|
||||
|
||||
/* Pills */
|
||||
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
|
||||
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
|
||||
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
|
||||
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
|
||||
|
||||
/* Traits / flèches */
|
||||
.line{stroke:#334155;stroke-width:1.4;fill:none}
|
||||
.dash{stroke-dasharray:6 6}
|
||||
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
|
||||
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
|
||||
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
|
||||
]]></style>
|
||||
</defs>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="1600"
|
||||
height="900"
|
||||
fill="url(#bg)"
|
||||
id="rect4" />
|
||||
<text
|
||||
x="40"
|
||||
y="56"
|
||||
class="title"
|
||||
id="text4">Archicratie — Machine éditoriale (v2, verbatim)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="84"
|
||||
class="subtitle"
|
||||
id="text5">Détails scripts/fichiers — 2026-02-20</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="140"
|
||||
width="460"
|
||||
height="236.05144"
|
||||
class="box"
|
||||
id="rect5" />
|
||||
<text
|
||||
x="58"
|
||||
y="170"
|
||||
class="h"
|
||||
id="text6">Sources (repo)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="194"
|
||||
class="t"
|
||||
id="text7">Contenu : src/content/** (MD/MDX)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="212"
|
||||
class="t"
|
||||
id="text8">Annotations : src/annotations/** (YAML)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="230"
|
||||
class="t"
|
||||
id="text9">UI : src/layouts + src/components + global.css</text>
|
||||
<text
|
||||
x="58"
|
||||
y="248"
|
||||
class="s"
|
||||
id="text10">Plugin paragraph-ids ajoute des ids stables sur paragraphes</text>
|
||||
<rect
|
||||
x="166"
|
||||
y="296"
|
||||
width="190"
|
||||
height="28"
|
||||
class="chip"
|
||||
id="rect10" />
|
||||
<text
|
||||
x="180"
|
||||
y="315"
|
||||
class="mono"
|
||||
id="text11"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">rehype-paragraph-ids.js</text>
|
||||
<rect
|
||||
x="560"
|
||||
y="140"
|
||||
width="500"
|
||||
height="239.42511"
|
||||
class="box"
|
||||
id="rect11" />
|
||||
<text
|
||||
x="578"
|
||||
y="170"
|
||||
class="h"
|
||||
id="text12">Build (Astro static)</text>
|
||||
<text
|
||||
x="578"
|
||||
y="194"
|
||||
class="t"
|
||||
id="text13">astro build → dist/**/index.html</text>
|
||||
<text
|
||||
x="578"
|
||||
y="212"
|
||||
class="t"
|
||||
id="text14">meta Pagefind: edition/level/status/version</text>
|
||||
<text
|
||||
x="578"
|
||||
y="230"
|
||||
class="t"
|
||||
id="text15">Layout : EditionLayout + SiteLayout</text>
|
||||
<text
|
||||
x="578"
|
||||
y="248"
|
||||
class="s"
|
||||
id="text16">data-pagefind-body = zone indexée</text>
|
||||
<rect
|
||||
x="750"
|
||||
y="300"
|
||||
width="128"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect16" />
|
||||
<text
|
||||
x="764"
|
||||
y="319"
|
||||
class="mono"
|
||||
id="text17"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm run build</text>
|
||||
<rect
|
||||
x="560"
|
||||
y="430"
|
||||
width="500"
|
||||
height="280"
|
||||
class="box2"
|
||||
id="rect17" />
|
||||
<text
|
||||
x="578"
|
||||
y="460"
|
||||
class="h"
|
||||
id="text18">Postbuild (qualité + recherche + indexes)</text>
|
||||
<text
|
||||
x="578"
|
||||
y="484"
|
||||
class="t"
|
||||
id="text19">Aliases d'ancres (backward compat)</text>
|
||||
<text
|
||||
x="578"
|
||||
y="502"
|
||||
class="t"
|
||||
id="text20">Dédoublonnage d'IDs (anti-régression)</text>
|
||||
<text
|
||||
x="578"
|
||||
y="520"
|
||||
class="t"
|
||||
id="text21">Index des paragraphes (para-index)</text>
|
||||
<text
|
||||
x="578"
|
||||
y="538"
|
||||
class="t"
|
||||
id="text22">Index des annotations (annotations-index)</text>
|
||||
<text
|
||||
x="578"
|
||||
y="556"
|
||||
class="t"
|
||||
id="text23">Pagefind (recherche full-text)</text>
|
||||
<rect
|
||||
x="690.71106"
|
||||
y="578.59302"
|
||||
width="246"
|
||||
height="28"
|
||||
class="chip"
|
||||
id="rect23" />
|
||||
<text
|
||||
x="704.71106"
|
||||
y="597.59302"
|
||||
class="mono"
|
||||
id="text24"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">inject-anchor-aliases.mjs</text>
|
||||
<rect
|
||||
x="705"
|
||||
y="622"
|
||||
width="214"
|
||||
height="28"
|
||||
class="chip"
|
||||
id="rect24" />
|
||||
<text
|
||||
x="719"
|
||||
y="641"
|
||||
class="mono"
|
||||
id="text25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dedupe-ids-dist.mjs</text>
|
||||
<rect
|
||||
x="710.3858"
|
||||
y="664.52344"
|
||||
width="206"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect25" />
|
||||
<text
|
||||
x="724.3858"
|
||||
y="683.52344"
|
||||
class="mono"
|
||||
id="text26"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">build-para-index.mjs</text>
|
||||
<rect
|
||||
x="1265"
|
||||
y="660"
|
||||
width="254"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect26" />
|
||||
<text
|
||||
x="1279"
|
||||
y="679"
|
||||
class="mono"
|
||||
id="text27">build-annotations-index.mjs</text>
|
||||
<rect
|
||||
x="1120"
|
||||
y="140"
|
||||
width="440"
|
||||
height="240"
|
||||
class="box"
|
||||
id="rect27" />
|
||||
<text
|
||||
x="1138"
|
||||
y="170"
|
||||
class="h"
|
||||
id="text28">Artefacts (dist/)</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="194"
|
||||
class="mono"
|
||||
id="text29">HTML statique + assets</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="212"
|
||||
class="mono"
|
||||
id="text30">dist/pagefind/**</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="230"
|
||||
class="mono"
|
||||
id="text31">dist/para-index.json</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="248"
|
||||
class="mono"
|
||||
id="text32">dist/annotations-index.json</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="266"
|
||||
class="s"
|
||||
id="text33">(en dev) recopiés dans public/*-index.json</text>
|
||||
<rect
|
||||
x="1120"
|
||||
y="430"
|
||||
width="441.2103"
|
||||
height="280.95309"
|
||||
class="box2"
|
||||
id="rect33" />
|
||||
<text
|
||||
x="1138"
|
||||
y="460"
|
||||
class="h"
|
||||
id="text34">Runtime navigateur (lecture)</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="484"
|
||||
class="t"
|
||||
id="text35">LocalToc sync (H2/H3)</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="502"
|
||||
class="t"
|
||||
id="text36">banner-follow + reading-follow__inner</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="520"
|
||||
class="t"
|
||||
id="text37">SidePanel: niveaux + annotations + propose</text>
|
||||
<text
|
||||
x="1138"
|
||||
y="538"
|
||||
class="s"
|
||||
id="text38">Comportement lecture: H2/H3 unifiés (plus d’accordéon gênant)</text>
|
||||
<rect
|
||||
x="1263.4493"
|
||||
y="588.65356"
|
||||
width="150"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect38" />
|
||||
<text
|
||||
x="1277.4493"
|
||||
y="607.65356"
|
||||
class="mono"
|
||||
id="text39"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">SidePanel.astro</text>
|
||||
<rect
|
||||
x="1260.8547"
|
||||
y="633.4342"
|
||||
width="160"
|
||||
height="28"
|
||||
class="chip"
|
||||
id="rect39" />
|
||||
<text
|
||||
x="1274.8547"
|
||||
y="652.4342"
|
||||
class="mono"
|
||||
id="text40"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">LevelToggle.astro</text>
|
||||
<rect
|
||||
x="41.210289"
|
||||
y="431.52798"
|
||||
width="462.42056"
|
||||
height="275.41605"
|
||||
class="box2"
|
||||
id="rect40" />
|
||||
<text
|
||||
x="58"
|
||||
y="470"
|
||||
class="h"
|
||||
id="text41"
|
||||
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Flux “Proposer” (tickets)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="494"
|
||||
class="t"
|
||||
id="text42"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI collecte: page + paragraphe + type + message</text>
|
||||
<text
|
||||
x="58"
|
||||
y="512"
|
||||
class="t"
|
||||
id="text43"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Création d'issue Gitea (labels)</text>
|
||||
<text
|
||||
x="58"
|
||||
y="530"
|
||||
class="t"
|
||||
id="text44"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Lien retour: issue → page + id</text>
|
||||
<text
|
||||
x="58"
|
||||
y="548"
|
||||
class="s"
|
||||
id="text45"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Option: bridge same-origin pour éviter CORS/auth</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="610"
|
||||
width="150"
|
||||
height="28"
|
||||
class="chip"
|
||||
id="rect45" />
|
||||
<text
|
||||
x="74"
|
||||
y="629"
|
||||
class="mono"
|
||||
id="text46"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PUBLIC_GITEA_*</text>
|
||||
<rect
|
||||
x="230"
|
||||
y="610"
|
||||
width="262"
|
||||
height="28"
|
||||
class="chip2"
|
||||
id="rect46" />
|
||||
<text
|
||||
x="244"
|
||||
y="629"
|
||||
class="mono"
|
||||
id="text47"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PUBLIC_ISSUE_BRIDGE_PATH</text>
|
||||
<path
|
||||
d="M500 250 C530 250 530 250 560 250"
|
||||
class="arrowA"
|
||||
id="path47" />
|
||||
<path
|
||||
d="M810 400 C810 420 810 420 810 430"
|
||||
class="arrowA"
|
||||
id="path48" />
|
||||
<path
|
||||
d="M1060 250 C1090 250 1090 250 1120 250"
|
||||
class="arrowA"
|
||||
id="path49" />
|
||||
<path
|
||||
d="M1340 380 C1340 410 1340 410 1340 430"
|
||||
class="arrow"
|
||||
id="path50" />
|
||||
<path
|
||||
d="M500 540 C620 540 620 520 560 520"
|
||||
class="arrow"
|
||||
id="path51" />
|
||||
<text
|
||||
x="520"
|
||||
y="525"
|
||||
class="s"
|
||||
id="text51">issues</text>
|
||||
<rect
|
||||
x="40"
|
||||
y="820"
|
||||
width="1520"
|
||||
height="60"
|
||||
class="box2"
|
||||
id="rect51" />
|
||||
<text
|
||||
x="58"
|
||||
y="850"
|
||||
class="h"
|
||||
id="text52">Conseil de maintenance</text>
|
||||
<text
|
||||
x="58"
|
||||
y="874"
|
||||
class="s"
|
||||
id="text53">Toute évolution UI/indices doit rester déterministe : build identique sur Mac, CI, et NAS. En cas de hotfix, re-sync via PR.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,613 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1500"
|
||||
height="1050"
|
||||
viewBox="0 0 1500 1050"
|
||||
role="img"
|
||||
aria-label="Archicratie — Machine éditoriale (verbatim) : scripts, indices DEV/PROD, postbuild exact, proposer direct vs bridge"
|
||||
version="1.1"
|
||||
id="svg77"
|
||||
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim-v3.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview77"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.83714286"
|
||||
inkscape:cx="756.14334"
|
||||
inkscape:cy="367.32082"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg77" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<style
|
||||
id="style1">
|
||||
/* Inkscape-safe: pas de CSS variables */
|
||||
.title{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:26px; font-weight:900; fill:#0f172a;}
|
||||
.sub{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:650; fill:#475569;}
|
||||
.laneT{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:900; fill:#0f172a;}
|
||||
.laneN{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:650; fill:#475569;}
|
||||
.h{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:900; fill:#0f172a;}
|
||||
.p{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:650; fill:#475569;}
|
||||
.small{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:11px; font-weight:650; fill:#475569;}
|
||||
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; font-size:11px; font-weight:800; fill:#334155;}
|
||||
|
||||
.canvas{fill:#f8fafc; stroke:#e2e8f0; stroke-width:1;}
|
||||
.lane{fill:#f1f5f9; stroke:#cbd5e1; stroke-width:1;}
|
||||
.laneAlt{fill:#eef2f7; stroke:#cbd5e1; stroke-width:1;}
|
||||
.box{fill:#ffffff; stroke:#94a3b8; stroke-width:1.4;}
|
||||
.boxAlt{fill:#f8fafc; stroke:#94a3b8; stroke-width:1.4;}
|
||||
.call{fill:#ffffff; fill-opacity:.72; stroke:#cbd5e1; stroke-width:1.1;}
|
||||
|
||||
.arrow{stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#ah);}
|
||||
.arrowSoft{stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#ahSoft);}
|
||||
.dash{stroke-dasharray:7 6;}
|
||||
</style>
|
||||
<marker
|
||||
id="ah"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="5"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L10,5 L0,10 Z"
|
||||
fill="#64748b"
|
||||
id="path1" />
|
||||
</marker>
|
||||
<marker
|
||||
id="ahSoft"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="9"
|
||||
refY="5"
|
||||
orient="auto">
|
||||
<path
|
||||
d="M0,0 L10,5 L0,10 Z"
|
||||
fill="#94a3b8"
|
||||
id="path2" />
|
||||
</marker>
|
||||
</defs>
|
||||
<rect
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="1499"
|
||||
height="1049"
|
||||
rx="26"
|
||||
class="canvas"
|
||||
id="rect2" />
|
||||
<text
|
||||
x="44"
|
||||
y="54"
|
||||
class="title"
|
||||
id="text2">Archicratie — Machine éditoriale (verbatim technique)</text>
|
||||
<text
|
||||
x="44"
|
||||
y="82"
|
||||
class="sub"
|
||||
id="text3">3 ajouts “pro” inclus : (1) indices DEV vs PROD, (2) fork Proposer direct vs bridge, (3) ordre postbuild exact.</text>
|
||||
<!-- Lanes -->
|
||||
<rect
|
||||
x="40"
|
||||
y="115"
|
||||
width="420"
|
||||
height="880"
|
||||
rx="18"
|
||||
class="lane"
|
||||
id="rect3" />
|
||||
<rect
|
||||
x="480"
|
||||
y="115"
|
||||
width="520"
|
||||
height="880"
|
||||
rx="18"
|
||||
class="laneAlt"
|
||||
id="rect4" />
|
||||
<rect
|
||||
x="1020"
|
||||
y="115"
|
||||
width="440"
|
||||
height="880"
|
||||
rx="18"
|
||||
class="lane"
|
||||
id="rect5" />
|
||||
<text
|
||||
x="60"
|
||||
y="148"
|
||||
class="laneT"
|
||||
id="text5">A) Entrées & canon</text>
|
||||
<text
|
||||
x="60"
|
||||
y="170"
|
||||
class="laneN"
|
||||
id="text6">Ce qui est versionné et alimente la build</text>
|
||||
<text
|
||||
x="500"
|
||||
y="148"
|
||||
class="laneT"
|
||||
id="text7">B) Build + postbuild</text>
|
||||
<text
|
||||
x="500"
|
||||
y="170"
|
||||
class="laneN"
|
||||
id="text8">Astro (static) + scripts (ordre fixe)</text>
|
||||
<text
|
||||
x="1040"
|
||||
y="148"
|
||||
class="laneT"
|
||||
id="text9">C) Runtime (DEV/PROD) + “Proposer”</text>
|
||||
<text
|
||||
x="1040"
|
||||
y="170"
|
||||
class="laneN"
|
||||
id="text10">Indices servis, UI, et création d’issues</text>
|
||||
<!-- A) Entrées -->
|
||||
<rect
|
||||
x="70"
|
||||
y="210"
|
||||
width="360"
|
||||
height="165"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect10" />
|
||||
<text
|
||||
x="92"
|
||||
y="238"
|
||||
class="h"
|
||||
id="text11">Contenu canon (pages)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="260"
|
||||
class="p"
|
||||
id="text12">Astro Content : MD / MDX (pages, chapitres, etc.)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="284"
|
||||
class="mono"
|
||||
id="text13">src/content/**</text>
|
||||
<text
|
||||
x="92"
|
||||
y="310"
|
||||
class="small"
|
||||
id="text14">Layouts/TOC/reading-follow consomment ces pages.</text>
|
||||
<text
|
||||
x="92"
|
||||
y="334"
|
||||
class="small"
|
||||
id="text15">Les IDs de paragraphes doivent rester stables (anti-régression).</text>
|
||||
<rect
|
||||
x="70"
|
||||
y="395"
|
||||
width="360"
|
||||
height="190"
|
||||
rx="16"
|
||||
class="boxAlt"
|
||||
id="rect15" />
|
||||
<text
|
||||
x="92"
|
||||
y="423"
|
||||
class="h"
|
||||
id="text16">Annotations (surcouche)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="445"
|
||||
class="p"
|
||||
id="text17">YAML : notes, refs, illus, commentaires par paragraphe.</text>
|
||||
<text
|
||||
x="92"
|
||||
y="468"
|
||||
class="mono"
|
||||
id="text18">src/annotations/**</text>
|
||||
<text
|
||||
x="92"
|
||||
y="494"
|
||||
class="p"
|
||||
id="text19">Indexé en JSON pour le SidePanel :</text>
|
||||
<text
|
||||
x="92"
|
||||
y="516"
|
||||
class="mono"
|
||||
id="text20">dist/annotations-index.json (PROD)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="538"
|
||||
class="mono"
|
||||
id="text21">public/annotations-index.json (DEV)</text>
|
||||
<rect
|
||||
x="70"
|
||||
y="605"
|
||||
width="360"
|
||||
height="170"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect21" />
|
||||
<text
|
||||
x="92"
|
||||
y="633"
|
||||
class="h"
|
||||
id="text22">Scripts (import & qualité)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="655"
|
||||
class="p"
|
||||
id="text23">Import DOCX, checks d’IDs, aliases d’ancres, etc.</text>
|
||||
<text
|
||||
x="92"
|
||||
y="678"
|
||||
class="mono"
|
||||
id="text24">scripts/import-docx.mjs</text>
|
||||
<text
|
||||
x="92"
|
||||
y="700"
|
||||
class="mono"
|
||||
id="text25">scripts/check-anchors.mjs</text>
|
||||
<text
|
||||
x="92"
|
||||
y="724"
|
||||
class="small"
|
||||
id="text26">Objectif : build reproductible + compat backward (ancres).</text>
|
||||
<!-- B) Build + postbuild -->
|
||||
<rect
|
||||
x="510"
|
||||
y="210"
|
||||
width="460"
|
||||
height="190"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect26" />
|
||||
<text
|
||||
x="532"
|
||||
y="238"
|
||||
class="h"
|
||||
id="text27">Build Astro (static)</text>
|
||||
<text
|
||||
x="532"
|
||||
y="260"
|
||||
class="p"
|
||||
id="text28">Génère le HTML + assets + routes (output: static).</text>
|
||||
<text
|
||||
x="532"
|
||||
y="284"
|
||||
class="mono"
|
||||
id="text29">npm run build</text>
|
||||
<text
|
||||
x="532"
|
||||
y="306"
|
||||
class="mono"
|
||||
id="text30">→ astro build → dist/**</text>
|
||||
<text
|
||||
x="532"
|
||||
y="330"
|
||||
class="p"
|
||||
id="text31">UI d’édition (côté pages) :</text>
|
||||
<text
|
||||
x="532"
|
||||
y="352"
|
||||
class="mono"
|
||||
id="text32">src/layouts/EditionLayout.astro</text>
|
||||
<text
|
||||
x="532"
|
||||
y="374"
|
||||
class="mono"
|
||||
id="text33">src/components/SidePanel.astro</text>
|
||||
<rect
|
||||
x="510"
|
||||
y="420"
|
||||
width="460"
|
||||
height="330"
|
||||
rx="16"
|
||||
class="boxAlt"
|
||||
id="rect33" />
|
||||
<text
|
||||
x="532"
|
||||
y="448"
|
||||
class="h"
|
||||
id="text34">Postbuild (ordre exact = contrat)</text>
|
||||
<text
|
||||
x="532"
|
||||
y="472"
|
||||
class="p"
|
||||
id="text35">À conserver tel quel pour éviter les régressions.</text>
|
||||
<text
|
||||
x="532"
|
||||
y="500"
|
||||
class="mono"
|
||||
id="text36">1) node scripts/inject-anchor-aliases.mjs</text>
|
||||
<text
|
||||
x="532"
|
||||
y="522"
|
||||
class="mono"
|
||||
id="text37">2) node scripts/dedupe-ids-dist.mjs</text>
|
||||
<text
|
||||
x="532"
|
||||
y="544"
|
||||
class="mono"
|
||||
id="text38"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan79"
|
||||
x="532"
|
||||
y="544">3) node scripts/build-para-index.mjs</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan80"
|
||||
x="532"
|
||||
y="557.75">--in dist --out dist/para-index.json</tspan></text>
|
||||
<text
|
||||
x="532"
|
||||
y="578"
|
||||
class="mono"
|
||||
id="text39"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan81"
|
||||
x="532"
|
||||
y="578">4) node scripts/build-annotations-index.mjs</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan82"
|
||||
x="532"
|
||||
y="591.75">--in src/annotations --out dist/annotations-index.json</tspan></text>
|
||||
<text
|
||||
x="532"
|
||||
y="612"
|
||||
class="mono"
|
||||
id="text40">5) npx pagefind --site dist (→ dist/pagefind/**)</text>
|
||||
<text
|
||||
x="532"
|
||||
y="638"
|
||||
class="p"
|
||||
id="text41">Sorties PROD (statique) :</text>
|
||||
<text
|
||||
x="532"
|
||||
y="660"
|
||||
class="mono"
|
||||
id="text42">dist/para-index.json</text>
|
||||
<text
|
||||
x="532"
|
||||
y="682"
|
||||
class="mono"
|
||||
id="text43">dist/annotations-index.json</text>
|
||||
<text
|
||||
x="532"
|
||||
y="704"
|
||||
class="mono"
|
||||
id="text44">dist/pagefind/**</text>
|
||||
<text
|
||||
x="532"
|
||||
y="736"
|
||||
class="small"
|
||||
id="text45">Mini-règle : inject (aliases) AVANT dedupe, et indices AVANT pagefind.</text>
|
||||
<rect
|
||||
x="510"
|
||||
y="770"
|
||||
width="460"
|
||||
height="195"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect45" />
|
||||
<text
|
||||
x="532"
|
||||
y="798"
|
||||
class="h"
|
||||
id="text46">DEV server : indices “public/” (mini-ajout #1)</text>
|
||||
<text
|
||||
x="532"
|
||||
y="822"
|
||||
class="p"
|
||||
id="text47">En DEV, l’UI lit via HTTP depuis <tspan
|
||||
class="mono"
|
||||
id="tspan46">public/</tspan> (pas <tspan
|
||||
class="mono"
|
||||
id="tspan47">dist/</tspan>).</text>
|
||||
<text
|
||||
x="532"
|
||||
y="846"
|
||||
class="mono"
|
||||
id="text48">predev: build-annotations-index → public/annotations-index.json</text>
|
||||
<text
|
||||
x="532"
|
||||
y="868"
|
||||
class="mono"
|
||||
id="text49">predev: build-para-index (depuis dist) → public/para-index.json</text>
|
||||
<text
|
||||
x="532"
|
||||
y="892"
|
||||
class="small"
|
||||
id="text50">Si ces fichiers manquent : 404 (normal) → relancer <tspan
|
||||
class="mono"
|
||||
id="tspan49">npm run dev</tspan> (predev).</text>
|
||||
<!-- C) Runtime + proposer -->
|
||||
<rect
|
||||
x="1050"
|
||||
y="210"
|
||||
width="380"
|
||||
height="200"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect50" />
|
||||
<text
|
||||
x="1072"
|
||||
y="238"
|
||||
class="h"
|
||||
id="text51">Runtime PROD</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="262"
|
||||
class="p"
|
||||
id="text52">Site statique servi depuis dist/**</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="286"
|
||||
class="mono"
|
||||
id="text53">dist/*.html</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="308"
|
||||
class="mono"
|
||||
id="text54">dist/para-index.json</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="330"
|
||||
class="mono"
|
||||
id="text55">dist/annotations-index.json</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="352"
|
||||
class="mono"
|
||||
id="text56">dist/pagefind/**</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="378"
|
||||
class="small"
|
||||
id="text57">Ces artefacts sont reproductibles via CI.</text>
|
||||
<rect
|
||||
x="1050"
|
||||
y="430"
|
||||
width="380"
|
||||
height="250"
|
||||
rx="16"
|
||||
class="boxAlt"
|
||||
id="rect57" />
|
||||
<text
|
||||
x="1072"
|
||||
y="458"
|
||||
class="h"
|
||||
id="text58">“Proposer” (mini-ajout #2)</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="482"
|
||||
class="p"
|
||||
id="text59">Depuis SidePanel / ProposeModal (UI).</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="512"
|
||||
class="p"
|
||||
id="text60">Mode A — Direct navigateur → Gitea API</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="534"
|
||||
class="small"
|
||||
id="text61">⚠️ CORS + auth + token : fragile / déconseillé.</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="566"
|
||||
class="p"
|
||||
id="text62">Mode B — Bridge same-origin (recommandé)</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="588"
|
||||
class="mono"
|
||||
id="text63">PUBLIC_ISSUE_BRIDGE_PATH (ex: /bridge/issues)</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="612"
|
||||
class="small"
|
||||
id="text64">Le serveur/proxy ajoute l’auth (secrets) et appelle Gitea côté backend.</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="638"
|
||||
class="small"
|
||||
id="text65">Résultat : pas de secret exposé au client + moins de soucis CORS.</text>
|
||||
<rect
|
||||
x="1050"
|
||||
y="700"
|
||||
width="380"
|
||||
height="165"
|
||||
rx="16"
|
||||
class="box"
|
||||
id="rect65" />
|
||||
<text
|
||||
x="1072"
|
||||
y="728"
|
||||
class="h"
|
||||
id="text66">Gitea : Issues</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="752"
|
||||
class="p"
|
||||
id="text67">Création de tickets (PR/CI séparés du flux éditorial)</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="776"
|
||||
class="mono"
|
||||
id="text68">/issues/new</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="798"
|
||||
class="mono"
|
||||
id="text69">labels / assignee / templates</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="828"
|
||||
class="small"
|
||||
id="text70">Le workflow Git/PR/CI reste la source canon (pas la prod).</text>
|
||||
<!-- Callout -->
|
||||
<rect
|
||||
x="1050"
|
||||
y="885"
|
||||
width="380"
|
||||
height="95.972694"
|
||||
rx="14"
|
||||
class="call"
|
||||
id="rect70" />
|
||||
<text
|
||||
x="1072"
|
||||
y="912"
|
||||
class="h"
|
||||
id="text71">Mini-ajout #3 — ordre postbuild</text>
|
||||
<text
|
||||
x="1072"
|
||||
y="936"
|
||||
class="p"
|
||||
id="text72"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan77"
|
||||
x="1072"
|
||||
y="936">inject-anchor-aliases → dedupe-ids → para-index</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan78"
|
||||
x="1072"
|
||||
y="951.47437">→ annotations-index → pagefind</tspan></text>
|
||||
<text
|
||||
x="1072"
|
||||
y="968"
|
||||
class="small"
|
||||
id="text73">C’est le “contrat” anti-régression : si tu changes l’ordre, tu re-tests tout.</text>
|
||||
<!-- Arrows (liaisons principales) -->
|
||||
<path
|
||||
class="arrow"
|
||||
d="M430 300 C455 300, 475 300, 510 300"
|
||||
id="path73" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M430 500 C455 500, 480 490, 510 520"
|
||||
id="path74" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M970 330 C1000 330, 1010 330, 1050 330"
|
||||
id="path75" />
|
||||
<path
|
||||
class="arrowSoft dash"
|
||||
d="M 967.84983,823.85666 C 1006.4505,737.84983 1012.5597,719.6587 1050,610"
|
||||
id="path76"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
class="arrow"
|
||||
d="M1240 680 C1240 695, 1240 695, 1240 700"
|
||||
id="path77" />
|
||||
<text
|
||||
x="44"
|
||||
y="1028"
|
||||
class="sub"
|
||||
id="text77">Inkscape-safe : couleurs & fond explicites (zéro var()).</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,634 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="1020"
|
||||
viewBox="0 0 1600 1020"
|
||||
version="1.1"
|
||||
id="svg77"
|
||||
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
inkscape:export-filename="out/archicratie-web-edition-machine-editoriale-verbatim.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview77"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="524.05446"
|
||||
inkscape:cy="684.41755"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="235"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg77" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<marker
|
||||
id="arrow"
|
||||
viewBox="0 0 10 10"
|
||||
refX="9.5"
|
||||
refY="5"
|
||||
markerWidth="8"
|
||||
markerHeight="8"
|
||||
orient="auto-start-reverse">
|
||||
<path
|
||||
d="M 0 0 L 10 5 L 0 10 z"
|
||||
fill="#222"
|
||||
id="path1" />
|
||||
</marker>
|
||||
<style
|
||||
id="style1">
|
||||
.title { font: 700 22px sans-serif; fill:#111; }
|
||||
.small { font: 12px sans-serif; fill:#111; }
|
||||
.h2 { font: 700 16px sans-serif; fill:#111; }
|
||||
.h3 { font: 700 14px sans-serif; fill:#111; }
|
||||
.txt { font: 13px sans-serif; fill:#111; }
|
||||
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
|
||||
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
|
||||
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
|
||||
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
|
||||
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
|
||||
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<!-- Header -->
|
||||
<text
|
||||
x="40"
|
||||
y="45"
|
||||
class="title"
|
||||
id="text1">Archicratie – Web Edition : Machine éditoriale VERBATIM (Proposer / Citer / /_auth/whoami / aliases / apply-ticket)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="75"
|
||||
class="small"
|
||||
id="text2">Sources “vraies” : src/layouts/EditionLayout.astro (WHOAMI_PATH="/_auth/whoami", GITEA_* via import.meta.env.PUBLIC_*), src/anchors/anchor-aliases.json, scripts/inject-anchor-aliases.mjs, scripts/apply-ticket.mjs --alias.</text>
|
||||
<!-- ZONE A: Runtime -->
|
||||
<rect
|
||||
x="35"
|
||||
y="110"
|
||||
width="1530"
|
||||
height="420"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect2" />
|
||||
<text
|
||||
x="60"
|
||||
y="145"
|
||||
class="h2"
|
||||
id="text3">A — Runtime (navigateur) : paragraphe → outils (¶ / Citer / Proposer) → issue Gitea</text>
|
||||
<!-- Reader -->
|
||||
<rect
|
||||
x="60"
|
||||
y="175"
|
||||
width="380"
|
||||
height="160"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect3" />
|
||||
<text
|
||||
x="80"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text4">Utilisateur (lecteur/éditeur)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text5">• lit une page</text>
|
||||
<text
|
||||
x="80"
|
||||
y="252"
|
||||
class="txt"
|
||||
id="text6"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• sur un paragraphe : Citer / Proposer / Marque-page</text>
|
||||
<text
|
||||
x="80"
|
||||
y="274"
|
||||
class="txt"
|
||||
id="text7">• Proposer visible uniquement si “editors”</text>
|
||||
<text
|
||||
x="80"
|
||||
y="300"
|
||||
class="small"
|
||||
id="text8">(le gate est runtime via whoami)</text>
|
||||
<!-- Astro page + EditionLayout -->
|
||||
<rect
|
||||
x="470"
|
||||
y="175"
|
||||
width="560"
|
||||
height="330"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect8" />
|
||||
<text
|
||||
x="490"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text9">Site Astro statique — EditionLayout</text>
|
||||
<text
|
||||
x="490"
|
||||
y="230"
|
||||
class="mono"
|
||||
id="text10">src/layouts/EditionLayout.astro</text>
|
||||
<text
|
||||
x="490"
|
||||
y="260"
|
||||
class="h3"
|
||||
id="text11">Variables publiques injectées</text>
|
||||
<text
|
||||
x="490"
|
||||
y="282"
|
||||
class="txt"
|
||||
id="text13">• <tspan
|
||||
class="mono"
|
||||
id="tspan11">PUBLIC_GITEA_BASE</tspan>, <tspan
|
||||
class="mono"
|
||||
id="tspan12">PUBLIC_GITEA_OWNER</tspan>, <tspan
|
||||
class="mono"
|
||||
id="tspan13">PUBLIC_GITEA_REPO</tspan></text>
|
||||
<text
|
||||
x="490"
|
||||
y="304"
|
||||
class="txt"
|
||||
id="text14">• si une manque : giteaReady=false → Proposer désactivé</text>
|
||||
<text
|
||||
x="490"
|
||||
y="338"
|
||||
class="h3"
|
||||
id="text15">Outils paragraphe</text>
|
||||
<text
|
||||
x="490"
|
||||
y="362"
|
||||
class="txt"
|
||||
id="text17"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Citer : copie une citation structurée (titre + URL#ancre)</text>
|
||||
<text
|
||||
x="490"
|
||||
y="384"
|
||||
class="txt"
|
||||
id="text18"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Proposer : modal 2 étapes → ouvre <tspan
|
||||
class="mono"
|
||||
id="tspan17"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/issues/new?...</tspan></text>
|
||||
<text
|
||||
x="490"
|
||||
y="418"
|
||||
class="h3"
|
||||
id="text19"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:14px;line-height:normal;font-family:sans-serif">Gate “editors” (whoami)</text>
|
||||
<text
|
||||
x="490"
|
||||
y="440"
|
||||
class="txt"
|
||||
id="text20"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• <tspan
|
||||
class="mono"
|
||||
id="tspan19"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">WHOAMI_PATH="/_auth/whoami"</tspan> + fetch same-origin</text>
|
||||
<text
|
||||
x="490"
|
||||
y="462"
|
||||
class="txt"
|
||||
id="text21"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• lit header <tspan
|
||||
class="mono"
|
||||
id="tspan20"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">Remote-Groups</tspan> → affiche/retire Proposer du DOM</text>
|
||||
<!-- Edge/Auth -->
|
||||
<rect
|
||||
x="1060"
|
||||
y="175"
|
||||
width="250"
|
||||
height="160"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect21" />
|
||||
<text
|
||||
x="1080"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text22">Traefik edge</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text23">• Host(archicratie…)</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="252"
|
||||
class="txt"
|
||||
id="text24">• middleware : chain-auth</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="274"
|
||||
class="txt"
|
||||
id="text25">• route <tspan
|
||||
class="mono"
|
||||
id="tspan24">/_auth/whoami</tspan></text>
|
||||
<text
|
||||
x="1080"
|
||||
y="300"
|
||||
class="small"
|
||||
id="text26">forward-auth vers Authelia</text>
|
||||
<rect
|
||||
x="1060"
|
||||
y="350"
|
||||
width="250"
|
||||
height="155"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect26" />
|
||||
<text
|
||||
x="1080"
|
||||
y="380"
|
||||
class="h2"
|
||||
id="text27">Authelia + LLDAP</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="405"
|
||||
class="txt"
|
||||
id="text28">• forward-auth : <tspan
|
||||
class="mono"
|
||||
id="tspan27">:9091</tspan></text>
|
||||
<text
|
||||
x="1080"
|
||||
y="427"
|
||||
class="txt"
|
||||
id="text29">• groupes via LDAP</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="449"
|
||||
class="txt"
|
||||
id="text30">• injecte headers Remote-*</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="471"
|
||||
class="small"
|
||||
id="text31">non auth ⇒ 302 vers auth.*</text>
|
||||
<!-- Gitea issue -->
|
||||
<rect
|
||||
x="1330"
|
||||
y="175"
|
||||
width="235"
|
||||
height="330"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect31" />
|
||||
<text
|
||||
x="1350"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text32">Gitea (UI)</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text33">Issue préremplie :</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="255"
|
||||
class="mono"
|
||||
id="text34">BASE/OWNER/REPO/issues/new</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="287"
|
||||
class="txt"
|
||||
id="text35">Contenu typique :</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="309"
|
||||
class="txt"
|
||||
id="text36">• URL page + <tspan
|
||||
class="mono"
|
||||
id="tspan35">#p-…</tspan></text>
|
||||
<text
|
||||
x="1350"
|
||||
y="331"
|
||||
class="txt"
|
||||
id="text37">• Type / State / Category</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="353"
|
||||
class="txt"
|
||||
id="text38">• proposition / commentaire</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="385"
|
||||
class="txt"
|
||||
id="text39">Résultat :</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="407"
|
||||
class="txt"
|
||||
id="text40">• issue = backlog éditorial</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="429"
|
||||
class="txt"
|
||||
id="text41">• labels (CI/bot) pour tri</text>
|
||||
<!-- Runtime connections -->
|
||||
<path
|
||||
d="M 440 260 L 470 260"
|
||||
class="line"
|
||||
id="path41" />
|
||||
<path
|
||||
d="M 1030 470 L 1060 470"
|
||||
class="dash"
|
||||
id="path42" />
|
||||
<path
|
||||
d="M 1310 320 L 1330 320"
|
||||
class="line"
|
||||
id="path43" />
|
||||
<path
|
||||
d="M 1030 270 L 1060 270"
|
||||
class="dash"
|
||||
id="path44" />
|
||||
<rect
|
||||
x="60"
|
||||
y="355"
|
||||
width="380"
|
||||
height="150"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect44" />
|
||||
<text
|
||||
x="80"
|
||||
y="385"
|
||||
class="h2"
|
||||
id="text44">Contrat runtime (robuste)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="410"
|
||||
class="txt"
|
||||
id="text45">• Citer marche sans droits (copie + lien)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="432"
|
||||
class="txt"
|
||||
id="text46">• Proposer n’existe pas si non “editors”</text>
|
||||
<text
|
||||
x="80"
|
||||
y="454"
|
||||
class="txt"
|
||||
id="text47">• whoami renvoie 302 login si non auth</text>
|
||||
<text
|
||||
x="80"
|
||||
y="476"
|
||||
class="txt"
|
||||
id="text48">• si PUBLIC_GITEA_* faux → 404/login loop</text>
|
||||
<!-- ZONE B: CI / labels -->
|
||||
<rect
|
||||
x="35"
|
||||
y="545"
|
||||
width="1530"
|
||||
height="210"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect48" />
|
||||
<text
|
||||
x="60"
|
||||
y="580"
|
||||
class="h2"
|
||||
id="text49">B — Automatisation : issue → labels + checks qualité</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="610"
|
||||
width="520"
|
||||
height="120"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect49" />
|
||||
<text
|
||||
x="80"
|
||||
y="640"
|
||||
class="h2"
|
||||
id="text50">Gitea Actions (workflows)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="665"
|
||||
class="txt"
|
||||
id="text51">• triggers : issues opened / edited (labels)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="687"
|
||||
class="txt"
|
||||
id="text52">• checks build : anchors / aliases / inline-js / dist audit</text>
|
||||
<text
|
||||
x="80"
|
||||
y="709"
|
||||
class="small"
|
||||
id="text53">token API requis côté job (ex : FORGE_TOKEN) pour écrire labels</text>
|
||||
<rect
|
||||
x="610"
|
||||
y="610"
|
||||
width="430"
|
||||
height="120"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect53" />
|
||||
<text
|
||||
x="630"
|
||||
y="640"
|
||||
class="h2"
|
||||
id="text54">Runner</text>
|
||||
<text
|
||||
x="630"
|
||||
y="665"
|
||||
class="txt"
|
||||
id="text55">• conteneur : <tspan
|
||||
class="mono"
|
||||
id="tspan54">gitea-act-runner (gitea/act_runner)</tspan></text>
|
||||
<text
|
||||
x="630"
|
||||
y="687"
|
||||
class="txt"
|
||||
id="text56">• exécute jobs (souvent en conteneur)</text>
|
||||
<text
|
||||
x="630"
|
||||
y="709"
|
||||
class="txt"
|
||||
id="text57">• appelle API Gitea pour labels</text>
|
||||
<rect
|
||||
x="1070"
|
||||
y="610"
|
||||
width="465"
|
||||
height="120"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect57" />
|
||||
<text
|
||||
x="1090"
|
||||
y="640"
|
||||
class="h2"
|
||||
id="text58">Gitea (API) + labels</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="665"
|
||||
class="txt"
|
||||
id="text59">• labels = tri natif (type/state/cat)</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="687"
|
||||
class="txt"
|
||||
id="text60">• backlog propre, opérable</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="709"
|
||||
class="small"
|
||||
id="text61">si 401 : token manquant/mauvais droits</text>
|
||||
<path
|
||||
d="M 580 670 L 610 670"
|
||||
class="line"
|
||||
id="path61" />
|
||||
<path
|
||||
d="M 1040 670 L 1070 670"
|
||||
class="line"
|
||||
id="path62" />
|
||||
<!-- ZONE C: Re-integration + anchors -->
|
||||
<rect
|
||||
x="35"
|
||||
y="780"
|
||||
width="1530"
|
||||
height="210"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect62" />
|
||||
<text
|
||||
x="400"
|
||||
y="815"
|
||||
class="h2"
|
||||
id="text62"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">C — Réintégration : correction → contenu web + stabilité des ancres (aliases build-time)</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="850"
|
||||
width="520"
|
||||
height="115"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect63" />
|
||||
<text
|
||||
x="80"
|
||||
y="880"
|
||||
class="h2"
|
||||
id="text63">Apply-ticket</text>
|
||||
<text
|
||||
x="80"
|
||||
y="905"
|
||||
class="mono"
|
||||
id="text64">scripts/apply-ticket.mjs <issue_number> --alias</text>
|
||||
<text
|
||||
x="80"
|
||||
y="929"
|
||||
class="txt"
|
||||
id="text65">• applique patch dans src/content/…</text>
|
||||
<text
|
||||
x="80"
|
||||
y="951"
|
||||
class="txt"
|
||||
id="text66">• écrit alias old→new dans <tspan
|
||||
class="mono"
|
||||
id="tspan65">src/anchors/anchor-aliases.json</tspan></text>
|
||||
<rect
|
||||
x="610.74738"
|
||||
y="848.78973"
|
||||
width="591.40692"
|
||||
height="117.42059"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect66" />
|
||||
<text
|
||||
x="630"
|
||||
y="880"
|
||||
class="h2"
|
||||
id="text67">Aliases canon + injection</text>
|
||||
<text
|
||||
x="630"
|
||||
y="905"
|
||||
class="mono"
|
||||
id="text68">src/anchors/anchor-aliases.json</text>
|
||||
<text
|
||||
x="630"
|
||||
y="929"
|
||||
class="txt"
|
||||
id="text69">postbuild : <tspan
|
||||
class="mono"
|
||||
id="tspan68">node scripts/inject-anchor-aliases.mjs</tspan></text>
|
||||
<text
|
||||
x="630"
|
||||
y="951"
|
||||
class="txt"
|
||||
id="text70">• injecte <tspan
|
||||
class="mono"
|
||||
id="tspan69"><span id="oldId" class="para-alias"></tspan> avant newId dans dist/**/index.html</text>
|
||||
<rect
|
||||
x="1227.5703"
|
||||
y="850"
|
||||
width="331.42966"
|
||||
height="115"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect70" />
|
||||
<text
|
||||
x="1240"
|
||||
y="880"
|
||||
class="h2"
|
||||
id="text71"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Preuves (tests)</text>
|
||||
<text
|
||||
x="1240"
|
||||
y="905"
|
||||
class="txt"
|
||||
id="text72"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• <tspan
|
||||
class="mono"
|
||||
id="tspan71"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/check-anchor-aliases.mjs</tspan></text>
|
||||
<text
|
||||
x="1240"
|
||||
y="929"
|
||||
class="txt"
|
||||
id="text73"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• <tspan
|
||||
class="mono"
|
||||
id="tspan72"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/verify-anchor-aliases-in-dist.mjs</tspan></text>
|
||||
<text
|
||||
x="1240"
|
||||
y="951"
|
||||
class="txt"
|
||||
id="text74"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• <tspan
|
||||
class="mono"
|
||||
id="tspan73"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/check-anchors.mjs</tspan></text>
|
||||
<path
|
||||
d="M 1495 545 L 1495 505"
|
||||
class="dash"
|
||||
id="path74" />
|
||||
<path
|
||||
d="M 320 730 L 320 850"
|
||||
class="line"
|
||||
id="path75" />
|
||||
<path
|
||||
d="M 580 908 L 610 908"
|
||||
class="line"
|
||||
id="path76" />
|
||||
<path
|
||||
d="M 1202.0514,908 H 1226"
|
||||
class="line"
|
||||
id="path77"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
631
docs/diagrams/archicratie-web-edition-machine-editoriale.svg
Normal file
@@ -0,0 +1,631 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="1020"
|
||||
viewBox="0 0 1600 1020"
|
||||
version="1.1"
|
||||
id="svg77"
|
||||
sodipodi:docname="archicratie-web-edition-machine-editoriale.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
inkscape:export-filename="out/archicratie-web-edition-machine-editoriale.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview77"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="409.07716"
|
||||
inkscape:cy="573.0711"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg77" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<marker
|
||||
id="arrow"
|
||||
viewBox="0 0 10 10"
|
||||
refX="9.5"
|
||||
refY="5"
|
||||
markerWidth="8"
|
||||
markerHeight="8"
|
||||
orient="auto-start-reverse">
|
||||
<path
|
||||
d="M 0 0 L 10 5 L 0 10 z"
|
||||
fill="#222"
|
||||
id="path1" />
|
||||
</marker>
|
||||
<style
|
||||
id="style1">
|
||||
.title { font: 700 22px sans-serif; fill:#111; }
|
||||
.small { font: 12px sans-serif; fill:#111; }
|
||||
.h2 { font: 700 16px sans-serif; fill:#111; }
|
||||
.h3 { font: 700 14px sans-serif; fill:#111; }
|
||||
.txt { font: 13px sans-serif; fill:#111; }
|
||||
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
|
||||
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
|
||||
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
|
||||
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
|
||||
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
|
||||
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<!-- Header -->
|
||||
<text
|
||||
x="40"
|
||||
y="45"
|
||||
class="title"
|
||||
id="text1">Archicratie – Web Edition : Machine éditoriale VERBATIM (Proposer / Citer / /_auth/whoami / aliases / apply-ticket)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="75"
|
||||
class="small"
|
||||
id="text2">Sources “vraies” : src/layouts/EditionLayout.astro (WHOAMI_PATH="/_auth/whoami", GITEA_* via import.meta.env.PUBLIC_*), src/anchors/anchor-aliases.json, scripts/inject-anchor-aliases.mjs, scripts/apply-ticket.mjs --alias.</text>
|
||||
<!-- ZONE A: Runtime -->
|
||||
<rect
|
||||
x="35"
|
||||
y="110"
|
||||
width="1530"
|
||||
height="420"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect2" />
|
||||
<text
|
||||
x="60"
|
||||
y="145"
|
||||
class="h2"
|
||||
id="text3">A — Runtime (navigateur) : paragraphe → outils (¶ / Citer / Proposer) → issue Gitea</text>
|
||||
<!-- Reader -->
|
||||
<rect
|
||||
x="60"
|
||||
y="175"
|
||||
width="380"
|
||||
height="160"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect3" />
|
||||
<text
|
||||
x="80"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text4">Utilisateur (lecteur/éditeur)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text5">• lit une page</text>
|
||||
<text
|
||||
x="80"
|
||||
y="252"
|
||||
class="txt"
|
||||
id="text6">• sur un paragraphe : ¶ / Citer / Proposer</text>
|
||||
<text
|
||||
x="80"
|
||||
y="274"
|
||||
class="txt"
|
||||
id="text7">• Proposer visible uniquement si “editors”</text>
|
||||
<text
|
||||
x="80"
|
||||
y="300"
|
||||
class="small"
|
||||
id="text8">(le gate est runtime via whoami)</text>
|
||||
<!-- Astro page + EditionLayout -->
|
||||
<rect
|
||||
x="470"
|
||||
y="175"
|
||||
width="560"
|
||||
height="330"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect8" />
|
||||
<text
|
||||
x="490"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text9">Site Astro statique — EditionLayout</text>
|
||||
<text
|
||||
x="490"
|
||||
y="230"
|
||||
class="mono"
|
||||
id="text10">src/layouts/EditionLayout.astro</text>
|
||||
<text
|
||||
x="490"
|
||||
y="260"
|
||||
class="h3"
|
||||
id="text11">Variables publiques injectées</text>
|
||||
<text
|
||||
x="490"
|
||||
y="282"
|
||||
class="txt"
|
||||
id="text13">• <tspan
|
||||
class="mono"
|
||||
id="tspan11">PUBLIC_GITEA_BASE</tspan>, <tspan
|
||||
class="mono"
|
||||
id="tspan12">PUBLIC_GITEA_OWNER</tspan>, <tspan
|
||||
class="mono"
|
||||
id="tspan13">PUBLIC_GITEA_REPO</tspan></text>
|
||||
<text
|
||||
x="490"
|
||||
y="304"
|
||||
class="txt"
|
||||
id="text14">• si une manque : giteaReady=false → Proposer désactivé</text>
|
||||
<text
|
||||
x="490"
|
||||
y="338"
|
||||
class="h3"
|
||||
id="text15">Outils paragraphe</text>
|
||||
<text
|
||||
x="490"
|
||||
y="360"
|
||||
class="txt"
|
||||
id="text16">• ¶ : lien d’ancre vers <tspan
|
||||
class="mono"
|
||||
id="tspan15">#p-…</tspan></text>
|
||||
<text
|
||||
x="490"
|
||||
y="382"
|
||||
class="txt"
|
||||
id="text17">• Citer : copie une citation structurée (titre + URL#ancre)</text>
|
||||
<text
|
||||
x="490"
|
||||
y="404"
|
||||
class="txt"
|
||||
id="text18">• Proposer : modal 2 étapes → ouvre <tspan
|
||||
class="mono"
|
||||
id="tspan17">/issues/new?...</tspan></text>
|
||||
<text
|
||||
x="490"
|
||||
y="438"
|
||||
class="h3"
|
||||
id="text19">Gate “editors” (whoami)</text>
|
||||
<text
|
||||
x="490"
|
||||
y="460"
|
||||
class="txt"
|
||||
id="text20">• <tspan
|
||||
class="mono"
|
||||
id="tspan19">WHOAMI_PATH="/_auth/whoami"</tspan> + fetch same-origin</text>
|
||||
<text
|
||||
x="490"
|
||||
y="482"
|
||||
class="txt"
|
||||
id="text21">• lit header <tspan
|
||||
class="mono"
|
||||
id="tspan20">Remote-Groups</tspan> → affiche/retire Proposer du DOM</text>
|
||||
<!-- Edge/Auth -->
|
||||
<rect
|
||||
x="1060"
|
||||
y="175"
|
||||
width="250"
|
||||
height="160"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect21" />
|
||||
<text
|
||||
x="1080"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text22">Traefik edge</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text23">• Host(archicratie…)</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="252"
|
||||
class="txt"
|
||||
id="text24">• middleware : chain-auth</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="274"
|
||||
class="txt"
|
||||
id="text25">• route <tspan
|
||||
class="mono"
|
||||
id="tspan24">/_auth/whoami</tspan></text>
|
||||
<text
|
||||
x="1080"
|
||||
y="300"
|
||||
class="small"
|
||||
id="text26">forward-auth vers Authelia</text>
|
||||
<rect
|
||||
x="1060"
|
||||
y="350"
|
||||
width="250"
|
||||
height="155"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect26" />
|
||||
<text
|
||||
x="1080"
|
||||
y="380"
|
||||
class="h2"
|
||||
id="text27">Authelia + LLDAP</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="405"
|
||||
class="txt"
|
||||
id="text28">• forward-auth : <tspan
|
||||
class="mono"
|
||||
id="tspan27">:9091</tspan></text>
|
||||
<text
|
||||
x="1080"
|
||||
y="427"
|
||||
class="txt"
|
||||
id="text29">• groupes via LDAP</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="449"
|
||||
class="txt"
|
||||
id="text30">• injecte headers Remote-*</text>
|
||||
<text
|
||||
x="1080"
|
||||
y="471"
|
||||
class="small"
|
||||
id="text31">non auth ⇒ 302 vers auth.*</text>
|
||||
<!-- Gitea issue -->
|
||||
<rect
|
||||
x="1330"
|
||||
y="175"
|
||||
width="220.47655"
|
||||
height="328.7897"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect31" />
|
||||
<text
|
||||
x="1350"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text32">Gitea (UI)</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text33">Issue préremplie :</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="255"
|
||||
class="mono"
|
||||
id="text34">BASE/OWNER/REPO/issues/new</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="287"
|
||||
class="txt"
|
||||
id="text35">Contenu typique :</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="309"
|
||||
class="txt"
|
||||
id="text36">• URL page + <tspan
|
||||
class="mono"
|
||||
id="tspan35">#p-…</tspan></text>
|
||||
<text
|
||||
x="1350"
|
||||
y="331"
|
||||
class="txt"
|
||||
id="text37">• Type / State / Category</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="353"
|
||||
class="txt"
|
||||
id="text38">• proposition / commentaire</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="385"
|
||||
class="txt"
|
||||
id="text39">Résultat :</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="407"
|
||||
class="txt"
|
||||
id="text40">• issue = backlog éditorial</text>
|
||||
<text
|
||||
x="1350"
|
||||
y="429"
|
||||
class="txt"
|
||||
id="text41">• labels (CI/bot) pour tri</text>
|
||||
<!-- Runtime connections -->
|
||||
<path
|
||||
d="M 440 260 L 470 260"
|
||||
class="line"
|
||||
id="path41" />
|
||||
<path
|
||||
d="M 1030 470 L 1060 470"
|
||||
class="dash"
|
||||
id="path42" />
|
||||
<path
|
||||
d="M 1310 320 L 1330 320"
|
||||
class="line"
|
||||
id="path43" />
|
||||
<path
|
||||
d="M 1030 270 L 1060 270"
|
||||
class="dash"
|
||||
id="path44" />
|
||||
<rect
|
||||
x="60"
|
||||
y="355"
|
||||
width="380"
|
||||
height="150"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect44" />
|
||||
<text
|
||||
x="80"
|
||||
y="385"
|
||||
class="h2"
|
||||
id="text44">Contrat runtime (robuste)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="410"
|
||||
class="txt"
|
||||
id="text45">• Citer marche sans droits (copie + lien)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="432"
|
||||
class="txt"
|
||||
id="text46">• Proposer n’existe pas si non “editors”</text>
|
||||
<text
|
||||
x="80"
|
||||
y="454"
|
||||
class="txt"
|
||||
id="text47">• whoami renvoie 302 login si non auth</text>
|
||||
<text
|
||||
x="80"
|
||||
y="476"
|
||||
class="txt"
|
||||
id="text48">• si PUBLIC_GITEA_* faux → 404/login loop</text>
|
||||
<!-- ZONE B: CI / labels -->
|
||||
<rect
|
||||
x="35"
|
||||
y="545"
|
||||
width="1530"
|
||||
height="210"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect48" />
|
||||
<text
|
||||
x="60"
|
||||
y="580"
|
||||
class="h2"
|
||||
id="text49">B — Automatisation : issue → labels + checks qualité</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="610"
|
||||
width="520"
|
||||
height="120"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect49" />
|
||||
<text
|
||||
x="80"
|
||||
y="640"
|
||||
class="h2"
|
||||
id="text50">Gitea Actions (workflows)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="665"
|
||||
class="txt"
|
||||
id="text51">• triggers : issues opened / edited (labels)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="687"
|
||||
class="txt"
|
||||
id="text52">• checks build : anchors / aliases / inline-js / dist audit</text>
|
||||
<text
|
||||
x="80"
|
||||
y="709"
|
||||
class="small"
|
||||
id="text53">token API requis côté job (ex : FORGE_TOKEN) pour écrire labels</text>
|
||||
<rect
|
||||
x="610"
|
||||
y="610"
|
||||
width="430"
|
||||
height="120"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect53" />
|
||||
<text
|
||||
x="630"
|
||||
y="640"
|
||||
class="h2"
|
||||
id="text54">Runner</text>
|
||||
<text
|
||||
x="630"
|
||||
y="665"
|
||||
class="txt"
|
||||
id="text55">• conteneur : <tspan
|
||||
class="mono"
|
||||
id="tspan54">gitea-act-runner (gitea/act_runner)</tspan></text>
|
||||
<text
|
||||
x="630"
|
||||
y="687"
|
||||
class="txt"
|
||||
id="text56">• exécute jobs (souvent en conteneur)</text>
|
||||
<text
|
||||
x="630"
|
||||
y="709"
|
||||
class="txt"
|
||||
id="text57">• appelle API Gitea pour labels</text>
|
||||
<rect
|
||||
x="1070"
|
||||
y="610"
|
||||
width="465"
|
||||
height="120"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect57" />
|
||||
<text
|
||||
x="1090"
|
||||
y="640"
|
||||
class="h2"
|
||||
id="text58">Gitea (API) + labels</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="665"
|
||||
class="txt"
|
||||
id="text59">• labels = tri natif (type/state/cat)</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="687"
|
||||
class="txt"
|
||||
id="text60">• backlog propre, opérable</text>
|
||||
<text
|
||||
x="1090"
|
||||
y="709"
|
||||
class="small"
|
||||
id="text61">si 401 : token manquant/mauvais droits</text>
|
||||
<path
|
||||
d="M 580 670 L 610 670"
|
||||
class="line"
|
||||
id="path61" />
|
||||
<path
|
||||
d="M 1040 670 L 1070 670"
|
||||
class="line"
|
||||
id="path62" />
|
||||
<!-- ZONE C: Re-integration + anchors -->
|
||||
<rect
|
||||
x="35"
|
||||
y="780"
|
||||
width="1530"
|
||||
height="210"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect62" />
|
||||
<text
|
||||
x="300"
|
||||
y="815"
|
||||
class="h2"
|
||||
id="text62"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">C — Réintégration : correction → contenu web + stabilité des ancres (aliases build-time)</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="850"
|
||||
width="520"
|
||||
height="115"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect63" />
|
||||
<text
|
||||
x="80"
|
||||
y="880"
|
||||
class="h2"
|
||||
id="text63">Apply-ticket</text>
|
||||
<text
|
||||
x="80"
|
||||
y="905"
|
||||
class="mono"
|
||||
id="text64">scripts/apply-ticket.mjs <issue_number> --alias</text>
|
||||
<text
|
||||
x="80"
|
||||
y="929"
|
||||
class="txt"
|
||||
id="text65">• applique patch dans src/content/…</text>
|
||||
<text
|
||||
x="80"
|
||||
y="951"
|
||||
class="txt"
|
||||
id="text66">• écrit alias old→new dans <tspan
|
||||
class="mono"
|
||||
id="tspan65">src/anchors/anchor-aliases.json</tspan></text>
|
||||
<rect
|
||||
x="610"
|
||||
y="850"
|
||||
width="518.78973"
|
||||
height="130.73373"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect66" />
|
||||
<text
|
||||
x="630"
|
||||
y="880"
|
||||
class="h2"
|
||||
id="text67">Aliases canon + injection</text>
|
||||
<text
|
||||
x="630"
|
||||
y="905"
|
||||
class="mono"
|
||||
id="text68">src/anchors/anchor-aliases.json</text>
|
||||
<text
|
||||
x="630"
|
||||
y="929"
|
||||
class="txt"
|
||||
id="text69">postbuild : <tspan
|
||||
class="mono"
|
||||
id="tspan68">node scripts/inject-anchor-aliases.mjs</tspan></text>
|
||||
<text
|
||||
x="630"
|
||||
y="951"
|
||||
class="txt"
|
||||
id="text70"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan77"
|
||||
x="630"
|
||||
y="951">• injecte</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan78"
|
||||
x="630"
|
||||
y="967.25"><span id="oldId" class="para-alias"> avant newId dans dist/**/index.html</tspan></text>
|
||||
<rect
|
||||
x="1160"
|
||||
y="850"
|
||||
width="375"
|
||||
height="115"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect70" />
|
||||
<text
|
||||
x="1180"
|
||||
y="880"
|
||||
class="h2"
|
||||
id="text71">Preuves (tests)</text>
|
||||
<text
|
||||
x="1180"
|
||||
y="905"
|
||||
class="txt"
|
||||
id="text72">• <tspan
|
||||
class="mono"
|
||||
id="tspan71">scripts/check-anchor-aliases.mjs</tspan></text>
|
||||
<text
|
||||
x="1180"
|
||||
y="929"
|
||||
class="txt"
|
||||
id="text73">• <tspan
|
||||
class="mono"
|
||||
id="tspan72">scripts/verify-anchor-aliases-in-dist.mjs</tspan></text>
|
||||
<text
|
||||
x="1180"
|
||||
y="951"
|
||||
class="txt"
|
||||
id="text74">• <tspan
|
||||
class="mono"
|
||||
id="tspan73">scripts/check-anchors.mjs</tspan></text>
|
||||
<path
|
||||
d="M 1495 545 L 1495 505"
|
||||
class="dash"
|
||||
id="path74" />
|
||||
<path
|
||||
d="M 220,730 V 850"
|
||||
class="line"
|
||||
id="path75" />
|
||||
<path
|
||||
d="M 580 908 L 610 908"
|
||||
class="line"
|
||||
id="path76" />
|
||||
<path
|
||||
d="M 1130 908 L 1160 908"
|
||||
class="line"
|
||||
id="path77" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
618
docs/diagrams/diagram.svg
Normal file
@@ -0,0 +1,618 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1600"
|
||||
height="980"
|
||||
viewBox="0 0 1600 980"
|
||||
version="1.1"
|
||||
id="svg66"
|
||||
sodipodi:docname="diagram.svg"
|
||||
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview66"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.82625"
|
||||
inkscape:cx="1002.118"
|
||||
inkscape:cy="617.85174"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="1022"
|
||||
inkscape:window-x="234"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg66" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<marker
|
||||
id="arrow"
|
||||
viewBox="0 0 10 10"
|
||||
refX="9.5"
|
||||
refY="5"
|
||||
markerWidth="8"
|
||||
markerHeight="8"
|
||||
orient="auto-start-reverse">
|
||||
<path
|
||||
d="M 0 0 L 10 5 L 0 10 z"
|
||||
fill="#222"
|
||||
id="path1" />
|
||||
</marker>
|
||||
<style
|
||||
id="style1">
|
||||
.title { font: 700 22px sans-serif; fill:#111; }
|
||||
.h2 { font: 700 16px sans-serif; fill:#111; }
|
||||
.txt { font: 13px sans-serif; fill:#111; }
|
||||
.small { font: 12px sans-serif; fill:#111; }
|
||||
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
|
||||
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
|
||||
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
|
||||
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
|
||||
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
|
||||
</style>
|
||||
</defs>
|
||||
<!-- Header -->
|
||||
<text
|
||||
x="40"
|
||||
y="45"
|
||||
class="title"
|
||||
id="text1">Archicratie – Web Edition : schéma global (Local Mac Studio vs NAS Synology DS220+)</text>
|
||||
<text
|
||||
x="40"
|
||||
y="75"
|
||||
class="small"
|
||||
id="text2">Lecture : (1) utilisateur → DSM → Traefik/Authelia → site ; (2) dev → package → slot green/blue → switch Traefik (provider file) ; (3) proposer → issues → runner → labels.</text>
|
||||
<!-- LOCAL ZONE -->
|
||||
<rect
|
||||
x="35"
|
||||
y="110"
|
||||
width="520"
|
||||
height="820"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect2" />
|
||||
<text
|
||||
x="60"
|
||||
y="145"
|
||||
class="h2"
|
||||
id="text3">LOCAL — Mac Studio (atelier de dev)</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="175"
|
||||
width="470"
|
||||
height="95"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect3" />
|
||||
<text
|
||||
x="80"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text4">Repo Astro (édition)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text5">• src/content/ (contenu web)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="250"
|
||||
class="txt"
|
||||
id="text6">• scripts tooling (anchors, import docx, apply-ticket)</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="295"
|
||||
width="470"
|
||||
height="80"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect6" />
|
||||
<text
|
||||
x="80"
|
||||
y="325"
|
||||
class="h2"
|
||||
id="text7">Build statique</text>
|
||||
<text
|
||||
x="80"
|
||||
y="350"
|
||||
class="txt"
|
||||
id="text8">npm run build → dist/ (site statique)</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="400"
|
||||
width="470"
|
||||
height="95"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect8" />
|
||||
<text
|
||||
x="80"
|
||||
y="430"
|
||||
class="h2"
|
||||
id="text9">Release pack</text>
|
||||
<text
|
||||
x="80"
|
||||
y="455"
|
||||
class="txt"
|
||||
id="text10">archive .tar.gz + .sha256</text>
|
||||
<text
|
||||
x="80"
|
||||
y="475"
|
||||
class="txt"
|
||||
id="text11">destination NAS : /volume2/docker/archicratie-web/incoming/</text>
|
||||
<rect
|
||||
x="60"
|
||||
y="520"
|
||||
width="470"
|
||||
height="120"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect11" />
|
||||
<text
|
||||
x="80"
|
||||
y="550"
|
||||
class="h2"
|
||||
id="text12">Centre de vérité</text>
|
||||
<text
|
||||
x="80"
|
||||
y="575"
|
||||
class="txt"
|
||||
id="text13">Tu commits/push vers Gitea (NAS) :</text>
|
||||
<text
|
||||
x="80"
|
||||
y="598"
|
||||
class="txt"
|
||||
id="text14">• code + docs + diagrammes (SVG)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="621"
|
||||
class="txt"
|
||||
id="text15">• issues = backlog éditorial</text>
|
||||
<!-- NAS ZONE -->
|
||||
<rect
|
||||
x="590"
|
||||
y="110"
|
||||
width="975"
|
||||
height="820"
|
||||
rx="18"
|
||||
class="zone"
|
||||
id="rect15" />
|
||||
<text
|
||||
x="615"
|
||||
y="145"
|
||||
class="h2"
|
||||
id="text16">DISTANT — NAS Synology DS220+ (DSM + Container Manager)</text>
|
||||
<!-- USERS -->
|
||||
<rect
|
||||
x="615"
|
||||
y="175"
|
||||
width="275"
|
||||
height="90"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect16" />
|
||||
<text
|
||||
x="635"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text17">Utilisateurs (web)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text18">• visiteurs</text>
|
||||
<text
|
||||
x="635"
|
||||
y="250"
|
||||
class="txt"
|
||||
id="text19">• éditeurs (accès protégé)</text>
|
||||
<!-- DSM Reverse Proxy -->
|
||||
<rect
|
||||
x="920"
|
||||
y="175"
|
||||
width="615"
|
||||
height="90"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect19" />
|
||||
<text
|
||||
x="945"
|
||||
y="205"
|
||||
class="h2"
|
||||
id="text20">DSM Reverse Proxy (HTTPS public → Traefik)</text>
|
||||
<text
|
||||
x="945"
|
||||
y="230"
|
||||
class="txt"
|
||||
id="text21">• pointe vers 127.0.0.1:18080 (Traefik edge)</text>
|
||||
<text
|
||||
x="945"
|
||||
y="252"
|
||||
class="txt"
|
||||
id="text22">• bascule/rollback = switch Traefik (reload) ; DSM reste stable</text>
|
||||
<!-- Edge Traefik -->
|
||||
<rect
|
||||
x="920"
|
||||
y="295"
|
||||
width="615"
|
||||
height="110"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect22" />
|
||||
<text
|
||||
x="945"
|
||||
y="325"
|
||||
class="h2"
|
||||
id="text23">Traefik (edge) — écoute 127.0.0.1:18080</text>
|
||||
<text
|
||||
x="945"
|
||||
y="350"
|
||||
class="txt"
|
||||
id="text24">• entrée unique derrière DSM</text>
|
||||
<text
|
||||
x="945"
|
||||
y="372"
|
||||
class="txt"
|
||||
id="text25">• middlewares : sanitize-remote + forward-auth Authelia</text>
|
||||
<text
|
||||
x="945"
|
||||
y="394"
|
||||
class="txt"
|
||||
id="text26">• un seul backend site actif (blue OU green) via 20-archicratie-backend.yml</text>
|
||||
<!-- Auth Stack -->
|
||||
<rect
|
||||
x="615"
|
||||
y="310"
|
||||
width="275"
|
||||
height="250"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect26" />
|
||||
<text
|
||||
x="635"
|
||||
y="340"
|
||||
class="h2"
|
||||
id="text27">Auth stack</text>
|
||||
<text
|
||||
x="635"
|
||||
y="365"
|
||||
class="txt"
|
||||
id="text28">Authelia</text>
|
||||
<text
|
||||
x="635"
|
||||
y="387"
|
||||
class="small"
|
||||
id="text29">• forward-auth : /api/authz/forward-auth</text>
|
||||
<text
|
||||
x="635"
|
||||
y="415"
|
||||
class="txt"
|
||||
id="text30">LLDAP</text>
|
||||
<text
|
||||
x="635"
|
||||
y="438"
|
||||
class="small"
|
||||
id="text31">• annuaire LDAP “source of truth”</text>
|
||||
<text
|
||||
x="635"
|
||||
y="466"
|
||||
class="txt"
|
||||
id="text32">Redis</text>
|
||||
<text
|
||||
x="635"
|
||||
y="489"
|
||||
class="small"
|
||||
id="text33">• sessions / cache (selon config)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="525"
|
||||
class="small"
|
||||
id="text34"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan68"
|
||||
x="635"
|
||||
y="525">Objectif : SSO/MFA + anti lock-out</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan69"
|
||||
x="635"
|
||||
y="540">déploiement progressif)</tspan></text>
|
||||
<!-- Web Blue/Green -->
|
||||
<rect
|
||||
x="920"
|
||||
y="435"
|
||||
width="300"
|
||||
height="135"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect34" />
|
||||
<text
|
||||
x="945"
|
||||
y="465"
|
||||
class="h2"
|
||||
id="text35">web_blue (slot A)</text>
|
||||
<text
|
||||
x="945"
|
||||
y="490"
|
||||
class="txt"
|
||||
id="text36">127.0.0.1:8081 → container:80</text>
|
||||
<text
|
||||
x="945"
|
||||
y="515"
|
||||
class="txt"
|
||||
id="text37">sert dist/ (Nginx/HTTP)</text>
|
||||
<text
|
||||
x="945"
|
||||
y="540"
|
||||
class="small"
|
||||
id="text38">ne jamais modifier si LIVE</text>
|
||||
<rect
|
||||
x="1235"
|
||||
y="435"
|
||||
width="300"
|
||||
height="135"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect38" />
|
||||
<text
|
||||
x="1260"
|
||||
y="465"
|
||||
class="h2"
|
||||
id="text39">web_green (slot B)</text>
|
||||
<text
|
||||
x="1260"
|
||||
y="490"
|
||||
class="txt"
|
||||
id="text40">127.0.0.1:8082 → container:80</text>
|
||||
<text
|
||||
x="1260"
|
||||
y="515"
|
||||
class="txt"
|
||||
id="text41">sert dist/ (Nginx/HTTP)</text>
|
||||
<text
|
||||
x="1260"
|
||||
y="540"
|
||||
class="small"
|
||||
id="text42">slot “next” (staging)</text>
|
||||
<!-- Switch script -->
|
||||
<rect
|
||||
x="847.41852"
|
||||
y="587.45636"
|
||||
width="691.17664"
|
||||
height="69.928497"
|
||||
rx="13.486373"
|
||||
class="note"
|
||||
id="rect42" />
|
||||
<text
|
||||
x="932.38275"
|
||||
y="617.42059"
|
||||
class="txt"
|
||||
id="text43"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan67"
|
||||
x="932.38275"
|
||||
y="617.42059">switch-archicratie.sh : bascule blue/green en réécrivant 20-archicratie-backend.yml</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="932.38275"
|
||||
y="633.67059"
|
||||
id="tspan3">puis reload Traefik (provider file)</tspan></text>
|
||||
<!-- Gitea -->
|
||||
<rect
|
||||
x="615"
|
||||
y="690"
|
||||
width="520"
|
||||
height="160"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect43" />
|
||||
<text
|
||||
x="635"
|
||||
y="720"
|
||||
class="h2"
|
||||
id="text44"
|
||||
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Gitea (forge web + API)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="745"
|
||||
class="txt"
|
||||
id="text45"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• repo = centre de vérité</text>
|
||||
<text
|
||||
x="635"
|
||||
y="768"
|
||||
class="txt"
|
||||
id="text46"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• issues = backlog</text>
|
||||
<text
|
||||
x="635"
|
||||
y="791"
|
||||
class="txt"
|
||||
id="text47"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• labels = tri natif (type/state/cat)</text>
|
||||
<text
|
||||
x="635"
|
||||
y="814"
|
||||
class="small"
|
||||
id="text48"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">Note : parfois laissé sans forward-auth (runner/accès API) selon réglage</text>
|
||||
<!-- Runner -->
|
||||
<rect
|
||||
x="1160"
|
||||
y="690"
|
||||
width="375"
|
||||
height="170"
|
||||
rx="12"
|
||||
class="box"
|
||||
id="rect48" />
|
||||
<text
|
||||
x="1185"
|
||||
y="720"
|
||||
class="h2"
|
||||
id="text49">Gitea Actions Runner (act_runner)</text>
|
||||
<text
|
||||
x="1185"
|
||||
y="745"
|
||||
class="txt"
|
||||
id="text50">• exécute les workflows</text>
|
||||
<text
|
||||
x="1185"
|
||||
y="768"
|
||||
class="txt"
|
||||
id="text51">• doit monter /var/run/docker.sock</text>
|
||||
<text
|
||||
x="1185"
|
||||
y="791"
|
||||
class="txt"
|
||||
id="text52">• jobs en conteneur (ex : python:3.12-slim)</text>
|
||||
<text
|
||||
x="1185"
|
||||
y="814"
|
||||
class="small"
|
||||
id="text53"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan78"
|
||||
x="1185"
|
||||
y="814">• applique labels via API avec PAT (FORGE_TOKEN)</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan79"
|
||||
x="1185"
|
||||
y="829">— sinon 401</tspan></text>
|
||||
<!-- Connections -->
|
||||
<!-- User -> DSM -->
|
||||
<path
|
||||
d="M 890 220 L 920 220"
|
||||
class="line"
|
||||
id="path53" />
|
||||
<!-- DSM -> Traefik -->
|
||||
<path
|
||||
d="M 1225 265 L 1225 295"
|
||||
class="line"
|
||||
id="path54" />
|
||||
<!-- Traefik -> Web (one active) -->
|
||||
<path
|
||||
d="M 1100 405 L 1070 435"
|
||||
class="dash"
|
||||
id="path55" />
|
||||
<path
|
||||
d="M 1355 405 L 1385 435"
|
||||
class="dash"
|
||||
id="path56" />
|
||||
<!-- Traefik -> Auth -->
|
||||
<path
|
||||
d="M 920 350 L 890 350"
|
||||
class="line"
|
||||
id="path57" />
|
||||
<!-- DSM -> Gitea (via router / host rules) -->
|
||||
<path
|
||||
d="M 917.44325,255.3177 883.05597,688.03328"
|
||||
class="dash"
|
||||
id="path58"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<!-- Gitea -> Runner -->
|
||||
<path
|
||||
d="M 1135 710 L 1160 750"
|
||||
class="line"
|
||||
id="path59" />
|
||||
<!-- Runner -> Gitea API -->
|
||||
<path
|
||||
d="M 1160 805 L 1135 740"
|
||||
class="dash"
|
||||
id="path60" />
|
||||
<!-- Local -> NAS incoming -->
|
||||
<path
|
||||
d="M 530 448 L 615 448"
|
||||
class="line"
|
||||
id="path61" />
|
||||
<!-- Legend -->
|
||||
<rect
|
||||
x="60"
|
||||
y="670"
|
||||
width="470"
|
||||
height="245"
|
||||
rx="12"
|
||||
class="note"
|
||||
id="rect61" />
|
||||
<text
|
||||
x="80"
|
||||
y="700"
|
||||
class="h2"
|
||||
id="text61">Légende / invariants (ce qui casse “pour de vrai”)</text>
|
||||
<text
|
||||
x="80"
|
||||
y="725"
|
||||
class="txt"
|
||||
id="text62"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan70"
|
||||
x="80"
|
||||
y="725">• Prod safe : on ne touche jamais au slot LIVE ;</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan71"
|
||||
x="80"
|
||||
y="741.25">build/test sur l’autre ; switch Traefik ; DSM ne change pas.</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="768"
|
||||
class="txt"
|
||||
id="text63"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan76"
|
||||
x="80"
|
||||
y="768">• Runner : sans docker.sock → aucun job ;</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan77"
|
||||
x="80"
|
||||
y="784.40051">sans FORGE_TOKEN → 401 (labels).</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="811"
|
||||
class="txt"
|
||||
id="text64"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan74"
|
||||
x="80"
|
||||
y="811">• Edge : Traefik :18080 derrière DSM ; sanitize-remote</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan75"
|
||||
x="80"
|
||||
y="827.25">+ forward-auth Authelia.</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="854"
|
||||
class="txt"
|
||||
id="text65"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan1"
|
||||
x="80"
|
||||
y="854">• Blue/green : 8081/8082 ; Traefik décide le LIVE</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2"
|
||||
x="80"
|
||||
y="870.25">(DSM pointe toujours sur :18080).</tspan></text>
|
||||
<text
|
||||
x="80"
|
||||
y="891"
|
||||
class="small"
|
||||
id="text66"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan72"
|
||||
x="80"
|
||||
y="891">Astuce : exporte en PNG/PDF pour lecture “grand public”,</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan73"
|
||||
x="80"
|
||||
y="906">garde SVG comme source éditable.</tspan></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/diagrams/out/archicratie-web-edition-git-ci-workflow-v1.png
Normal file
|
After Width: | Height: | Size: 395 KiB |
BIN
docs/diagrams/out/archicratie-web-edition-global-verbatim-v2.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 360 KiB |
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
@@ -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
@@ -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.
|
||||
691
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"astro": "^5.16.11"
|
||||
"astro": "^5.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
@@ -1247,9 +1247,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
|
||||
"integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1260,9 +1260,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1273,9 +1273,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1286,9 +1286,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1299,9 +1299,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1312,9 +1312,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1325,9 +1325,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
|
||||
"integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1338,9 +1338,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
|
||||
"integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1351,9 +1351,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1364,9 +1364,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1377,9 +1377,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1390,9 +1390,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1403,9 +1403,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1416,9 +1416,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1429,9 +1429,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1442,9 +1442,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1455,9 +1455,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1468,9 +1468,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1481,9 +1481,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1494,9 +1494,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1507,9 +1507,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1520,9 +1520,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1533,9 +1533,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1546,9 +1546,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1559,9 +1559,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1905,9 +1905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/astro": {
|
||||
"version": "5.16.11",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.16.11.tgz",
|
||||
"integrity": "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA==",
|
||||
"version": "5.18.0",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.18.0.tgz",
|
||||
"integrity": "sha512-CHiohwJIS4L0G6/IzE1Fx3dgWqXBCXus/od0eGUfxrZJD2um2pE7ehclMmgL/fXqbU7NfE1Ze2pq34h2QaA6iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^2.13.0",
|
||||
@@ -1933,7 +1933,7 @@
|
||||
"dlv": "^1.1.3",
|
||||
"dset": "^3.1.4",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"estree-walker": "^3.0.3",
|
||||
"flattie": "^1.1.1",
|
||||
"fontace": "~0.4.0",
|
||||
@@ -1954,16 +1954,16 @@
|
||||
"prompts": "^2.4.2",
|
||||
"rehype": "^13.0.2",
|
||||
"semver": "^7.7.3",
|
||||
"shiki": "^3.20.0",
|
||||
"shiki": "^3.21.0",
|
||||
"smol-toml": "^1.6.0",
|
||||
"svgo": "^4.0.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tsconfck": "^3.1.6",
|
||||
"ultrahtml": "^1.6.0",
|
||||
"unifont": "~0.7.1",
|
||||
"unifont": "~0.7.3",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"unstorage": "^1.17.3",
|
||||
"unstorage": "^1.17.4",
|
||||
"vfile": "^6.0.3",
|
||||
"vite": "^6.4.1",
|
||||
"vitefu": "^1.1.1",
|
||||
@@ -1990,6 +1990,463 @@
|
||||
"sharp": "^0.34.0"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -2426,9 +2883,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
@@ -5233,9 +5690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -5248,31 +5705,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.55.1",
|
||||
"@rollup/rollup-android-arm64": "4.55.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.55.1",
|
||||
"@rollup/rollup-darwin-x64": "4.55.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.55.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.55.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.55.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.55.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.55.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.55.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.55.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.55.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.55.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.55.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -5681,9 +6138,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.7",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
|
||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
19
package.json
@@ -4,32 +4,29 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev": "node scripts/write-dev-whoami.mjs && astro dev",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
|
||||
"clean": "rm -rf dist",
|
||||
"build": "astro build",
|
||||
"build:clean": "npm run clean && npm run build",
|
||||
|
||||
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && npx pagefind --site dist",
|
||||
|
||||
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && node scripts/build-para-index.mjs && node scripts/build-annotations-index.mjs && node scripts/purge-dist-dev-whoami.mjs && npx pagefind --site dist",
|
||||
"import": "node scripts/import-docx.mjs",
|
||||
"apply:ticket": "node scripts/apply-ticket.mjs",
|
||||
|
||||
"audit:dist": "node scripts/audit-dist.mjs",
|
||||
|
||||
"build:para-index": "node scripts/build-para-index.mjs",
|
||||
"build:annotations-index": "node scripts/build-annotations-index.mjs",
|
||||
"test:aliases": "node scripts/check-anchor-aliases.mjs",
|
||||
"test:anchors": "node scripts/check-anchors.mjs",
|
||||
"test:anchors:update": "node scripts/check-anchors.mjs --update",
|
||||
|
||||
"test": "npm run test:aliases && npm run build:clean && npm run audit:dist && node scripts/verify-anchor-aliases-in-dist.mjs && npm run test:anchors && node scripts/check-inline-js.mjs",
|
||||
|
||||
"test:annotations": "node scripts/check-annotations.mjs",
|
||||
"test:annotations:media": "node scripts/check-annotations-media.mjs",
|
||||
"test": "npm run test:aliases && npm run build:clean && npm run audit:dist && node scripts/verify-anchor-aliases-in-dist.mjs && npm run test:anchors && npm run test:annotations && npm run test:annotations:media && node scripts/check-inline-js.mjs",
|
||||
"ci": "CI=1 npm test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"astro": "^5.16.11"
|
||||
"astro": "^5.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
|
||||
0
public/media/.gitkeep
Normal file
@@ -1 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone","orientation":"any"}
|
||||
899
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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
148
scripts/build-para-index.mjs
Normal file
@@ -0,0 +1,148 @@
|
||||
// scripts/build-para-index.mjs
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { inDir: "dist", outFile: "dist/para-index.json" };
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
|
||||
if (a === "--in" && argv[i + 1]) {
|
||||
out.inDir = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (a.startsWith("--in=")) {
|
||||
out.inDir = a.slice("--in=".length);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (a === "--out" && argv[i + 1]) {
|
||||
out.outFile = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (a.startsWith("--out=")) {
|
||||
out.outFile = a.slice("--out=".length);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async function exists(p) {
|
||||
try {
|
||||
await fs.access(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function walk(dir) {
|
||||
const out = [];
|
||||
const ents = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const e of ents) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...(await walk(p)));
|
||||
else out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function stripTags(html) {
|
||||
return String(html || "")
|
||||
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
|
||||
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
|
||||
.replace(/<[^>]+>/g, " ");
|
||||
}
|
||||
|
||||
function decodeEntities(s) {
|
||||
// minimal, volontairement (évite dépendances)
|
||||
return String(s || "")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function normalizeSpaces(s) {
|
||||
return decodeEntities(s).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function relPageFromIndexHtml(inDirAbs, fileAbs) {
|
||||
const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/");
|
||||
if (!/index\.html$/i.test(rel)) return null;
|
||||
|
||||
// dist/<page>/index.html -> "/<page>/"
|
||||
const page = "/" + rel.replace(/index\.html$/i, "");
|
||||
return page;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { inDir, outFile } = parseArgs(process.argv.slice(2));
|
||||
const CWD = process.cwd();
|
||||
|
||||
const inDirAbs = path.isAbsolute(inDir) ? inDir : path.join(CWD, inDir);
|
||||
const outAbs = path.isAbsolute(outFile) ? outFile : path.join(CWD, outFile);
|
||||
|
||||
// ✅ antifragile: si dist/ (ou inDir) absent -> on SKIP proprement
|
||||
if (!(await exists(inDirAbs))) {
|
||||
console.log(`ℹ️ para-index: skip (input missing): ${inDir}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const files = (await walk(inDirAbs)).filter((p) => /index\.html$/i.test(p));
|
||||
|
||||
if (!files.length) {
|
||||
console.log(`ℹ️ para-index: skip (no index.html found in): ${inDir}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const items = [];
|
||||
const byId = Object.create(null);
|
||||
|
||||
// <p ... id="p-...">...</p>
|
||||
// (regex volontairement stricte sur l'id pour éviter faux positifs)
|
||||
const reP = /<p\b([^>]*\bid\s*=\s*["'](p-\d+-[^"']+)["'][^>]*)>([\s\S]*?)<\/p>/gi;
|
||||
|
||||
for (const f of files) {
|
||||
const page = relPageFromIndexHtml(inDirAbs, f);
|
||||
if (!page) continue;
|
||||
|
||||
const html = await fs.readFile(f, "utf8");
|
||||
|
||||
let m;
|
||||
while ((m = reP.exec(html))) {
|
||||
const id = m[2];
|
||||
const inner = m[3];
|
||||
|
||||
if (byId[id] != null) continue; // protège si jamais doublons
|
||||
|
||||
const text = normalizeSpaces(stripTags(inner));
|
||||
if (!text) continue;
|
||||
|
||||
byId[id] = items.length;
|
||||
items.push({ id, page, text });
|
||||
}
|
||||
}
|
||||
|
||||
const out = {
|
||||
schema: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
items,
|
||||
byId,
|
||||
};
|
||||
|
||||
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
||||
await fs.writeFile(outAbs, JSON.stringify(out), "utf8");
|
||||
|
||||
console.log(`✅ para-index: items=${items.length} -> ${path.relative(CWD, outAbs)}`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("FAIL: build-para-index crashed:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
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);
|
||||
});
|
||||
224
scripts/check-annotations.mjs
Normal file
@@ -0,0 +1,224 @@
|
||||
// scripts/check-annotations.mjs
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import YAML from "yaml";
|
||||
|
||||
const CWD = process.cwd();
|
||||
const ANNO_DIR = path.join(CWD, "src", "annotations");
|
||||
const DIST_DIR = path.join(CWD, "dist");
|
||||
const ALIASES_PATH = path.join(CWD, "src", "anchors", "anchor-aliases.json");
|
||||
|
||||
async function exists(p) {
|
||||
try { await fs.access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
async function walk(dir) {
|
||||
const out = [];
|
||||
const ents = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const e of ents) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...(await walk(p)));
|
||||
else out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function escRe(s) {
|
||||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function normalizePageKey(s) {
|
||||
return String(s || "").replace(/^\/+/, "").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function isPlainObject(x) {
|
||||
return !!x && typeof x === "object" && !Array.isArray(x);
|
||||
}
|
||||
|
||||
function isParaId(s) {
|
||||
return /^p-\d+-/i.test(String(s || ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Supporte:
|
||||
* - monolith: src/annotations/<pageKey>.yml -> pageKey = rel sans ext
|
||||
* - shard : src/annotations/<pageKey>/<paraId>.yml -> pageKey = dirname(rel), paraId = basename
|
||||
*
|
||||
* shard seulement si le fichier est dans un sous-dossier (anti cas pathologique).
|
||||
*/
|
||||
function inferFromFile(fileAbs) {
|
||||
const rel = path.relative(ANNO_DIR, fileAbs).replace(/\\/g, "/");
|
||||
const relNoExt = rel.replace(/\.(ya?ml|json)$/i, "");
|
||||
const parts = relNoExt.split("/").filter(Boolean);
|
||||
const base = parts[parts.length - 1] || "";
|
||||
const dirParts = parts.slice(0, -1);
|
||||
|
||||
const isShard = dirParts.length > 0 && isParaId(base);
|
||||
const pageKey = isShard ? dirParts.join("/") : relNoExt;
|
||||
const paraId = isShard ? base : "";
|
||||
|
||||
return { pageKey: normalizePageKey(pageKey), paraId };
|
||||
}
|
||||
|
||||
async function loadAliases() {
|
||||
if (!(await exists(ALIASES_PATH))) return {};
|
||||
try {
|
||||
const raw = await fs.readFile(ALIASES_PATH, "utf8");
|
||||
const json = JSON.parse(raw);
|
||||
return isPlainObject(json) ? json : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function parseDoc(raw, fileAbs) {
|
||||
if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
|
||||
return YAML.parse(raw);
|
||||
}
|
||||
|
||||
function getAlias(aliases, pageKey, oldId) {
|
||||
// supporte:
|
||||
// 1) { "<pageKey>": { "<old>": "<new>" } }
|
||||
// 2) { "<old>": "<new>" }
|
||||
const k1 = String(pageKey || "");
|
||||
const k2 = k1 ? ("/" + k1.replace(/^\/+|\/+$/g, "") + "/") : "";
|
||||
const a1 = (aliases?.[k1]?.[oldId]) || (k2 ? aliases?.[k2]?.[oldId] : "");
|
||||
if (a1) return String(a1);
|
||||
const a2 = aliases?.[oldId];
|
||||
if (a2) return String(a2);
|
||||
return "";
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!(await exists(ANNO_DIR))) {
|
||||
console.log("✅ annotations: aucun dossier src/annotations — rien à vérifier.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!(await exists(DIST_DIR))) {
|
||||
console.error("FAIL: dist/ absent. Lance d’abord `npm run build` (ou `npm test`).");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const aliases = await loadAliases();
|
||||
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
|
||||
|
||||
// perf: cache HTML par page (shards = beaucoup de fichiers pour 1 page)
|
||||
const htmlCache = new Map(); // pageKey -> html
|
||||
const missingDistPage = new Set(); // pageKey
|
||||
|
||||
let pagesSeen = new Set();
|
||||
let checked = 0;
|
||||
let failures = 0;
|
||||
const notes = [];
|
||||
|
||||
for (const f of files) {
|
||||
const rel = path.relative(CWD, f).replace(/\\/g, "/");
|
||||
const raw = await fs.readFile(f, "utf8");
|
||||
|
||||
let doc;
|
||||
try {
|
||||
doc = parseDoc(raw, f);
|
||||
} catch (e) {
|
||||
failures++;
|
||||
notes.push(`- PARSE FAIL: ${rel} (${String(e?.message ?? e)})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isPlainObject(doc) || doc.schema !== 1) {
|
||||
failures++;
|
||||
notes.push(`- INVALID: ${rel} (schema must be 1)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { pageKey, paraId: shardParaId } = inferFromFile(f);
|
||||
|
||||
if (doc.page != null && normalizePageKey(doc.page) !== pageKey) {
|
||||
failures++;
|
||||
notes.push(`- PAGE MISMATCH: ${rel} (page="${doc.page}" != path="${pageKey}")`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isPlainObject(doc.paras)) {
|
||||
failures++;
|
||||
notes.push(`- INVALID: ${rel} (missing object key "paras")`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// shard invariant (fort) : doit contenir paras[paraId]
|
||||
if (shardParaId) {
|
||||
if (!Object.prototype.hasOwnProperty.call(doc.paras, shardParaId)) {
|
||||
failures++;
|
||||
notes.push(`- SHARD MISMATCH: ${rel} (expected paras["${shardParaId}"] present)`);
|
||||
continue;
|
||||
}
|
||||
// si extras -> warning (non destructif)
|
||||
const keys = Object.keys(doc.paras);
|
||||
if (!(keys.length === 1 && keys[0] === shardParaId)) {
|
||||
notes.push(`- WARN shard has extra paras: ${rel} (expected only "${shardParaId}", got ${keys.join(", ")})`);
|
||||
}
|
||||
}
|
||||
|
||||
pagesSeen.add(pageKey);
|
||||
|
||||
const distFile = path.join(DIST_DIR, pageKey, "index.html");
|
||||
if (!(await exists(distFile))) {
|
||||
if (!missingDistPage.has(pageKey)) {
|
||||
missingDistPage.add(pageKey);
|
||||
failures++;
|
||||
notes.push(`- MISSING PAGE: dist/${pageKey}/index.html (from ${rel})`);
|
||||
} else {
|
||||
notes.push(`- WARN missing page already reported: dist/${pageKey}/index.html (from ${rel})`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let html = htmlCache.get(pageKey);
|
||||
if (!html) {
|
||||
html = await fs.readFile(distFile, "utf8");
|
||||
htmlCache.set(pageKey, html);
|
||||
}
|
||||
|
||||
for (const paraId of Object.keys(doc.paras)) {
|
||||
checked++;
|
||||
|
||||
if (!isParaId(paraId)) {
|
||||
failures++;
|
||||
notes.push(`- INVALID ID: ${rel} (${paraId})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const re = new RegExp(`\\bid=["']${escRe(paraId)}["']`, "g");
|
||||
if (re.test(html)) continue;
|
||||
|
||||
const alias = getAlias(aliases, pageKey, paraId);
|
||||
if (alias) {
|
||||
const re2 = new RegExp(`\\bid=["']${escRe(alias)}["']`, "g");
|
||||
if (re2.test(html)) {
|
||||
notes.push(`- WARN alias used: ${pageKey} ${paraId} -> ${alias}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
failures++;
|
||||
notes.push(`- MISSING ID: ${pageKey} (#${paraId})`);
|
||||
}
|
||||
}
|
||||
|
||||
const warns = notes.filter((x) => x.startsWith("- WARN"));
|
||||
const pages = pagesSeen.size;
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(`FAIL: annotations invalid (pages=${pages} checked=${checked} failures=${failures})`);
|
||||
for (const n of notes) console.error(n);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const w of warns) console.log(w);
|
||||
console.log(`✅ annotations OK: pages=${pages} checked=${checked} warnings=${warns.length}`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("FAIL: annotations check crashed:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -114,7 +114,6 @@ async function runMammoth(docxPath, assetsOutDirWebRoot) {
|
||||
);
|
||||
|
||||
let html = result.value || "";
|
||||
|
||||
// Mammoth gives relative src="image-xx.png" ; we will prefix later
|
||||
return html;
|
||||
}
|
||||
@@ -182,6 +181,25 @@ async function exists(p) {
|
||||
try { await fs.access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ compat:
|
||||
* - ancien : collection="archicratie" + slug="archicrat-ia/chapitre-3"
|
||||
* - nouveau : collection="archicrat-ia" + slug="chapitre-3"
|
||||
*
|
||||
* But : toujours écrire dans src/content/archicrat-ia/<slugSansPrefix>.mdx
|
||||
*/
|
||||
function normalizeDest(collection, slug) {
|
||||
let outCollection = String(collection || "").trim();
|
||||
let outSlug = String(slug || "").trim().replace(/^\/+|\/+$/g, "");
|
||||
|
||||
if (outCollection === "archicratie" && outSlug.startsWith("archicrat-ia/")) {
|
||||
outCollection = "archicrat-ia";
|
||||
outSlug = outSlug.replace(/^archicrat-ia\//, "");
|
||||
}
|
||||
|
||||
return { outCollection, outSlug };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const manifestPath = path.resolve(args.manifest);
|
||||
@@ -203,11 +221,14 @@ async function main() {
|
||||
|
||||
for (const it of selected) {
|
||||
const docxPath = path.resolve(it.source);
|
||||
const outFile = path.resolve("src/content", it.collection, `${it.slug}.mdx`);
|
||||
|
||||
const { outCollection, outSlug } = normalizeDest(it.collection, it.slug);
|
||||
|
||||
const outFile = path.resolve("src/content", outCollection, `${outSlug}.mdx`);
|
||||
const outDir = path.dirname(outFile);
|
||||
|
||||
const assetsPublicDir = path.posix.join("/imported", it.collection, it.slug);
|
||||
const assetsDiskDir = path.resolve("public", "imported", it.collection, it.slug);
|
||||
const assetsPublicDir = path.posix.join("/imported", outCollection, outSlug);
|
||||
const assetsDiskDir = path.resolve("public", "imported", outCollection, outSlug);
|
||||
|
||||
if (!(await exists(docxPath))) {
|
||||
throw new Error(`Missing source docx: ${docxPath}`);
|
||||
@@ -241,18 +262,20 @@ async function main() {
|
||||
html = rewriteLocalImageLinks(html, assetsPublicDir);
|
||||
body = html.trim() ? html : "<p>(Import vide)</p>";
|
||||
}
|
||||
|
||||
|
||||
const defaultVersion = process.env.PUBLIC_RELEASE || "0.1.0";
|
||||
|
||||
// ✅ IMPORTANT: archicrat-ia partage edition/status avec archicratie (pas de migration frontmatter)
|
||||
const schemaDefaultsByCollection = {
|
||||
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
|
||||
ia: { edition: "ia", status: "cas_pratique", level: 1 },
|
||||
traite: { edition: "traite", status: "ontodynamique", level: 1 },
|
||||
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
|
||||
atlas: { edition: "atlas", status: "atlas", level: 1 },
|
||||
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
|
||||
"archicrat-ia": { edition: "archicrat-ia", status: "essai_these", level: 1 },
|
||||
"cas-ia": { edition: "cas-ia", status: "application", level: 1 },
|
||||
traite: { edition: "traite", status: "ontodynamique", level: 1 },
|
||||
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
|
||||
atlas: { edition: "atlas", status: "atlas", level: 1 },
|
||||
};
|
||||
|
||||
const defaults = schemaDefaultsByCollection[it.collection] || { edition: it.collection, status: "draft", level: 1 };
|
||||
const defaults = schemaDefaultsByCollection[outCollection] || { edition: outCollection, status: "draft", level: 1 };
|
||||
|
||||
const fm = [
|
||||
"---",
|
||||
@@ -282,4 +305,4 @@ async function main() {
|
||||
main().catch((e) => {
|
||||
console.error("\nERROR:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
@@ -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++;
|
||||
|
||||
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);
|
||||
});
|
||||
101
scripts/seed-gitea-labels.mjs
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* seed-gitea-labels — crée les labels attendus (idempotent)
|
||||
*
|
||||
* Usage:
|
||||
* FORGE_TOKEN=... FORGE_API=http://192.168.1.20:3000 node scripts/seed-gitea-labels.mjs
|
||||
* (ou FORGE_BASE=https://gitea... si pas de FORGE_API)
|
||||
*
|
||||
* Optionnel:
|
||||
* GITEA_OWNER / GITEA_REPO (sinon auto-détecté via git remote origin)
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
function getEnv(name, fallback = "") {
|
||||
return (process.env[name] ?? fallback).trim();
|
||||
}
|
||||
|
||||
function inferOwnerRepoFromGit() {
|
||||
const r = spawnSync("git", ["remote", "get-url", "origin"], { encoding: "utf-8" });
|
||||
if (r.status !== 0) return null;
|
||||
const u = (r.stdout || "").trim();
|
||||
const m = u.match(/[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/);
|
||||
if (!m?.groups) return null;
|
||||
return { owner: m.groups.owner, repo: m.groups.repo };
|
||||
}
|
||||
|
||||
async function apiReq(base, token, method, path, payload = null) {
|
||||
const url = `${base.replace(/\/+$/, "")}/api/v1${path}`;
|
||||
const headers = {
|
||||
Authorization: `token ${token}`,
|
||||
Accept: "application/json",
|
||||
"User-Agent": "archicratie-seed-labels/1.0",
|
||||
};
|
||||
const init = { method, headers };
|
||||
|
||||
if (payload != null) {
|
||||
init.headers["Content-Type"] = "application/json";
|
||||
init.body = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
const res = await fetch(url, init);
|
||||
const text = await res.text().catch(() => "");
|
||||
let json = null;
|
||||
try { json = text ? JSON.parse(text) : null; } catch {}
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status} ${method} ${url}\n${text}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const token = getEnv("FORGE_TOKEN");
|
||||
if (!token) throw new Error("FORGE_TOKEN manquant");
|
||||
|
||||
const inferred = inferOwnerRepoFromGit() || {};
|
||||
const owner = getEnv("GITEA_OWNER", inferred.owner || "");
|
||||
const repo = getEnv("GITEA_REPO", inferred.repo || "");
|
||||
if (!owner || !repo) throw new Error("Impossible de déterminer owner/repo (GITEA_OWNER/GITEA_REPO ou git remote)");
|
||||
|
||||
const base = getEnv("FORGE_API") || getEnv("FORGE_BASE");
|
||||
if (!base) throw new Error("FORGE_API ou FORGE_BASE manquant");
|
||||
|
||||
const wanted = [
|
||||
// type/*
|
||||
{ name: "type/comment", color: "1d76db", description: "Commentaire éditorial (site)" },
|
||||
{ name: "type/media", color: "1d76db", description: "Media à intégrer (image/audio/video)" },
|
||||
{ name: "type/correction", color: "1d76db", description: "Correction proposée" },
|
||||
{ name: "type/fact-check", color: "1d76db", description: "Vérification / sourçage" },
|
||||
|
||||
// state/*
|
||||
{ name: "state/a-trier", color: "0e8a16", description: "À trier" },
|
||||
{ name: "state/recevable", color: "0e8a16", description: "Recevable" },
|
||||
{ name: "state/a-sourcer", color: "0e8a16", description: "À sourcer" },
|
||||
|
||||
// scope/*
|
||||
{ name: "scope/readers", color: "5319e7", description: "Signalé par lecteur" },
|
||||
{ name: "scope/editors", color: "5319e7", description: "Signalé par éditeur" },
|
||||
];
|
||||
|
||||
const labels = (await apiReq(base, token, "GET", `/repos/${owner}/${repo}/labels?limit=1000`)) || [];
|
||||
const existing = new Set(labels.map((x) => x?.name).filter(Boolean));
|
||||
|
||||
let created = 0;
|
||||
for (const L of wanted) {
|
||||
if (existing.has(L.name)) continue;
|
||||
await apiReq(base, token, "POST", `/repos/${owner}/${repo}/labels`, {
|
||||
name: L.name,
|
||||
color: L.color,
|
||||
description: L.description,
|
||||
});
|
||||
created++;
|
||||
console.log("✅ created:", L.name);
|
||||
}
|
||||
|
||||
if (created === 0) console.log("ℹ️ seed: nothing to do (all labels already exist)");
|
||||
else console.log(`✅ seed done: created=${created}`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("💥 seed-gitea-labels:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
131
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
@@ -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})`);
|
||||
BIN
sources/docx/commencer/document-de-presentation.docx
Normal file
60
sources/manifest-cas-ia.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
items:
|
||||
- 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: 10
|
||||
|
||||
- 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: 20
|
||||
|
||||
- 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: 30
|
||||
|
||||
- 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: 40
|
||||
|
||||
- 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: 50
|
||||
|
||||
- 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: 60
|
||||
|
||||
- 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: 70
|
||||
|
||||
- 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: 80
|
||||
|
||||
- source: sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Conclusion.docx
|
||||
collection: cas-ia
|
||||
slug: conclusion
|
||||
title: "Conclusion"
|
||||
order: 90
|
||||
|
||||
- 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: 100
|
||||
6
sources/manifest-commencer.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
items:
|
||||
- source: sources/docx/commencer/document-de-presentation.docx
|
||||
collection: commencer
|
||||
slug: document-de-presentation
|
||||
title: Document de présentation
|
||||
order: 0
|
||||
@@ -1,2 +1,8 @@
|
||||
{}
|
||||
|
||||
{
|
||||
"/archicrat-ia/chapitre-3/": {
|
||||
"p-1-60c7ea48": "p-1-a21087b0"
|
||||
},
|
||||
"/cas-ia/introduction/": {
|
||||
"p-10-ceba29a2": "p-10-93d1eda0"
|
||||
}
|
||||
}
|
||||
|
||||
0
src/annotations/.gitkeep
Normal file
@@ -1,18 +1,22 @@
|
||||
---
|
||||
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/"))
|
||||
const entries = (await getCollection(collection))
|
||||
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
|
||||
|
||||
const href = (slug) => `/archicratie/${slug}/`;
|
||||
const href = (slug) => `${basePath}/${slug}/`;
|
||||
---
|
||||
|
||||
<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">
|
||||
@@ -66,7 +70,6 @@ const href = (slug) => `/archicratie/${slug}/`;
|
||||
opacity: .88;
|
||||
}
|
||||
|
||||
/* On garde <ol> mais on neutralise tout marker/numéro */
|
||||
.toc-global__list{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
@@ -148,7 +151,6 @@ const href = (slug) => `/archicratie/${slug}/`;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.toc-global{ background: rgba(255,255,255,0.04); }
|
||||
.toc-link:hover{ background: rgba(255,255,255,0.06); }
|
||||
@@ -162,4 +164,4 @@ const href = (slug) => `/archicratie/${slug}/`;
|
||||
const active = document.querySelector(".toc-global .toc-item.is-active");
|
||||
if (active) active.scrollIntoView({ block: "nearest" });
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
@@ -1,35 +1,128 @@
|
||||
---
|
||||
// src/components/LevelToggle.astro
|
||||
const { initialLevel = 1 } = Astro.props;
|
||||
---
|
||||
<div class="level-toggle" role="group" aria-label="Niveau de lecture">
|
||||
<button type="button" class="lvl-btn" data-level="1" aria-pressed="true">Niveau 1</button>
|
||||
<button type="button" class="lvl-btn" data-level="2" aria-pressed="false">Niveau 2</button>
|
||||
<button type="button" class="lvl-btn" data-level="3" aria-pressed="false">Niveau 3</button>
|
||||
|
||||
<div class="level-toggle" role="group" aria-label="Mode d’édition">
|
||||
<button type="button" class="level-btn" data-level="1">Propos</button>
|
||||
<button type="button" class="level-btn" data-level="2">Références</button>
|
||||
<button type="button" class="level-btn" data-level="3">Illustrations</button>
|
||||
<button type="button" class="level-btn" data-level="4">Commentaires</button>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline define:vars={{ initialLevel }}>
|
||||
(() => {
|
||||
const KEY = "archicratie.readingLevel";
|
||||
const buttons = Array.from(document.querySelectorAll(".lvl-btn"));
|
||||
const BODY = document.body;
|
||||
|
||||
function apply(level) {
|
||||
document.body.setAttribute("data-reading-level", String(level));
|
||||
buttons.forEach((b) => b.setAttribute("aria-pressed", b.dataset.level === String(level) ? "true" : "false"));
|
||||
const wrap = document.querySelector(".level-toggle");
|
||||
if (!wrap) return;
|
||||
|
||||
const buttons = Array.from(wrap.querySelectorAll("button[data-level]"));
|
||||
if (!buttons.length) return;
|
||||
|
||||
const KEY = "archicratie:readingLevel";
|
||||
|
||||
function clampLevel(n) {
|
||||
const x = Number.parseInt(String(n), 10);
|
||||
if (!Number.isFinite(x)) return 1;
|
||||
return Math.min(4, Math.max(1, x));
|
||||
}
|
||||
|
||||
// Valeur par défaut : si rien n'est stocké, on met 1 (citoyen).
|
||||
// Si JS est absent/casse, le site reste lisible (tout s'affiche).
|
||||
const stored = Number(localStorage.getItem(KEY));
|
||||
const level = (stored === 1 || stored === 2 || stored === 3) ? stored : 1;
|
||||
function setActiveUI(lvl) {
|
||||
for (const b of buttons) {
|
||||
const on = String(b.dataset.level) === String(lvl);
|
||||
b.classList.toggle("is-active", on);
|
||||
b.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
apply(level);
|
||||
function captureBeforeLevelSwitch() {
|
||||
const paraId =
|
||||
window.__archiCurrentParaId ||
|
||||
window.__archiLastParaId ||
|
||||
String(location.hash || "").replace(/^#/, "") ||
|
||||
"";
|
||||
|
||||
buttons.forEach((b) => {
|
||||
b.addEventListener("click", () => {
|
||||
const lvl = Number(b.dataset.level);
|
||||
localStorage.setItem(KEY, String(lvl));
|
||||
apply(lvl);
|
||||
});
|
||||
window.__archiLevelSwitchCtx = {
|
||||
paraId,
|
||||
hash: location.hash || "",
|
||||
scrollY: window.scrollY || 0,
|
||||
t: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function applyLevel(lvl, { persist = true } = {}) {
|
||||
const v = clampLevel(lvl);
|
||||
|
||||
if (BODY) BODY.dataset.readingLevel = String(v);
|
||||
setActiveUI(v);
|
||||
|
||||
if (persist) {
|
||||
try { localStorage.setItem(KEY, String(v)); } catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("archicratie:readingLevel", { detail: { level: v } })
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// init : storage > initialLevel
|
||||
let start = clampLevel(initialLevel);
|
||||
try {
|
||||
const stored = localStorage.getItem(KEY);
|
||||
if (stored) start = clampLevel(stored);
|
||||
} catch {}
|
||||
|
||||
applyLevel(start, { persist: false });
|
||||
|
||||
// clicks
|
||||
wrap.addEventListener("click", (ev) => {
|
||||
const btn = ev.target?.closest?.("button[data-level]");
|
||||
if (!btn) return;
|
||||
ev.preventDefault();
|
||||
|
||||
// ✅ crucial : on capture la position AVANT le reflow lié au changement de niveau
|
||||
captureBeforeLevelSwitch();
|
||||
applyLevel(btn.dataset.level);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.level-toggle{
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.level-btn{
|
||||
border: 1px solid rgba(127,127,127,0.40);
|
||||
background: rgba(127,127,127,0.08);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: filter .12s ease, transform .12s ease, background .12s ease, border-color .12s ease;
|
||||
}
|
||||
|
||||
.level-btn:hover{
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.level-btn.is-active{
|
||||
border-color: rgba(160,160,255,0.95);
|
||||
background: rgba(140,140,255,0.18);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.level-btn.is-active:hover{
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
.level-btn:active{
|
||||
transform: translateY(1px);
|
||||
}
|
||||
</style>
|
||||
|
||||
1074
src/components/SidePanel.astro
Normal file
@@ -1,11 +1,17 @@
|
||||
<nav class="site-nav" aria-label="Navigation principale">
|
||||
<a href="/">Accueil</a><span aria-hidden="true"> · </span>
|
||||
<a href="/editions/">Carte des œuvres</a><span aria-hidden="true"> · </span>
|
||||
<a href="/methode/">Méthode</a><span aria-hidden="true"> · </span>
|
||||
<a href="/recherche/">Recherche</a><span aria-hidden="true"> · </span>
|
||||
<a href="/archicratie/">Essai-thèse</a><span aria-hidden="true"> · </span>
|
||||
<a href="/traite/">Traité</a><span aria-hidden="true"> · </span>
|
||||
<a href="/ia/">Cas IA</a><span aria-hidden="true"> · </span>
|
||||
<a href="/glossaire/">Glossaire</a><span aria-hidden="true"> · </span>
|
||||
<a href="/atlas/">Atlas</a>
|
||||
</nav>
|
||||
|
||||
<a href="/">Accueil</a>
|
||||
<span aria-hidden="true"> · </span>
|
||||
|
||||
<a href="/archicrat-ia/">Essai-thèse</a>
|
||||
<span aria-hidden="true"> · </span>
|
||||
|
||||
<a href="/cas-ia/">Cas IA</a>
|
||||
<span aria-hidden="true"> · </span>
|
||||
|
||||
<a href="/glossaire/">Glossaire</a>
|
||||
<span aria-hidden="true"> · </span>
|
||||
|
||||
<a href="/recherche/">Recherche</a>
|
||||
|
||||
</nav>
|
||||
@@ -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.
|
||||
|
||||
141
src/content/cas-ia/annexe-glossaire-audit.mdx
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "Annexe — Glossaire archicratique pour l’audit des systèmes d’IA"
|
||||
edition: "cas-ia"
|
||||
status: "application"
|
||||
level: 1
|
||||
version: "0.1.0"
|
||||
concepts: []
|
||||
links: []
|
||||
order: 100
|
||||
summary: ""
|
||||
source:
|
||||
kind: docx
|
||||
path: "sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Annexe_Glossaire_Archicratique_Cas_IA.docx"
|
||||
---
|
||||
# Annexe – Glossaire archicratique pour l’audit des systèmes d’IA
|
||||
|
||||
Cette annexe propose un bref glossaire des notions archicratiques mobilisées dans l’audit de Système F. Elle n’a pas vocation à réexposer la théorie dans toute son ampleur, mais à fournir au lecteur du cas pratique quelques repères opératoires pour suivre le fil des analyses.
|
||||
|
||||
## Arcalité
|
||||
|
||||
On appelle *arcalité* la *dimension de tout ordre régulateur qui concerne ses fondements* : *ce qui l’autorise, ce qui le rend légitime, ce qui lui donne droit de décider*. L’*arcalité* est faite de *récits*, de *principes*, de *valeurs*, de *visées explicites ou implicites* : lutter contre la fraude, protéger l’État social, garantir la sécurité, promouvoir la santé, favoriser le “talent”, préserver la liberté d’expression, etc.
|
||||
|
||||
Dans Système F, on distingue :
|
||||
|
||||
- une *arcalité déclarée* : chartes d’“IA digne de confiance”, discours politiques sur la lutte contre la fraude, documents de marketing qui promettent d’“objectiver” le risque ou d’“optimiser” la sélection ;
|
||||
|
||||
- une *arcalité implicite* : choix de variables (coûts de santé comme proxy des besoins, nationalité comme indicateur de risque), arbitrages entre faux positifs et faux négatifs, composition des jeux de données, sélection de critères de “talent” ou de “dangerosité”.
|
||||
|
||||
L’enjeu archicratique n’est pas de moraliser *a posteriori* ces choix, mais de les *faire comparer sur scène* : *exposer les axiomes silencieux* (“nous considérons qu’il est plus grave de laisser passer un fraudeur que de punir un innocent”, etc.), *et permettre qu’ils soient discutés, contestés, révisés*.
|
||||
|
||||
## Cratialité
|
||||
|
||||
La *cratialité* désigne la dimension opératoire du pouvoir régulateur : la manière dont il s’applique concrètement, par quels instruments, quelles chaînes techniques, quelles procédures. C’est le “*par quoi*” et le “*comment*”.
|
||||
|
||||
Dans Système F, la *cratialité* se déploie dans :
|
||||
|
||||
- les *données mobilisées* (dossiers fiscaux, historiques de soins, casiers judiciaires, CV, contenus de plateformes) ;
|
||||
|
||||
- les *pipelines* qui transforment des situations complexes en vecteurs numériques ;
|
||||
|
||||
- les *modèles* eux-mêmes (architectures, paramètres, fonctions de coût, seuils de décision) ;
|
||||
|
||||
- les *interfaces* (tableaux de bord, scores de risque, codes couleur, messages d’alerte) ;
|
||||
|
||||
- les *procédures d’intégration* (règles internes qui imposent de suivre la recommandation ou d’en justifier tout écart).
|
||||
|
||||
*Cratialité* n’est pas synonyme de “technique” : elle inclut aussi les *règles organisationnelles*, les *consignes*, les *scripts de travail*. L’audit archicratique ne se contente pas de repérer les algorithmes ; il suit les chaînes cratiales jusqu’aux guichets, aux tribunaux, aux services hospitaliers, aux départements RH, aux plateformes.
|
||||
|
||||
## Archicration
|
||||
|
||||
L’*archicration* est le troisième terme du triptyque : elle désigne la *scène d’épreuve où arcalité et cratialité sont amenées en visibilité, confrontées, mises à l’épreuve et, si nécessaire, transformées*.
|
||||
|
||||
Sans *archicration*, les fondements restent fantômes, et le pouvoir opératoire devient autarcique.
|
||||
|
||||
Une *archicration* digne de ce nom réunit quatre conditions minimales :
|
||||
|
||||
1. *Public(s) concerné(s) effectivement représentés* (et pas seulement des experts parlant “en leur nom”) ;
|
||||
|
||||
2. *Accès aux prises* : connaissance minimale des arcalités à l’œuvre (valeurs, finalités, proxies, fonctions de coût) et des cratialités (chaînes techniques, procédures, interfaces) ;
|
||||
|
||||
3. *Capacité de contestation* : possibilité d’interpeller les choix, de demander des modifications, de suspendre un dispositif ;
|
||||
|
||||
4. *Effet réel* : ce qui se dit sur la scène peut produire des transformations sur le système, pas seulement un “avis” consultatif.
|
||||
|
||||
Dans Système F, la plupart des dispositifs existants – comités d’éthique, audits de biais, formulaires de recours – n’atteignent pas ce seuil : ce sont des *archicrations fantômes*, qui donnent l’allure de la scène sans en avoir la puissance.
|
||||
|
||||
## Hypotopie, hypertopie, atopie
|
||||
|
||||
Ces trois termes qualifient la topologie des scènes où la régulation IA apparaît (ou disparaît).
|
||||
|
||||
- Une *hypotopie* est une *scène pauvre en prises, faiblement outillée, où les gens peuvent parler mais sans pouvoir effectivement infléchir la régulation*. Par exemple : un usager qui discute avec un agent de caisse sociale d’une décision prise en réalité par Système F, sans accès ni aux paramètres ni aux logs ; un formulaire de recours tellement opaque et lourd qu’il décourage toute contestation.
|
||||
|
||||
- Une *hypertopie* est une *scène surdotée à huit-clos* : tout s’y joue – paramètres, seuils, choix de déploiement – mais entre un nombre très limité d’acteurs (directions, ingénieurs, juristes, consultants). *Les comités de pilotage où l’on décide d’intégrer ou non Système F dans la chaîne de décision, les réunions de design des modèles sont souvent des hypertopies*.
|
||||
|
||||
- Une *atopie* est une *scène fantomatique* : elle mime le dispositif d’épreuve, sans donner prise réelle. Consultations publiques en ligne sans impact, “boîtes à idées” numériques, mécanismes de *feedback* qui alimentent surtout des métriques internes (satisfaction, engagement) sans reconfigurer la régulation.
|
||||
|
||||
L’épreuve topologique, dans le cas IA, consiste à cartographier où se trouvent les *hypotopies, hypertopies, atopies*, et à inventer des scènes nouvelles qui rééquilibrent cette topologie.
|
||||
|
||||
## Autarchicratie
|
||||
|
||||
L’*autarchicratie* désigne un régime où la régulation devient son propre souverain : les dispositifs de mesure, de modélisation et de contrôle se gouvernent eux-mêmes à partir de leurs propres métriques, et ne reconnaissent plus que très marginalement des scènes externes d’épreuve.
|
||||
|
||||
Dans Système F, l’*autarchicratie* prend plusieurs formes :
|
||||
|
||||
- *modèles évalués principalement par leurs métriques internes* (perte, précision, indicateurs d’équité) ;
|
||||
|
||||
- *dispositifs d’auto-audit et de reporting automatisé* qui servent de preuve de “responsabilité” sans ouvrir réellement le système à la contestation ;
|
||||
|
||||
- *boucles de rétroaction fermées* où les décisions passées alimentent les données futures (ceux que le système considère comme “à risque” seront davantage contrôlés, et donc produiront plus de “preuves” de risque).
|
||||
|
||||
L’*autarchicratie* est l’exact négatif de l’*archicratie* : *là où l’archicratie multiplie les scènes d’épreuve, l’autarchicratie les marginalise ou les simule*.
|
||||
|
||||
## Co-viabilité
|
||||
|
||||
Par *co-viabilité*, on entend la *capacité d’un ordre régulateur à rendre simultanément vivables plusieurs dimensions de l’existence* : sociale, écologique, symbolique, parfois économique.
|
||||
|
||||
Dans le cas IA :
|
||||
|
||||
- la *co-viabilité sociale* renvoie à l’*accès aux droits*, à la *protection contre l’arbitraire*, à la *dignité des personnes* (ne pas être réduit à un profil de risque opaque, pouvoir contester une décision qui affecte des prestations, des peines, des soins, un emploi) ;
|
||||
|
||||
- la *co-viabilité écologique* concerne les *coûts matériels de l’infrastructure* (consommation énergétique, pressions sur les ressources, effets territoriaux des centres de données et des centres d’extraction) et la *possibilité de les mettre en scène* ;
|
||||
|
||||
- la *co-viabilité symbolique* touche aux *représentations de la justice, du mérite, du risque, de la vérité*, et à la manière dont Système F contribue à les figer ou à les rouvrir.
|
||||
|
||||
L’*épreuve de co-viabilité* ne se limite donc pas à mesurer des “impacts” ; elle demande : *quels types de scènes faut-il instituer pour que ces dimensions puissent être mises en balance, arbitrées et révisées ?*
|
||||
|
||||
## Politique des épreuves viables
|
||||
|
||||
La *politique des épreuves viables* est le nom donné à une *orientation normative minimale* : plutôt que de définir un modèle de justice idéal, elle consiste à organiser les épreuves auxquelles les dispositifs régulateurs doivent se soumettre pour rester archicratiques.
|
||||
|
||||
Appliquée à l’IA, elle se traduit par une *série de gestes concrets* :
|
||||
|
||||
- *droit au différé contradictoire* pour les décisions appuyées par un système ;
|
||||
|
||||
- *journaux de justification* documentant les choix de modèles, de métriques, de proxies, d’usages ;
|
||||
|
||||
- *visas d’affectation* qui autorisent ou interdisent certains usages de scores dans des décisions critiques ;
|
||||
|
||||
- *coupe-circuits citoyens* permettant de suspendre un système en cas de dégâts massifs ;
|
||||
|
||||
- *tribunaux de l’algorithme*, assemblées d’affectation, budgets scéniques pour financer le temps de la délibération et de la traduction ;
|
||||
|
||||
- *révisions archicratives périodiques* s’accompagnant d’une *cartographie des scènes manquantes*.
|
||||
|
||||
Dans le cas de Système F, ces gestes ne sont pas des ornements : ils définissent le seuil en deçà duquel il n’est plus raisonnable de parler de gouvernance archicratique de l’IA, mais d’*autarchicratie numérique*.
|
||||
|
||||
## Système F
|
||||
|
||||
Enfin, *Système F* n’est pas le nom d’un produit commercial, mais celui d’une figure composite : un modèle de fondation (LLM / modèle multimodal) accessible par API, intégré dans des flux de travail décisionnels de la protection sociale, de la santé, de la justice, des ressources humaines, des plateformes numériques.
|
||||
|
||||
Il condense des caractéristiques empiriquement attestées :
|
||||
|
||||
- *usage de systèmes de scoring* pour cibler des contrôles de fraude, évaluer des risques pénaux, gérer des programmes de soins, filtrer des candidatures, modérer des contenus ;
|
||||
|
||||
- *insertion de modules d’IA dans des logiciels métier existants* ;
|
||||
|
||||
- *dépendance à des fournisseurs privés de services cloud et de modèles* ;
|
||||
|
||||
- *adoption de chartes d’“IA responsable” et de procédures d’audit parfois plus symboliques qu’effectives*.
|
||||
|
||||
L’intérêt de Système F n’est donc pas de décrire un futur hypothétique, mais de donner un nom commun à une configuration déjà largement engagée, afin de lui appliquer, sans esquive, l’ensemble des épreuves archicratiques.
|
||||
535
src/content/cas-ia/chapitre-1.mdx
Normal file
@@ -0,0 +1,535 @@
|
||||
---
|
||||
title: "Chapitre I — Épreuve de détectabilité"
|
||||
edition: "cas-ia"
|
||||
status: "application"
|
||||
level: 1
|
||||
version: "0.1.0"
|
||||
concepts: []
|
||||
links: []
|
||||
order: 20
|
||||
summary: ""
|
||||
source:
|
||||
kind: docx
|
||||
path: "sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_1_Epreuve_de_detectabilite.docx"
|
||||
---
|
||||
# I. Épreuve de détectabilité : *arcalité / cratialité / archicration* dans un système d’IA
|
||||
|
||||
L’épreuve de détectabilité ne consiste pas à ajouter une couche de vocabulaire au-dessus d’un dispositif déjà saturé de termes techniques. Elle exige, au contraire, un geste presque naïf : *où voit-on quelque chose ? Où peut-on désigner, avec un minimum de précision, ce qui fonde, ce qui opère et ce qui met en épreuve ?* Tant que ces trois prises restent indiscernables ou introuvables, l’*archicratie* n’est pas simplement déficitaire ; elle est empêchée. Appliquée à un grand système d’IA de fondation, l’épreuve de détectabilité commande une micro-cartographie patiente des lieux, des moments et des interfaces où Système F se rend effectivement présent – ou, plus souvent, se déploie sans se déclarer.
|
||||
|
||||
## I.1. Scénarisation structurée du système IA
|
||||
|
||||
L’enjeu de cette section n’est pas d’inventer un futur hypothétique, mais de composer un cas stylisé à partir d’éléments déjà avérés. Système F sera donc construit comme un agrégat abstrait de dispositifs bien documentés, provenant de plusieurs domaines (protection sociale, justice pénale, santé, recrutement, plateformes numériques). Chaque scène d’usage que nous décrirons ensuite reprend des traits explicitement attestés dans ces cas réels.
|
||||
|
||||
### I.1.1. Les briques empiriques de Système F
|
||||
|
||||
#### **Protection sociale : SyRI et le scandale des allocations familiales néerlandaises**
|
||||
|
||||
Dans le domaine de la protection sociale, deux affaires néerlandaises forment un socle empirique très clair.
|
||||
|
||||
Le système SyRI (*Systeem Risico Indicatie*) était un outil de détection de fraude aux prestations sociales, fondé sur le croisement massif de données issues de différentes administrations (assurance sociale, emploi, logement, fiscalité). En février 2020, le tribunal de district de La Haye a jugé que la législation encadrant SyRI violait l’article 8 de la Convention européenne des droits de l’homme (droit au respect de la vie privée), notamment en raison du manque de transparence sur le fonctionnement du système, de la faible proportionnalité et du ciblage de quartiers pauvres. Le tribunal a ordonné l’arrêt immédiat de l’utilisation de SyRI.
|
||||
|
||||
Parallèlement, le scandale des allocations pour la garde d’enfants (*toeslagenaffaire*) a mis au jour un modèle de classification du risque utilisé par l’administration fiscale néerlandaise, qui a conduit à accuser à tort environ 26 000 parents de fraude. Ces familles ont été sommées de rembourser des montants importants de prestations, ce qui a provoqué des situations de surendettement massif, de pertes de logement et de détresse psychologique ; une part disproportionnée des victimes avaient un arrière-plan migratoire. Amnesty International a montré que des éléments comme la nationalité ou la double nationalité étaient utilisés comme facteurs de risque, produisant une boucle de discrimination systémique ; l’autorité néerlandaise de protection des données a conclu à un traitement discriminatoire.
|
||||
|
||||
Ces deux cas attestent empiriquement que des systèmes de *scoring* algorithmique peuvent être intégrés au travail de guichet, cibler certaines catégories de population, fonctionner de manière opaque et produire des décisions automatiques de suspension ou de recouvrement, avec des voies de recours très limitées.
|
||||
|
||||
#### **Justice pénale : l’algorithme COMPAS et l’affaire Loomis**
|
||||
|
||||
Dans le champ pénal, l’algorithme COMPAS (Correctional Offender Management Profiling for Alternative Sanctions), développé par Northpointe/Equivant, est utilisé dans plusieurs États américains pour produire des scores de risque de récidive. Ces scores figurent dans des rapports d’aide à la décision destinés aux juges lors des phases de mise en liberté sous caution, de fixation de peine ou de libération conditionnelle.
|
||||
|
||||
Dans l’affaire State v. Loomis (Cour suprême du Wisconsin, 2016), le prévenu a contesté l’usage de COMPAS en soutenant que la méthodologie était secrète (secret commercial) et qu’il ne pouvait donc ni en vérifier l’exactitude ni la contester. La Cour a maintenu la possibilité pour le juge d’utiliser COMPAS, mais à condition d’accompagner le score de mises en garde sur ses limites et en rappelant que la décision finale reste humaine, la méthode demeurant néanmoins non accessible à la défense.
|
||||
|
||||
On dispose ainsi d’un cas où un score algorithmique chiffré s’inscrit explicitement dans la chaîne de la décision judiciaire, tout en restant largement opaque pour le justiciable et même pour le tribunal.
|
||||
|
||||
#### **Santé : l’algorithme de gestion de risques analysé par Obermeyer et al. (Science, 2019)**
|
||||
|
||||
Dans le système de santé américain, Ziad Obermeyer et ses co-auteurs ont étudié un algorithme commercial largement utilisé pour la gestion de programmes de “soins intensifs” destinés à des patients à haut risque.
|
||||
|
||||
Cet algorithme produit, pour chaque patient, un score de risque ; ceux dont le score dépasse un seuil sont orientés vers des programmes qui mobilisent des ressources supplémentaires (suivi renforcé, coordination des soins, etc.).
|
||||
|
||||
L’étude montre que, pour un niveau de risque donné selon l’algorithme, les patients noirs sont en moyenne beaucoup plus malades que les patients blancs (plus de pathologies non contrôlées, marqueurs biologiques plus dégradés).
|
||||
|
||||
La raison identifiée est que l’algorithme ne prédit pas directement la morbidité, mais les coûts de santé futurs ; or, dans un système marqué par des inégalités d’accès aux soins, les patients noirs engendrent en moyenne moins de dépenses, à état de santé équivalent. Ce choix de variable (coûts comme proxy des besoins) introduit une biais structurel qui conduit à sous-orienter vers les programmes de soins renforcés des patients noirs qui en auraient le plus besoin.
|
||||
|
||||
Ce cas illustre très précisément comment une fonction de coût et un choix de proxy peuvent incorporer une *arcalité implicite* (coûts ≈ besoins) et produire des *effets cratiaux massifs* sur l’accès aux ressources médicales.
|
||||
|
||||
#### **Recrutement : l’outil de tri de CV d’Amazon**
|
||||
|
||||
En 2018, Reuters a révélé qu’Amazon avait développé, puis abandonné, un outil interne de recrutement automatisé visant à classer des CV de candidat·es à des postes techniques. Entraîné sur une décennie d’historiques de recrutement dans un secteur très masculin, le système a “appris” que les profils masculins étaient plus souhaitables. Il pénalisait les CV comportant le mot “women’s” (comme “women’s chess club captain”) et déclassait les diplômées de certaines universités qualifiées de « féminines ».
|
||||
|
||||
Confrontée à ces biais, l’entreprise a renoncé à déployer cet outil en production. Mais l’enquête montre qu’il est techniquement possible – et, en pratique, déjà réalisé – d’intégrer des modèles genrés de scoring dans la chaîne de sélection des candidatures, en laissant leur logique sous-jacente hors de la vue des candidat·es.
|
||||
|
||||
#### **Plateformes numériques : modération et curation algorithmique**
|
||||
|
||||
Enfin, dans le monde des plateformes, la documentation abondante sur Facebook/Meta, YouTube, TikTok ou X (ex-Twitter) montre que la modération des contenus et la curation algorithmique reposent massivement sur des systèmes automatisés, l’intervention humaine intervenant préférentiellement en seconde ligne ou pour les cas litigieux.
|
||||
|
||||
Des guides destinés aux utilisateurs ou aux praticiens décrivent le fonctionnement général de ces dispositifs : chez Facebook, par exemple, l’IA est présentée comme “première ligne de défense”, qui scanne les contenus, détecte les possibles violations et les supprime directement ou les envoie à des modérateurs humains pour revue.
|
||||
|
||||
Au niveau réglementaire, le Digital Services Act européen impose désormais aux grandes plateformes de publier des rapports annuels de transparence sur la modération de contenu, en distinguant explicitement les contenus supprimés ou limités suite à des décisions automatisées.
|
||||
Le rapport 2022 de l’Agence des droits fondamentaux de l’UE sur les biais dans les algorithmes mentionne d’ailleurs l’usage croissant de systèmes de détection automatique de discours offensant, avec des risques de discrimination et de sur-modération de certaines communautés ou langues.
|
||||
|
||||
En parallèle, la création de l’*Oversight Board* de Meta, doté d’un pouvoir de révision de certaines décisions de modération, confirme l’ampleur des enjeux : une instance quasi-juridictionnelle a été instituée pour examiner des cas emblématiques et problématiques, souvent issus de décisions initiales prises par des systèmes automatisés ou semi-automatisés.
|
||||
|
||||
Ces éléments convergent : l’essentiel de ce qui se voit – ou ne se voit pas – sur les grandes plateformes est trié, promu, enfoui, suspendu par des combinaisons d’algorithmes de recommandation, de détection et de filtrage.
|
||||
|
||||
### I.1.2. De ces cas à un système composite : définition structurée de “Système F”
|
||||
|
||||
À partir de ces pièces empiriques, nous pouvons maintenant définir plus rigoureusement ce que nous appellerons Système F.
|
||||
|
||||
#### **Un fournisseur de modèle de fondation accessible par API**
|
||||
|
||||
Du côté de l’offre technique, il est désormais établi que des modèles de fondation (grands modèles de langage, modèles multimodaux) sont fournis sous la forme de services cloud accessibles via API. Le Comité européen de la protection des données (EDPB), par exemple, décrit explicitement le modèle “LLM as a Service” : un fournisseur héberge le modèle, en contrôle les poids et la formation, et donne accès aux utilisateurs par une interface de programmation, sans leur donner la main sur l’architecture interne.
|
||||
|
||||
Des initiatives comme Azure OpenAI Service pour les administrations publiques américaines ou européennes, et des programmes dédiés comme “OpenAI for Government” ou “ChatGPT Gov”, illustrent l’extension de ce modèle au secteur public : les administrations peuvent appeler des modèles puissants pour traiter des textes, analyser des dossiers, générer des réponses, via des API sécurisées.
|
||||
|
||||
L’Ada Lovelace Institute a documenté, dans un rapport dédié, l’usage déjà existant de modèles de fondation dans la sphère publique, intégrés à des outils “grand public” (moteurs de recherche, suites bureautiques, logiciels métiers) que les agents utilisent au quotidien.
|
||||
|
||||
Dans notre scénarisation, Système F désigne donc un fournisseur de modèle de fondation (type LLM / modèle multimodal), hébergé dans le cloud, accessible via API, et mis à disposition d’acteurs publics et privés.
|
||||
|
||||
#### **Des intégrateurs qui fabriquent des solutions verticales**
|
||||
|
||||
Entre le fournisseur de modèle et les administrations, il y a des intégrateurs : grandes entreprises de services numériques, start-ups spécialisées, équipes internes “IA” de ministères ou d’agences. Leur rôle concret, pour l’instant, est bien attesté : adapter des briques de modèles génériques à des cas d’usage sectoriels (chatbots administratifs, aides à la rédaction de courriers, outils d’analyse de documents, systèmes de tri ou de priorisation).
|
||||
|
||||
Les dispositifs de détection de fraude sociale, de *scoring* pénal, de gestion de risques en santé et de tri de CV mentionnés plus haut ne reposent pas tous sur des modèles de fondation au sens strict, mais ils incarnent déjà ce rôle d’intégrateur : transformer une capacité de calcul et de classification en un module prêt à l’emploi pour une administration précise. Système F se situe dans cette continuité : un modèle générique, repris et encapsulé par une diversité de prestataires qui construisent des “solutions” pour la protection sociale, la justice, la santé, le recrutement, la modération.
|
||||
|
||||
#### **Des organisations utilisatrices : ministères, caisses, hôpitaux, tribunaux, entreprises, plateformes**
|
||||
|
||||
Enfin, les utilisateurs institutionnels que nous considérons – caisses de prestations sociales, services fiscaux, tribunaux, hôpitaux, services RH, plateformes de réseaux sociaux – ne sont pas conjecturaux : ce sont précisément ceux qui, dans les affaires documentées, ont déjà recouru à des algorithmes de scoring, de tri ou de filtrage. Les études sur l’IA dans le secteur public montrent que les cas d’usage montent en puissance, notamment dans les fonctions de traitement de dossiers, de tri de demandes, de détection d’anomalies, d’assistance au recrutement et d’information au public.
|
||||
|
||||
En agrégeant ces éléments, Système F désigne donc une chaîne socio-technique structurée en trois niveaux :
|
||||
|
||||
- un fournisseur de modèle de fondation via API ;
|
||||
|
||||
- des intégrateurs qui encapsulent ce modèle dans des solutions sectorielles ;
|
||||
|
||||
- des organisations utilisatrices qui insèrent ces solutions dans leurs procédures quotidiennes.
|
||||
|
||||
Cette structure n’est pas une vue de l’esprit : elle reflète l’architecture déjà mise en place par les grands fournisseurs d’IA et adoptée, progressivement, par des administrations publiques et des entreprises.
|
||||
|
||||
### I.1.3. Quatre scènes typiques d’usage, stylisées mais ancrées
|
||||
|
||||
Sur cette base, nous pouvons maintenant détailler quatre scènes d’usage de Système F, en indiquant à chaque fois les cas réels qui nourrissent la description.
|
||||
|
||||
#### **L’agent de protection sociale et la liste des dossiers**
|
||||
|
||||
Dans de nombreux pays européens, les systèmes de détection de fraude aux prestations prennent la forme de listes de dossiers accompagnées d’un score de risque. Les travaux sur SyRI et sur le scandale des allocations néerlandaises montrent que des fonctionnaires se voient présenter des listes de bénéficiaires classés par “profil de risque”, résultant de modèles qui croisent des données multiples (revenus, composition familiale, historique fiscal, etc.).
|
||||
|
||||
Dans notre scène, l’agent·e d’une caisse sociale ouvre une application de gestion des dossiers : une file virtuelle apparaît, avec pour chaque dossier un score numérique et un code couleur. Le tri par défaut ne suit plus l’ordre chronologique de dépôt, mais la priorité calculée par un module issu de Système F, configuré pour combiner “risque de fraude” et “urgence” selon des paramètres fixés en amont. Cette configuration est directement inspirée des systèmes réels de risque de classement (*risk scoring*), même lorsqu’ils ne reposaient pas encore sur des modèles de fondation.
|
||||
|
||||
#### **Le juge et le rapport de risque**
|
||||
|
||||
L’expérience américaine avec COMPAS montre qu’un rapport de risque algorithmique peut être intégré au dossier judiciaire, sous la forme d’un document qui synthétise un score global et quelques facteurs aggravants ou atténuants. Dans l’affaire Loomis, le prévenu a été évalué comme “à haut risque” par COMPAS, et ce rapport a pesé dans la justification de la peine.
|
||||
|
||||
Dans notre scène, le juge ne voit pas directement Système F, mais un rapport standardisé annexé au dossier : un score, une classe de risque, des éléments de langage justifiant ce score, issus d’un module pénal connecté au modèle de fondation. Ce schéma reprend les traits factuels de COMPAS (score, opacité de la méthode, statut “d’aide à la décision”) tout en les transposant dans une architecture de modèle de fondation accessible via API.
|
||||
|
||||
#### **La médecin et la priorisation des patients**
|
||||
|
||||
L’algorithme étudié par Obermeyer et al. montre comment un outil de gestion des risques en santé peut décider l’éligibilité à un programme de soins renforcés sur la base d’un score calculé à partir des coûts passés.
|
||||
|
||||
Dans notre scène, un médecin hospitalier ouvre la liste des patients en attente d’un examen lourd ou d’une consultation spécialisée. L’interface, alimentée par un module de Système F, propose une “vue optimisée” : les patients sont ordonnés selon un indice de priorité calculé, avec un message qui signale lorsqu’elle s’écarte de cette priorisation (“vous vous écartez de la recommandation algorithmique, confirmez-vous ?”). La scène est stylisée, mais elle transpose directement la logique documentée : un score de risque conditionne l’accès à des ressources rares, et le praticien se voit suggérer un ordre “optimal”.
|
||||
|
||||
#### **La plateforme et la visibilité des contenus**
|
||||
|
||||
Les documents publics de Meta/Facebook reconnaissent que des systèmes automatisés scannent les contenus, les signalent, voire les suppriment directement en première intention. Les obligations de transparence du DSA confirment que les grandes plateformes utilisent des outils automatisés de modération et de recommandation à grande échelle.
|
||||
|
||||
Dans notre scène, un utilisateur poste un contenu critique sur une politique publique ou un témoignage lié à des prestations sociales. Un module dérivé de Système F, intégré à la chaîne de modération et de recommandation, évalue ce contenu : il peut le classer comme “à risque” (discours haineux, désinformation présumée, “contenu limite”), réduire sa visibilité ou déclencher une revue humaine. L’utilisateur ne verra, le plus souvent, qu’un message laconique (“votre contenu a enfreint nos standards”) ou une chute d’audience difficilement interprétable.
|
||||
|
||||
## I.2. *Arcalité déclarée* du système
|
||||
|
||||
Par “*arcalité déclarée*”, nous désignerons l’ensemble des formules par lesquelles les acteurs qui conçoivent, diffusent ou utilisent des dispositifs algorithmiques disent ce qu’ils font et au nom de quoi ils le font. Il ne s’agit pas des détails techniques de l’implémentation, ni des effets réels que nous avons mis au jour dans les affaires de fraude sociale, de justice pénale, de santé, de recrutement ou de plateformes ; il s’agit des *justifications publiques*, telles qu’elles apparaissent dans les textes législatifs, les décisions de justice, les rapports d’autorités administratives, les documents d’entreprises, les chartes de “bonne conduite” et les programmes de régulation internationale. C’est cette couche discursive que l’épreuve archicratique doit d’abord prendre au sérieux, non pour la dénoncer en bloc comme “pure idéologie”, mais pour mesurer ce qu’elle rend visible et ce qu’elle tient pour acquis.
|
||||
|
||||
Dans les cinq domaines où nous avons décrit des usages de Système F – protection sociale, justice pénale, santé, recrutement, plateformes numériques –, cette *arcalité déclarée* adopte des formes différentes, liées aux traditions propres de chaque champ, mais elle se rassemble autour d’une grammaire relativement stable : *lutte contre des menaces identifiées* (fraude, récidive, complications coûteuses, “mauvais recrutements”, contenus illégaux ou dangereux), *rationalisation de l’action publique ou privée*, *promesse d’objectivité*, *impératif de sécurité et de confiance*. À ce niveau, Système F apparaît d’abord comme un auxiliaire : l’algorithme n’est pas présenté comme un souverain qui se substituerait aux institutions, mais comme un instrument qui permettrait à celles-ci de mieux remplir leurs missions.
|
||||
|
||||
### I.2.1. La fraude comme récit de fondation
|
||||
|
||||
Dans le domaine de la protection sociale, l’*arcalité déclarée* est structurée par un récit désormais bien installé : *il s’agit de défendre l’intégrité de l’État social contre la fraude aux prestations, l’abus de droits et les détournements de fonds publics*. Comme l’a montré l’affaire SyRI et, plus encore, le scandale des allocations pour la garde d’enfants, la lutte contre la ‘*fraude*’ devient la matrice discursive qui justifie le recours à des systèmes de classements massifs et opaques.
|
||||
|
||||
Dans ce cadre discursif, les termes techniques – *profil de risque*, *croisement de bases de données*, *ciblage des contrôles* – se présentent comme de simples moyens d’atteindre un but qui, lui, est posé comme incontestable : *protéger la “soutenabilité” des régimes de prestations en évitant qu’ils ne soient “vidés de leur substance” par des comportements abusifs*. L’essentiel de l’argument arcal se déploie dans un lexique de la vigilance et de la responsabilité : l’administration se doit, pour protéger les “vrais bénéficiaires”, de traquer les fraudeurs avec des outils modernes, proportionnés et ciblés.
|
||||
|
||||
Du point de vue archicratique, on voit se dessiner ici une figure typique : la *fraude* devient le point focal de la justification. La question de savoir ce qu’est, concrètement, une erreur de bonne foi, un litige interprétatif, une situation de vulnérabilité structurelle, est reléguée à l’arrière-plan. La tension entre présomption d’innocence et présomption de suspicion est peu articulée ; le terme de “fraude” fonctionne comme un opérateur de condensation qui autorise des dispositifs de surveillance étendus, pourvu qu’ils soient décrits comme des moyens “nécessaires” à la sauvegarde de l’État social. L’*arcalité déclarée* est donc forte (*défense d’un bien collectif précieux*), mais très générale : elle ne descend guère au niveau des catégories fines qui seront effectivement travaillées par Système F.
|
||||
|
||||
### I.2.2. Objectivation du risque et cohérence des peines
|
||||
|
||||
Dans le champ de la justice pénale, l’*arcalité déclarée* s’appuie sur un autre récit, lui aussi bien identifié dans la littérature : celui de l’*évaluation actuarielle du risque* et de la “*cohérence*” *des décisions de justice*. Les guides destinés aux praticiens pour l’usage de COMPAS, par exemple, présentent l’outil comme une méthode “objective, standardisée, fondée sur la recherche” pour estimer la probabilité de récidive à partir d’un ensemble de facteurs psychosociaux et historiques.
|
||||
|
||||
Dans l’arrêt *State v. Loomis*, la Cour suprême du Wisconsin accepte cette finalité : l’usage d’un instrument actuariel est recevable dès lors qu’il est clairement qualifié d’“aide à la décision” et que le juge conserve, en principe, la maîtrise de la peine. La Cour insiste sur la nécessité de rappeler les limites du système, mais elle ne remet pas en cause la légitimité de l’objectif affiché : rendre les décisions plus cohérentes, plus prévisibles, moins dépendantes des intuitions ou des préjugés des magistrats. L’*arcalité déclarée* articule ici un double horizon : *améliorer la justice distributive* (réduire les disparités de traitement) *et la sécurité publique* (anticiper les comportements futurs “à risque”).
|
||||
|
||||
Ce discours s’inscrit dans une histoire plus longue de “gestion du risque” en droit pénal : montée des outils actuariels de probation, intérêt pour les “évaluations fondées sur la preuve”, critiques de l’arbitraire judiciaire. Il emprunte beaucoup au vocabulaire de la statistique, de la psychologie et du management du risque. L’algorithme n’est jamais présenté comme une source de normativité autonome ; il fournit des scores, des ratios, des catégories (“faible”, “moyen”, “élevé”) qui doivent éclairer des décisions qui, elles, demeurent juridiquement encadrées. L’*arcalité déclarée* est donc celle d’une *rationalisation du pouvoir de juger* : mettre la peine sous le signe du calcul et de l’expertise plutôt que du tâtonnement individuel.
|
||||
|
||||
### I.2.3. Gestion de population et ciblage de ressources rares
|
||||
|
||||
Dans le domaine de la santé, les algorithmes que nous avons retenus comme briques de Système F sont inscrits dans un lexique propre : celui de la “politique santé des populations”, de la “gestion de patients jugés à risque”, de la “prévention des hospitalisations évitables”. L’algorithme analysé par Obermeyer et al. est présenté par ses concepteurs et par les hôpitaux qui l’utilisent comme un outil permettant d’identifier les patients “aux besoins complexes” et de les orienter vers des programmes de gestion de soins intensifs.
|
||||
|
||||
Les documents de promotion de ce type de systèmes insistent sur un double objectif : d’une part, “*améliorer la qualité des soins*” en offrant un suivi renforcé aux patients les plus vulnérables ; d’autre part, “*maîtriser les coûts*” en réduisant les complications graves, les ré-hospitalisations et les recours imprévus aux urgences. L’*arcalité déclarée* assume pleinement cette tension : il s’agit de *concilier des impératifs cliniques et budgétaires en allouant des ressources rares* (temps médical, programmes sophistiqués, coordination, technologies coûteuses) *de la “façon la plus efficiente possible*”.
|
||||
|
||||
Sur le plan discursif, la notion de “*risque élevé*” est donc chargée d’une valeur positive : loin de stigmatiser les patients, elle leur ouvre l’accès à des dispositifs jugés bénéfiques. Le modèle d’IA est décrit comme un moyen de “repérer ce que l’œil humain ne voit pas facilement”, c’est-à-dire de *détecter en amont des trajectoires de dégradation de la santé*. À nouveau, l’*arcalité déclarée* en appelle à des valeurs fortes : *meilleure prise en charge, prévention, justice dans l’allocation des soins, soutenabilité financière des systèmes de santé*.
|
||||
|
||||
### I.2.4. “Talent”, méritocratie et efficacité de la sélection
|
||||
|
||||
Dans le secteur du recrutement, les systèmes de tri automatisé empruntent d’autres registres. Les discours qui entourent le projet de classement des CV développé par Amazon, ainsi que la littérature managériale plus large sur les “*talent analytics*”, convergent vers une même promesse : “*mécaniser la recherche des meilleurs talents*”, “*libérer les recruteurs des tâches répétitives*”, “*standardiser la sélection*” et “*réduire les biais individuels*” en confiant la première lecture des dossiers à un modèle.
|
||||
|
||||
L’*arcalité déclarée* ici ne renvoie ni à l’État social ni à la justice pénale, mais à une certaine *représentation de la méritocratie en entreprise* : les “bons profils” seraient ceux qui maximisent la performance, la productivité, l’adéquation à la culture de l’organisation. Le système d’IA est présenté comme un “assistant impartial” capable de faire émerger des “signaux faibles” dans les CV, de repérer des trajectoires prometteuses, de réduire le poids des impressions fugitives et des “préjugés inconscients” des recruteurs.
|
||||
|
||||
Ce discours est en résonance avec une tradition managériale qui valorise le *data-driven HR*, la capacité à “objectiver” des intuitions en les traduisant en scores, en *rankings*, en probabilités de succès. L’algorithme devient une sorte de miroir supposé neutre des “caractéristiques qui différencient les meilleurs employés des autres”, pour reprendre une formule fréquemment mobilisée dans ce champ. L’*arcalité déclarée* associe donc trois promesses : *gain d’efficacité*, *amélioration de la qualité des recrutements*, *réduction affichée des biais humains*.
|
||||
|
||||
### I.2.5. Sécurité, “contenus illégaux” et transparence
|
||||
|
||||
Dans l’univers des grandes plateformes, le Digital Services Act européen donne une formulation particulièrement nette de l’*arcalité déclarée*. Le DSA affirme vouloir “*réduire la distribution de contenus illégaux*”, protéger les utilisateurs contre diverses formes de risques en ligne, et instaurer des exigences nouvelles de transparence et de responsabilisation pour les intermédiaires.
|
||||
|
||||
Les plateformes, de leur côté, ont développé depuis plusieurs années un lexique de sûreté et de réduction des risques : leurs règles de “standards communautaires” ou de “règles de la communauté” visent à “protéger” les utilisateurs contre les discours haineux, le harcèlement, la propagande terroriste, la désinformation dommageable, les images violentes ou sexualisées non consenties. Elles décrivent leurs systèmes d’IA comme une “*première ligne de défense*” qui filtre les contenus au moment de la mise en ligne, identifie les violations manifestes, et transmet les cas ambigus à des équipes de modération humaines.
|
||||
|
||||
L’*arcalité déclarée* se trouve ici à la frontière entre droit et morale : les plateformes affirment respecter les législations nationales et européennes, mais elles revendiquent aussi la mise en œuvre de “standards de communauté” qui excèdent parfois les strictes obligations juridiques. Avec le DSA, une grammaire spécifique se stabilise : celle de la “*sécurité en ligne*” et de la “*transparence des décisions de modération*”, adossée à des *obligations concrètes* (mécanismes de signalement des contenus illégaux, justification des décisions de retrait ou de déréférencement, bases de données publiques d’actions de modération).
|
||||
|
||||
Dans ce cadre, l’usage d’outils automatisés est présenté comme un moyen de faire face à des volumes massifs de contenus, de “réagir rapidement” à des menaces, de limiter l’exposition du public à des messages jugés dangereux. L’*arcalité déclarée* articule donc *protection des usagers, respect de la liberté d’expression* (au moins dans l’intention), *et exigence de transparence accrue.*
|
||||
|
||||
### I.2.6. Principes transversaux : “*IA responsable*”, “*IA digne de confiance*”
|
||||
|
||||
Au-dessus de ces légitimités sectorielles, une couche plus générale s’est constituée autour de l’idée d’“*IA responsable*” ou d’“*IA digne de confiance*”. Les Principes de l’OCDE sur l’intelligence artificielle, adoptés en 2019, affirment que les systèmes d’IA devraient “*bénéficier aux personnes et à la planète en favorisant une croissance inclusive, le développement durable et le bien-être*”, être conçus de manière à “*respecter l’état de droit, les droits de l’homme, les valeurs démocratiques et la diversité*”, et inclure des garanties appropriées (*intervention humaine, traçabilité, sécurité*).
|
||||
|
||||
Les Lignes directrices européennes pour une IA digne de confiance, élaborées par le groupe d’experts de haut niveau sur l’IA, déclinent ces ambitions en sept “*exigences*” : agence humaine et contrôle, robustesse et sécurité techniques, gouvernance des données, transparence, diversité et équité, bien-être sociétal et environnemental, responsabilité. Elles sont assorties d’une liste d’auto-évaluation (ALTAI) destinée aux développeurs et aux utilisateurs pour vérifier que leurs systèmes s’y conforment.
|
||||
|
||||
Les grands fournisseurs privés de modèles de fondation se sont, pour l’essentiel, alignés sur cette grammaire. Google a publié en 2018 ses “*AI Principles*”, qui insistent sur le fait que les applications d’IA doivent être “*socialement bénéfiques*”, “*éviter de créer ou de renforcer des biais injustes*”, être “*construites et testées pour la sécurité*”, “*être responsables devant les personnes*” et “*incorporer la vie privée dès la conception*”. Microsoft a formulé, quant à lui, un ensemble de principes analogues : *équité, fiabilité et sécurité, confidentialité et sécurité, transparence, responsabilité, inclusivité*, qui servent de base à son “*Responsible AI Standard*” interne.
|
||||
|
||||
Cette couche transversale d’énoncés n’est pas anecdotique : elle fournit le vocabulaire dans lequel Système F doit, aujourd’hui, se présenter pour être légitime. Une administration qui lance un appel d’offres pour un module de détection de fraude ou d’aide à la décision judiciaire attend des fournisseurs qu’ils s’inscrivent dans ce registre ; une entreprise qui internalise un système de tri de CV est incitée à le faire sous la bannière de l’“IA responsable” et de la lutte contre la discrimination. L’*arcalité déclarée* de Système F est donc redoublée par ce *halo de principes généraux*, qui se veulent compatibles avec les droits fondamentaux et les valeurs démocratiques.
|
||||
|
||||
### I.2.7. Une grammaire commune de légitimation
|
||||
|
||||
Si l’on rassemble ces différents registres, une grammaire commune de l’*arcalité déclarée* apparaît assez nettement. Elle articule au moins quatre motifs récurrents.
|
||||
|
||||
Le premier est celui de la *protection contre des menaces identifiées*. Dans la protection sociale, c’est la fraude aux prestations qui menace la viabilité du système ; dans la justice pénale, c’est la récidive qui met en danger la sécurité publique ; dans la santé, ce sont les complications évitables et les trajectoires de dégradation qui menacent la stabilité des systèmes de soins ; dans le recrutement, ce sont les “mauvais choix” qui mettent en péril la compétitivité de l’entreprise ; sur les plateformes, ce sont les contenus illégaux ou nocifs qui mettent en péril les usagers et l’espace public. L’algorithme est habilité à partir du moment où il est cadré comme une *barrière* supplémentaire contre ces risques.
|
||||
|
||||
Le deuxième motif est celui de la *rationalisation* et de l’*efficience*. L’automatisation est décrite comme un moyen de traiter plus de dossiers, plus rapidement, avec moins de ressources humaines, de rendre les décisions plus cohérentes, de réduire l’arbitraire. Dans le langage des administrations sociales, cela se traduit par la promesse de “*ciblage des contrôles*” et de “*réduction des abus*” ; dans celui des hôpitaux, par la “*priorisation des patients à haut risque*” ; dans celui des services RH, par la “*gestion de volumes massifs de candidatures*” ; dans celui des plateformes, par la possibilité de *modérer des quantités de contenus impossibles à gérer manuellement*. Système F est ici la figure d’un auxiliaire rationnel, intensifiant les capacités habituelles des organisations.
|
||||
|
||||
Le troisième motif est celui de l’*objectivité* – ou, à tout le moins, d’une réduction des biais imputés aux évaluations purement humaines. Dans la justice pénale, l’*évaluation actuarielle* se donne comme “*fondée sur la preuve*” plutôt que sur les intuitions ; dans la santé, le score de risque agrège des données cliniques et des historiques de dépenses ; dans le recrutement, l’algorithme promet d’être indifférent au genre, à l’origine, à l’apparence ; dans les plateformes, les modèles de détection de contenus haineux ou terroristes se présentent comme appliquant des critères constants. L’*arcalité déclarée* insiste sur l’idée que ces dispositifs ne font que *mesurer* ou *mettre en forme* *des régularités objectives déjà là*.
|
||||
|
||||
Enfin, le quatrième motif est celui de la *sécurité* et de la *confiance*. Les discours sur l’“*IA digne de confiance*” et les documents de régulation européenne convergent : *pour être acceptable, un système doit être robuste, fiable, “sûr”, respecter la vie privée, être transparent et rendre des comptes*. Dans le langage des plateformes, la “*sûreté*” est un *produit que l’on fournit aux utilisateurs* ; dans les textes de l’OCDE et de l’Union européenne, la “*confiance*” est une *condition de possibilité du déploiement de l’IA dans des secteurs sensibles*.
|
||||
|
||||
Cette grammaire n’est pas simplement un vernis. Elle structure les termes dans lesquels les acteurs peuvent se justifier devant les tribunaux, les autorités de régulation, les opinions publiques. Elle *détermine ce qui peut être publiquement défendu* et ce qui, au contraire, doit rester dans les coulisses (sélection des variables, choix des proxies, calibrage des seuils). Du point de vue archicratique, elle constitue bien une *arcalité* : *une manière d’exposer – au moins en principe – les finalités et les valeurs censées commander l’usage* de Système F.
|
||||
|
||||
### I.2.8. Une *arcalité réelle*, mais *extra-scénique*
|
||||
|
||||
Reste à savoir dans quelle mesure cette *arcalité déclarée*, riche et apparemment sophistiquée, remplit les conditions minimales d’une *arcalité* proprement archicratique, c’est-à-dire d’une mise en scène effective des fondements. La réponse, à ce stade de l’audit, est ambivalente.
|
||||
|
||||
D’un côté, il serait excessif de considérer que les dispositifs que nous avons examinés seraient dépourvus de toute justification explicite. La lutte contre la fraude sociale, la recherche de cohérence dans les peines, la volonté de mieux prendre en charge des patients vulnérables, l’ambition de limiter les discriminations à l’embauche, la nécessité de réguler les contenus illégaux ou dangereux sur les grandes plateformes, la référence aux droits fondamentaux et aux valeurs démocratiques dans les textes internationaux : tout cela constitue un *socle normatif substantiel*. L’*arcalité déclarée* n’est pas un pur écran de fumée ; elle exprime des préoccupations réelles, souvent largement partagées.
|
||||
|
||||
D’un autre côté, cette *arcalité* reste le plus souvent *extra-scénique*. Elle s’exprime dans des préambules de lois, des communiqués, des chartes, des rapports, mais elle n’est guère travaillée dans des scènes où les notions centrales – fraude, risque acceptable, mérite, besoin de soin, dangerosité, contenu nuisible – seraient définies, discutées, révisées en présence de ceux qui en subissent les effets. La définition pratique de la fraude, dans les dispositifs de protection sociale, n’est pas débattue en tant que telle avec les allocataires ; la hiérarchie entre erreurs acceptables et erreurs intolérables dans la justice pénale n’est pas posée frontalement aux justiciables ; le choix de prendre les coûts comme proxy des besoins en santé n’est pas soumis à une délibération spécifique avec les patients et les soignants ; les critères de “talent” dans le recrutement ou de “contenu nuisible” sur les plateformes ne donnent que rarement lieu à des dispositifs de confrontation structurée.
|
||||
|
||||
L’effet archicratique de cette situation est double. D’une part, les *valeurs affichées* – protection de l’État social, prévention de la récidive, justice dans l’accès aux soins, égalité des chances, sécurité des espaces numériques, respect des droits fondamentaux – fonctionnent comme des *slogans d’arrière-plan* : elles justifient en bloc l’entrée de Système F dans les chaînes décisionnelles, sans que soient explicitées les manières concrètes dont elles sont traduites en paramètres, en seuils, en arbitrages d’erreurs. D’autre part, cette *faiblesse scénique* ouvre un espace où les véritables fondements opératoires tendent à se déplacer vers d’autres couches du dispositif : fonctions de coût, choix de proxies, sélection de variables, architecture des jeux de données.
|
||||
|
||||
Autrement dit, l’*arcalité déclarée* de Système F est à la fois réelle et incomplète : elle pose des finalités, mais elle ne prend pas à bras-le-corps la question de leur implémentation normative fine. Elle ouvre un horizon de légitimation, mais elle laisse largement hors scène les fondements effectifs à partir desquels le système se met à discriminer, prioriser ou classer.
|
||||
|
||||
C’est cette dissociation que la section suivante se propose d’examiner. En passant à l’*arcalité implicite*, nous quitterons les formulations officielles pour aller voir où se logent, dans Système F, les axiomes silencieux : ceux qui définissent en pratique ce qui compte comme fraude, comme risque tolérable, comme mérite, comme besoin, comme contenu acceptable. C’est là que se noue l’*oblitération archicratique* caractéristique des dispositifs contemporains : non pas absence de fondement, mais fuite du fondement derrière l’optimisation.
|
||||
|
||||
## I.3. *Arcalité implicite*
|
||||
|
||||
Avec l’*arcalité déclarée*, nous avons observé ce que les acteurs disent de Système F : protection de l’État social, objectivation du risque pénal, ciblage des soins, rationalisation du recrutement, sécurité des espaces numériques, “IA responsable” et “digne de confiance”. L’*arcalité implicite* se loge ailleurs : dans les *fonctions de coût*, les *proxies choisis*, les *métriques d’évaluation*, la *composition des jeux de données*, les *seuils*, les *arbitrages sur les erreurs*. C’est là que se décide, au sens fort, ce qu’est un “bon” résultat pour le système, ce qu’on est prêt à sacrifier, qui l’on accepte de sur-surveiller, qui l’on accepte de mal desservir.
|
||||
|
||||
Du point de vue archicratique, chaque choix de ce type relève d’un acte de fondation silencieux : il fixe une hiérarchie entre injustices acceptables, il distribue les soupçons et les protections, il stabilise une certaine vision de ce qui compte. L’important n’est pas d’opposer un “pôle technique” neutre à un “pôle politique” chargé ; c’est de *reconnaître que le paramétrage lui-même a une portée normative*, et qu’il constitue une *arcalité effective* qui, le plus souvent, ne comparaît jamais en tant que telle. C’est ce régime des fondements non mis en scène que nous appellerons *arcalité implicite* ou *arcalité fantôme*.
|
||||
|
||||
### I.3.1. Arbitrages d’erreurs et profilage social
|
||||
|
||||
Dans les dispositifs de détection de fraude aux prestations qui composent la brique “protection sociale” de Système F, l’objectif opérationnel est de distinguer des dossiers “à risque” des autres. Techniquement, cela se traduit par une *fonction de coût qui pondère plusieurs objectifs* : *maximiser la détection des fraudes avérées*, *limiter les faux positifs*, *contenir les coûts d’enquête*, *éviter de suspendre abusivement des prestations*. En apparence, il s’agit d’*optimiser une procédure de contrôle* ; en réalité, on décide comment répartir la charge de l’erreur entre l’État et les allocataires.
|
||||
|
||||
L’affaire néerlandaise des allocations pour la garde d’enfants, relue par Amnesty International dans son rapport *Xenophobic Machines*, a montré à quel point ces arbitrages implicites pouvaient devenir violents. Des dizaines de milliers de familles, très majoritairement à faibles revenus et souvent issues de l’immigration, ont été accusées à tort de fraude et soumises à des recouvrements massifs sur la base de profils de risque établis par un système automatisé. Amnesty documente l’usage de variables comme la nationalité ou la double nationalité dans la construction des profils, ce qui produisait un ciblage disproportionné de certains groupes ethniques et migratoires.
|
||||
|
||||
Du point de vue archicratique, plusieurs axiomes implicites se donnent ici à voir. D’abord, l’idée selon laquelle il serait acceptable de concentrer un volume élevé de faux positifs sur des populations déjà précaires, au nom de la défense du budget public, sans scène où ces personnes puissent contester la hiérarchie ainsi instituée entre la protection des fonds et la protection des droits. Ensuite, l’idée qu’une caractéristique comme la nationalité puisse servir de proxy de suspicion, alors même que le droit non discriminatoire des États européens proscrit précisément ce type d’usage. Le rapport *Xenophobic Machines* parle de discrimination “intégrée” dans la conception même du système, non seulement dans ses usages déviants.
|
||||
|
||||
La décision du tribunal de La Haye dans l’affaire SyRI confirme le diagnostic à un autre niveau. SyRI était un dispositif de profilage de risque en matière de sécurité sociale, fondé sur le croisement massif de données issues de multiples administrations et sur la production de “*rapports de risque*” transmis aux services d’inspection. Le tribunal a jugé que la législation encadrant SyRI violait l’article 8 de la Convention européenne des droits de l’homme, en raison notamment d’un déficit de transparence, de l’ampleur du traitement et du ciblage de quartiers défavorisés.
|
||||
|
||||
Ce qui est en cause, là encore, n’est pas seulement une erreur de calibrage, mais une *arcalité implicite* : il serait considéré comme *tolérable de soumettre certains territoires à un profilage intensif*, *de croiser leurs données à grande échelle, de déclencher des enquêtes intrusives sur la base de scores opaques, au nom d’un “juste équilibre” entre lutte contre la fraude et respect de la vie privée*. Or ce “*juste équilibre*” n’a pas été déterminé sur une scène où l’on aurait mis en débat les types d’erreurs acceptables, les populations exposées, la nature des données croisées. Il a été fixé dans la fonction de coût globale du système, puis naturalisé sous la forme d’une procédure “moderne” de contrôle.
|
||||
|
||||
L’*arcalité implicite* de Système F, dans ce segment social, prend donc la forme d’une *hiérarchisation silencieuse* : mieux vaut tolérer un nombre important de faux positifs concentrés sur des familles vulnérables que d’accepter une fraude résiduelle ; mieux vaut considérer certains profils nationaux ou résidentiels comme intrinsèquement plus suspects que d’ouvrir un débat public sur les causes structurelles des erreurs, des omissions ou des malentendus administratifs.
|
||||
|
||||
### I.3.2. Le *proxy* “*coûts*” comme axiome de valeur
|
||||
|
||||
L’algorithme de gestion des risques étudié par Ziad Obermeyer et ses co-auteurs offre un exemple paradigmatique d’*arcalité implicite*. Le système, largement utilisé dans les systèmes de santé américains, sert à déterminer quels patients doivent bénéficier de programmes de “*care management*” intensif : ceux dont le score dépasse un certain seuil se voient offrir un suivi renforcé, des ressources supplémentaires et une coordination accrue.
|
||||
|
||||
Les auteurs montrent que, pour un même score de risque donné par l’algorithme, les patients noirs sont en moyenne bien plus malades que les patients blancs : davantage de pathologies chroniques non contrôlées, plus de complications, etc. La raison n’est pas une “erreur” de calcul, mais le choix du proxy : l’algorithme prédit les coûts de santé futurs plutôt que la morbidité elle-même. Or, dans un système marqué par des inégalités d’accès aux soins, on dépense historiquement moins pour les patients noirs que pour les patients blancs à état de santé comparable. En prenant les coûts comme substitut des besoins, l’algorithme sous-estime donc systématiquement les besoins des patients noirs, ce qui conduit à les orienter beaucoup plus rarement vers les programmes intensifs.
|
||||
|
||||
Du point de vue archicratique, le choix du proxy “coûts” constitue un acte d’*arcalité implicite* majeur : il institue, sans jamais le dire, l’idée que *la meilleure approximation des besoins de santé d’un individu est ce que le système a déjà dépensé pour lui*. Dans un univers égalitaire et sans contraintes budgétaires, ce raccourci pourrait éventuellement se discuter ; dans un univers où l’accès aux soins est profondément inégal, il revient à considérer que les vies pour lesquelles on a historiquement le moins dépensé sont objectivement moins “à risque” et moins prioritaires. L’axiome est extraordinairement chargé sur le plan moral et politique, mais il n’est pas présenté comme un choix de justice : il est codé comme un paramètre d’optimisation raisonnable.
|
||||
|
||||
Obermeyer et al. montrent qu’en remplaçant le proxy “coûts” par un proxy plus proche des besoins cliniques (par exemple le nombre de maladies chroniques non contrôlées), on pourrait presque tripler la part des patients noirs orientés vers les programmes intensifs.
|
||||
|
||||
Autrement dit, un seul choix de variable suffit à faire basculer la répartition d’un dispositif de soin à grande échelle. Or ce choix n’a donné lieu ni à une controverse publique, ni à une délibération institutionnelle, ni à un dispositif d’*archicration* : il a été décidé en amont, au croisement de considérations pratiques (disponibilité des données, facilité de mesure) et de rationalités gestionnaires (coûts comme indicateur privilégié), puis diffusé comme allant de soi.
|
||||
|
||||
Ce cas illustre avec une précision chirurgicale ce que nous nommons *arcalité fantôme* : l’*existence de fondements normatifs réels* – ici, une conception du besoin de santé traduite en coût – *qui pilotent une large distribution de ressources, sans jamais être convoqués sur une scène où ils devraient se justifier devant ceux qu’ils affectent*.
|
||||
|
||||
### I.3.3. Hiérarchies des erreurs et justice des risques
|
||||
|
||||
L’outil d’évaluation de risque pénal COMPAS, lorsqu’on le regarde à travers les débats suscités par l’enquête de ProPublica et les réponses de ses concepteurs, met en lumière un autre aspect de l’*arcalité implicite* : la gestion différenciée des faux positifs et des faux négatifs selon les groupes. ProPublica a montré en 2016 que, dans le comté de Broward, les accusés noirs étaient beaucoup plus souvent classés à tort comme “à haut risque” que les accusés blancs (faux positifs), tandis que les accusés blancs étaient plus souvent classés à tort comme “faible risque” alors qu’ils récidivaient (faux négatifs).
|
||||
|
||||
Des chercheurs et les auteurs de COMPAS ont contesté la méthodologie de ProPublica, en soulignant qu’il est mathématiquement impossible de satisfaire simultanément plusieurs notions de “*justice algorithmique*” (par exemple égalité des taux de faux positifs et égalité de calibration) lorsque les taux de récidive diffèrent entre groupes.
|
||||
|
||||
Mais, du point de vue archicratique, cette impossibilité mathématique ne dissout pas le problème : elle le reformule avec plus d’acuité. Si toutes les configurations sont impossibles simultanément, il faut choisir lesquelles on privilégie : *faut-il minimiser les faux négatifs* (ne pas laisser sortir un individu qui récidivera) *au prix d’un grand nombre de faux positifs* (maintenir en détention des personnes qui n’auraient pas récidivé) ? *Ou l’inverse ? Est-il acceptable que ces arbitrages se distribuent différemment selon les groupes racisés ?*
|
||||
|
||||
En droit pénal, cette question n’est pas nouvelle : elle recoupe des débats de longue durée sur la *présomption d’innocence*, sur la maxime selon laquelle “*mieux vaut laisser dix coupables en liberté que condamner un innocent*”, sur la hiérarchie entre sécurité collective et protection contre l’erreur judiciaire. Ce que le recours à un outil actuariel comme COMPAS transforme, c’est le lieu où cette hiérarchie est fixée : au lieu d’être l’objet d’une discussion explicite – au Parlement, dans la doctrine, dans la jurisprudence – elle est réglée par la manière dont on écrit la fonction de coût, choisit les métriques d’évaluation, fixe les seuils de risque et paramètre la calibration. Les débats techniques sur les définitions de l’équité deviennent le substitut de débats normatifs sur ce que l’on juge plus grave : enfermer injustement ou libérer à tort.
|
||||
|
||||
Là encore, l’*arcalité implicite* n’est pas l’absence de normativité, mais sa *relégation dans des micro-décisions techniques qui n’apparaissent jamais comme telles aux justiciables*. Un prévenu auquel on annonce un score de risque “élevé” ne voit pas la structure normative qui a présidé au calibrage des erreurs ; un juge qui consulte ce score ne voit pas davantage la hiérarchie implicite entre types d’injustice. La scène archicratique – celle où l’on discuterait ouvertement du partage admissible des risques d’erreurs – se trouve remplacée par une scène actuarielle limitée à quelques spécialistes.
|
||||
|
||||
### I.3.4. Apprendre des hiérarchies sociales existantes
|
||||
|
||||
Le système de tri de CV développé par Amazon, puis abandonné avant son déploiement, met au jour une autre dimension de l’*arcalité implicite* : *l’importation non critique de hiérarchies sociales existantes dans la définition de ce qui compte comme “talent”*. L’enquête de Jeffrey Dastin pour Reuters a montré que l’outil, entraîné sur une dizaine d’années d’historiques de recrutement dans des métiers techniques majoritairement masculins, avait “appris” à désavantager les CV féminins : il pénalisait les dossiers contenant le mot “women’s” et rétrogradait certains établissements fréquentés par des femmes.
|
||||
|
||||
L’*arcalité implicite* ne réside pas seulement dans ces effets visibles, mais dans la décision initiale de prendre le passé des recrutements comme référence pour l’avenir. En faisant de la capacité à prédire “qui serait embauché” à partir des données historiques la fonction de coût principale, Amazon a installé comme norme ce que ses pratiques antérieures, déjà marquées par un déséquilibre de genre, considéraient comme un “*bon candidat*”. L’algorithme n’invente pas le biais ; il le systématise et le cristallise.
|
||||
|
||||
Du point de vue archicratique, cette configuration revient à traiter les décisions passées – elles-mêmes situées dans des rapports de pouvoir, des routines, des préjugés – comme un “réel” à imiter. La question “qu’est-ce qu’un talent ?” n’est pas abordée comme une question ouverte, susceptible de révision, mais comme un pattern à extraire d’un corpus de CV, de trajectoires et de décisions. L’*arcalité implicite* associe trois éléments : *l’idée que la performance passée est la meilleure mesure du mérite futur* ; *la naturalisation de hiérarchies de genre, de diplôme, de style de CV* ; *la marginalisation de toute scène où ces hiérarchies pourraient être discutées par les personnes qu’elles affectent.*
|
||||
|
||||
L’épisode se conclut par l’abandon du projet, une fois les biais mis au jour par les ingénieurs. Mais il illustre bien la logique de Système F : tant que l’algorithme fonctionne dans les coulisses, la question de la normativité de ses critères reste invisible. Ce n’est qu’au moment où l’outil “déraille” visiblement – ici, en supprimant presque mécaniquement les femmes du pool de candidats – que les questions archicratiques surgissent, sur un mode défensif.
|
||||
|
||||
### I.3.5. Seuils de tolérance et architectures de la visibilité
|
||||
|
||||
Dans le cas des plateformes numériques, l’*arcalité implicite* se manifeste dans un autre registre : celui des seuils de tolérance au contenu et des architectures de visibilité. Les textes de mise en œuvre du Digital Services Act, ainsi que les guides de transparence publiés par la Commission européenne et les analyses doctrinales récentes, montrent que les grandes plateformes sont désormais explicitement tenues de rendre compte de leurs décisions de modération, des outils automatisés qu’elles emploient et des risques systémiques qu’elles identifient.
|
||||
|
||||
Les rapports de transparence de Meta, combinés aux travaux de l’*Oversight Board* et aux analyses critiques sur l’évolution des “*Community Standards*”, illustrent concrètement l’ampleur de la délégation faite à des systèmes d’IA pour détecter, classer et retirer des contenus. Meta reconnaît que des systèmes automatisés identifient et agissent sur une grande partie des contenus avant même qu’ils soient signalés, et l’*Oversight Board* rappelle que des millions d’utilisateurs font appel de décisions de retrait initialement prises par ces systèmes, souvent en expliquant que leur contenu relevait de l’information, de la satire ou du témoignage.
|
||||
|
||||
Dans ce contexte, l’*arcalité implicite* ne se situe pas seulement dans la définition formelle des “discours haineux”, des “contenus terroristes” ou de la “désinformation”, ni même dans la distinction entre illégalité et simple non-conformité aux standards de communauté. Elle se loge dans la manière dont les systèmes automatisés et leurs opérateurs paramètrent les seuils : *à partir de quel degré d’ambiguïté un contenu est-il retiré préventivement ? jusqu’où privilégie-t-on la réduction du risque d’exposition au détriment de la préservation de la parole minoritaire ? comment traite-t-on les contenus dans des langues peu dotées, pour lesquelles les modèles sont moins précis ?* Les études récentes sur l’impact de ces choix dans le Sud global ou pour certaines communautés minorées montrent que les erreurs de classification et les suppressions abusives se concentrent souvent là où les ressources linguistiques et les contextes locaux sont les moins bien pris en compte.
|
||||
|
||||
Le DSA impose aux très grandes plateformes la création de bases de données de décisions de modération et ouvre des accès à la recherche, ce qui, en principe, devrait permettre de rendre visibles ces arbitrages. Mais, tant que le débat reste centré sur la conformité globale aux obligations (rapports de transparence, existence de mécanismes de recours, descriptions qualitatives des systèmes automatisés), la question archicratique de fond – à savoir la hiérarchie implicite entre les types d’erreurs acceptables, les types de contenus sur-modérés ou sous-modérés, les publics plus ou moins protégés – demeure largement encapsulée dans les paramètres des modèles et dans les règles internes.
|
||||
|
||||
### I.3.6. L’*arcalité fantôme* comme régime des fondements non mis en scène
|
||||
|
||||
Les fragments que nous venons de parcourir permettent de préciser, sans métaphore, ce que nous nommons *arcalité implicite* ou *arcalité fantôme*.
|
||||
|
||||
Nous appellerons *arcalité fantôme* l’*opérateur dont les fondements normatifs du dispositif ne disparaissent pas, mais se trouvent encapsulés dans des options de paramétrage* (fonctions de coût, proxies, métriques, seuils) *qui opèrent à l’intérieur de la cratialité, sans jamais être exposées comme telles sur une scène d’épreuve*.
|
||||
|
||||
Dans la protection sociale, la *manière de calibrer les modèles de détection de fraude* – *choix des variables, des seuils, des quartiers ciblés, rapport accepté entre faux positifs et faux négatifs* – encode une *hiérarchie entre la lutte contre les abus et la protection des allocataires de bonne foi*, ainsi qu’une *distribution des soupçons selon des lignes de classe et d’origine*. Cette *hiérarchie* n’est en aucune manière discutée publiquement avec les personnes concernées ; elle est incorporée dans la fonction de coût globale du dispositif.
|
||||
|
||||
Dans la santé, le *choix d’un proxy* comme les *coûts de santé* pour représenter *les besoins encode une conception implicite de la valeur des vies en fonction des dépenses déjà engagées*, *dans un contexte où ces dépenses sont ethniquement inégales*. Ce choix transforme une inégalité historique d’accès aux soins en différence “objective” de risque, puis en inégalité d’accès à des programmes intensifs supplémentaires.
|
||||
|
||||
Dans la justice pénale, la définition d’un profil de risque et la manière de pondérer les erreurs entre différents groupes encode une hiérarchie des injustices supportables : mieux vaut enfermer trop de personnes qui ne récidiveront pas, ou libérer trop de personnes qui récidiveront, et pour quels groupes précisément ? La littérature sur COMPAS montre que, quelles que soient les positions prises dans le débat, un choix normatif irréductible doit être fait, mais qu’il est pris en pratique dans l’écriture de la fonction de coût et des métriques, non dans une scène de délibération pénale explicite.
|
||||
|
||||
Dans le recrutement, la décision de prendre les pratiques passées comme guide principal encode une conception du mérite qui sanctuarise des hiérarchies sociales et de genre. L’outil d’Amazon a rendu visible ce mécanisme en produisant des effets suffisamment grossiers pour être politiquement indéfendables ; mais le mécanisme – apprentissage à partir d’historiques de décisions biaisées – demeure au cœur de nombreux systèmes d’“analyse de talents”.
|
||||
|
||||
Sur les plateformes, enfin, les seuils de détection, la granularité des catégories de contenu, les modèles de risque assignés aux différentes langues et régions encodent une hiérarchie implicite entre la protection contre certains types de discours et la tolérance à d’autres, entre la visibilité accordée à certains groupes et la sur-modération d’autres. Les obligations du DSA ouvrent des voies de visibilité, mais ne constituent pas en elles-mêmes une scène où ces hiérarchies seraient mises en débat avec les publics concernés.
|
||||
|
||||
Dans tous ces cas, on ne peut pas dire qu’il n’y ait pas de fondements : au contraire, ils sont nombreux, puissants, efficaces. Ils prennent la forme de fonctions de coût, de choix de proxies, de distributions de données, de métriques de performance, de seuils de décision. Simplement, ces fondements ne sont pas exposés comme tels ; ils n’apparaissent ni dans les chartes, ni dans les communiqués, ni dans les rapports de principe. Ils agissent dans la *cratialité* du système – dans son code, ses pipelines, ses paramétrages –, mais ils n’accèdent jamais au statut de fondements discutables.
|
||||
|
||||
C’est en ce sens que nous parlons d’*arcalité fantôme* : non pas une absence d’*arcalité*, mais une *arcalité exilée hors de la scène*. La modernité algorithmique ne se caractérise pas seulement par une intensification de la capacité à calculer, à corréler, à prédire ; elle se caractérise par une dés-scénarisation des fondements, c’est-à-dire par le transfert des décisions normatives les plus décisives dans des couches techniques où elles ne sont plus identifiées comme telles.
|
||||
|
||||
Au regard de l’*archicratie*, cela signifie que la promesse d’une “*IA responsable*” ou “*digne de confiance*” reste structurellement bancale tant que l’on ne ramène pas ces choix à la scène, c’est-à-dire tant qu’on ne crée pas de dispositifs archicratifs pour les rendre visibles, contestables, révisables. L’épreuve de détectabilité, dans sa dimension d’*arcalité implicite*, permet précisément de diagnostiquer ce défaut : là où les axiomes les plus lourds sont dissimulés derrière le langage de l’optimisation, l’*archicratie* n’est pas seulement déficitaire ; elle est *empêchée*. C’est sur cette base que pourra être menée, dans les sections ultérieures, l’analyse des *cratialités* de Système F et des *archicrations* – rares, fragmentaires, parfois fantomatiques – qui tentent déjà, dans certains secteurs, de réintroduire de la scène dans un ordre régulé par les modèles.
|
||||
|
||||
## I.4. Cratialité du système IA
|
||||
|
||||
Si l’*arcalité* répond à la question “*selon quoi ou qui ?*”, la *cratialité* répond à la question “*par quoi et comment ?*”. Dans Système F, ce “*par quoi et comment*” n’est pas un point, mais une chaîne : *collecte et circulation de données*, *construction d’attributs*, *entraînement du modèle*, *réglages successifs*, *mise en production*, *intégration dans des logiciels très ordinaires* et enfin *procédures d’usage*. Ce que l’*archicratie* appelle *cratialité*, c’est précisément cette chaîne, dès lors qu’elle ne se contente plus d’outiller une activité, mais *distribue de manière répétée des accès, des soupçons, des protections, des sanctions*.
|
||||
|
||||
Dans un centre de données où tourne le modèle de fondation, dans le service informatique d’un ministère, dans une direction de l’innovation d’un hôpital, cette chaîne est documentée en détail : *diagrammes d’architecture*, *pipelines de données*, *journaux de traitement*. Mais pour l’agent de guichet qui consulte une file de dossiers triés, pour la médecin confrontée à une liste de patients “priorisés”, pour le juge qui reçoit un rapport chiffré, pour le recruteur ou l’équipe de modération qui voit un indicateur rouge, *cette cratialité est entièrement compacte* : un score, une couleur, un message, parfois une mention vague à un “outil d’analyse avancée”. L’épreuve de détectabilité conduit alors à remonter, autant que possible, cette chaîne invisible, en s’appuyant sur ce que les travaux existants documentent déjà.
|
||||
|
||||
### I.4.1. Des archives régulatrices recyclées en carburant statistique
|
||||
|
||||
La première strate de la *cratialité* de Système F, ce sont les *données*. Non pas des “matières premières” neutres, mais des *archives de décisions passées*.
|
||||
|
||||
Ainsi, avec SyRI, la littérature juridique montre que le système a été conçu comme un *instrument de “couplage” massif entre fichiers administratifs* : *données fiscales, registres de sécurité sociale, informations sur l’emploi, le logement ou la dette, issues d’un ensemble d’organismes publics qui, jusque-là, n’étaient pas nécessairement interconnectés*. L’objet n’était pas simplement de consulter l’un ou l’autre fichier, mais de constituer un “*dépôt de risque*” où les trajectoires de vie des habitants de certains quartiers étaient recomposées en *profils susceptibles de déclencher un contrôle*. Les dossiers saisis par les ONG et la décision de La Haye insistent sur ce point : c’est bien cette concentration, ce couplage étendu et asymétrique des données qui a conduit le tribunal à juger que le dispositif ne respectait pas la vie privée.
|
||||
|
||||
Dans le scandale des allocations pour la garde d’enfants, l’algorithme incriminé n’a pas été conçu *ex nihilo* : il reposait sur les dossiers fiscaux et sociaux accumulés par l’administration, sur les anciens contrôles, sur les historiques de remboursement, sur des métadonnées apparemment triviales (nationalité, double nationalité, type de crèche). Les enquêtes parlementaires et les analyses doctrinales montrent que la machine à profiler a, de ce fait, hérité d’une longue histoire de suspicion ciblée, pour la concentrer sur des familles à bas revenus, souvent issues de l’immigration.
|
||||
|
||||
Dans ce dispositif précis de santé étudié par Obermeyer et ses co-auteurs, la base d’apprentissage est constituée de données de facturation : coûts passés, diagnostics, passages à l’hôpital, prescriptions, etc. L’algorithme ne “voit” pas les patients, il voit les dépenses que le système de santé a consenties pour eux. Quand le dispositif est ensuite utilisé pour sélectionner les patients éligibles à des programmes de soins intensifs, la *cratialité* de Système F prolonge ainsi une économie historique des soins : ceux dont on a peu dépensé pour eux sont moins visibles pour l’algorithme, même s’ils sont plus malades.
|
||||
|
||||
Du côté pénal, les études sur COMPAS rappellent que les scores sont calculés à partir d’une combinaison d’éléments du casier judiciaire et des réponses à un questionnaire de 137 items, portant sur la trajectoire de vie, l’environnement social, l’emploi, la scolarité, le logement. Là encore, Système F ne “crée” pas les variables ; il hérite de la manière dont la police a arrêté, dont les tribunaux ont condamné, dont les services sociaux ont consigné des éléments de biographie, dans un contexte où ces histoires sont déjà fortement structurées par la race, la classe, le territoire.
|
||||
|
||||
Dans le recrutement, l’outil d’Amazon décortiqué par Reuters a été entraîné sur une décennie de décisions de recrutement dans des métiers techniques, essentiellement occupés par des hommes. Les CV, lettres, diplômes et trajectoires de carrière qui composent cette base reflètent un champ professionnel déjà fortement genré ; c’est cette archive que le modèle prend pour horizon de référence.
|
||||
|
||||
Enfin, sur les plateformes, les systèmes de modération et de recommandation ingèrent des flux continus de contenus, mais aussi des archives de signalements, de suppressions, de “likes”, de signalements de discours haineux ou terroristes. Les rapports de transparence imposés par le Digital Services Act et les documents publiés par Meta insistent sur le *rôle des systèmes automatisés dans la détection initiale des contenus, en se nourrissant précisément de cette mémoire d’interventions passées*.
|
||||
|
||||
Dans tous ces cas, la *cratialité* de Système F commence donc par un *geste de reprise* : les *bases de données sur lesquelles repose l’entraînement et le fonctionnement du système sont déjà des condensés d’arbitrages régulateurs, de contrôles et de sélections, parfois de longues routines discriminatoires*. La chaîne cratiale ne se contente pas d’“absorber” la réalité sociale ; elle absorbe des archives de pouvoir.
|
||||
|
||||
### I.4.2. Pipelines : transformer des vies en vecteurs
|
||||
|
||||
La deuxième strate de la *cratialité* est moins visible encore : ce sont les chaînes de transformation qui convertissent ces données hétérogènes en représentations exploitables par le modèle. Les manuels de science des données et les guides sur l’usage de l’IA dans les administrations décrivent des étapes désormais classiques : nettoyage, normalisation, sélection ou construction de variables, agrégation, puis vectorisation.
|
||||
|
||||
Dans le cas d’un dispositif de détection de fraude sociale, une succession de décisions apparemment techniques se met en place : faut-il coder la “nationalité” comme une variable binaire, ou comme une liste fine de pays ? Faut-il comptabiliser le nombre de déménagements sur cinq ans, ou sur dix ? Comment transformer des remarques écrites par des agents en indicateurs numériques ? Le rapport de la Commission d’enquête parlementaire néerlandaise sur le scandale des allocations montre que, dans la pratique, ces choix ont conduit à donner un poids particulier à certains marqueurs (double nationalité, erreurs mineures dans les formulaires), qui faisaient passer des familles entières du côté du “risque élevé”.
|
||||
|
||||
Pour l’*algorithme de santé* analysé par Obermeyer et al., cette chaîne consiste à *agréger des années de dépenses en santé, de diagnostics et d’hospitalisations en un score unidimensionnel de “risque”, censé représenter les “besoins futurs” du patient*. Le fait d’opter pour les coûts plutôt que pour des indicateurs cliniques, puis de compresser ces informations en un score continu, est un *geste cratial* autant que statistique : il *rend possible l’intégration du système dans les tableaux de bord des gestionnaires*, et *autorise un tri automatique des patients à échelle industrielle*.
|
||||
|
||||
Dans COMPAS, transformer 137 réponses et un casier judiciaire en un score de 1 à 10 suppose plusieurs *couches de prétraitement* : *recodage des réponses en catégories numériques*, *pondération de facteurs*, *combinaison en indices partiels*, puis en *score global*. Les études qui ont reconstruit partiellement la méthode à partir de données ouvertes montrent à quel point *cette chaîne incorpore des éléments contextuels* (code postal, stabilité résidentielle, entourage, historique de consommation de drogues) qui, une fois vectorisés, deviennent des *attributs apparemment neutres, mais fortement corrélés à des trajectoires socio-raciales différenciées*.
|
||||
|
||||
L’*outil de tri de CV* d’Amazon, pour sa part, devait convertir des documents richement formatés en vecteurs de caractéristiques : *universités*, *mots-clés*, *expériences*, *engagements*, parfois même *tournures de phrases*. C’est dans cette phase que le système a “*appris*” à pénaliser des expressions comme “*women’s chess club*” ou des références à des universités connotées féminines, parce que ces traits coïncidaient, dans les données d’entraînement, avec des candidatures historiquement moins retenues.
|
||||
|
||||
Enfin, les *pipelines de modération* et de *recommandation* transforment chaque message, image ou vidéo en une série de marqueurs : *langue, thème, tonalité, degré présumé de violence ou de haine, signaux de “fiabilité” de la source, etc.* Les documents de Meta et les analyses indépendantes montrent que ces *pipelines* combinent *détection automatisée*, *listes de mots*, *signaux issus de comportements passés*, avant de produire des étiquettes (“contenu à risque”, “contenu limite”) qui orientent la visibilité des publications bien en amont de toute décision humaine.
|
||||
|
||||
Ce que la lecture archicratique met ici en avant, ce n’est pas seulement la technicité de ces *pipelines*, mais leur *effet de réduction* : *des vies, des trajectoires, des plaintes, des prises de parole sont ramenées à des vecteurs, des classes, des scores*. Cette réduction est nécessaire pour que Système F fonctionne ; elle n’est pas en soi illégitime. Mais elle constitue l’un des lieux où la *cratialité* fait passer un seuil : *ce qui devient calculable devient gouvernable*.
|
||||
|
||||
### I.4.3. Modèles, paramètres, seuils : la mécanique fine des décisions
|
||||
|
||||
La troisième strate de la *cratialité* de Système F est celle des *modèles* et de leurs *réglages*. Les manuels de *machine learning* parlent *d’architectures, de fonctions de perte, d’optimisation, d’hyperparamètres*. Pour l’*archicratie*, ces notions deviennent intéressantes lorsqu’elles cristallisent des *choix régulateurs*.
|
||||
|
||||
Ainsi, *santé* étudié par Obermeyer et al., le modèle est paramétré pour *minimiser l’erreur globale de prédiction des coûts futurs*. Ce choix – minimiser une somme d’erreurs plutôt que de garantir un niveau de traitement minimal pour certains groupes – a pour effet d’optimiser la performance moyenne au prix d’une sous-protection systématique des patients noirs. Les auteurs montrent qu’en modifiant la fonction de coût pour intégrer explicitement des mesures de morbidité, le classement des patients noirs change radicalement.
|
||||
|
||||
Dans COMPAS, les paramètres du modèle et la manière dont les scores sont répartis en catégories (“faible”, “moyen”, “élevé”) déterminent la proportion de personnes qui basculent dans chaque classe de risque. Les études empiriques indiquent que, pour un même score, la probabilité de récidive est similaire entre accusés noirs et blancs, mais que la distribution des erreurs (faux positifs, faux négatifs) diffère fortement selon les groupes. L’architecture et les réglages du modèle correspondent donc à un compromis implicite : il est jugé acceptable de produire davantage de faux positifs pour certains groupes, afin de maintenir une calibration globale satisfaisante.
|
||||
|
||||
Les *systèmes de scoring* de fraude sociale fonctionnent de la même manière : la décision de fixer un seuil de déclenchement de contrôle à tel niveau plutôt qu’à tel autre se traduit immédiatement en nombre de dossiers réexaminés, en proportion de familles frappées par des procédures, en intensité de la surveillance sur certains quartiers. Les travaux juridiques sur SyRI insistent sur le fait que ce seuil, et les indicateurs qui y conduisent, n’étaient pas seulement inconnus du public, mais inaccessibles même aux personnes contrôlées.
|
||||
|
||||
Dans le cas d’Amazon, l’architecture exacte du modèle n’a pas été publiée, mais les sources indiquent qu’il s’agissait d’un système d’apprentissage supervisé qui attribuait une note d’une à cinq étoiles aux CV, en imitant les décisions de recrutement passées. La simple existence d’une échelle discrète de 1 à 5, avec un tri automatique des dossiers en fonction de cette note, traduit un choix cratial : il n’y a plus de lecture directe de chaque CV, mais un filtrage basé sur un signal synthétique, qui décide de ce qui est vu ou non par les recruteurs.
|
||||
|
||||
Dans tous ces cas, les modèles, les paramètres et les seuils ne sont pas seulement des composantes techniques ; ils sont les points précis où la chaîne cratiale se décide : *combien de personnes seront contrôlées, qui sera inclus dans un programme de soins, qui portera le stigmate d’un “haut risque”, qui sera vu par un recruteur ou par le public d’une plateforme*. La *cratialité* de Système F, c’est cette capacité à faire varier, par ajustement de quelques coefficients, la forme concrète de l’accès aux droits, aux ressources, à la visibilité.
|
||||
|
||||
### I.4.4. Interfaces : là où la *cratialité IA* rencontre la *cratialité humaine*
|
||||
|
||||
Pour les agents, juges, médecins, recruteurs, modérateurs, la *cratialité* n’apparaît cependant qu’à l’autre bout de la chaîne : dans les *interfaces*. C’est là que Système F devient visible – sous la forme d’une colonne de scores, d’un code couleur, d’un message d’alerte, d’une suggestion “recommandée par le système”.
|
||||
|
||||
Les études sur les systèmes d’aide à la décision clinique montrent que la manière dont une recommandation est affichée (alerte intrusive, message discret, couleur, possibilité de justification) influence fortement la propension des praticiens à la suivre ou à la contourner. Les travaux sur les biais d’automation confirment que, lorsque l’interface présente une proposition algorithmique avec un fort statut d’autorité (icône de validation, texte en vert, mention “recommandé”), les opérateurs ont tendance à lui accorder plus de crédit qu’à leur propre jugement, surtout dans des contextes de charge de travail élevée.
|
||||
|
||||
Transposé à Système F, cela signifie qu’un agent de caisse sociale, confronté à une liste de dossiers triés par un score rouge–orange–vert, sera incité à traiter en priorité les dossiers rouges, même si rien ne l’oblige formellement à suivre l’ordre proposé. Une médecin, devant une liste de patients ordonnés par un indice de “priorité” accompagné d’un avertissement lorsqu’elle s’en écarte, devra assumer de “désobéir” au système pour reclasser un patient. Un juge, lisant un rapport de risque pénal avec un score mis en avant et une série de formulations standardisées, sait que toute divergence devra être explicitée dans son jugement.
|
||||
|
||||
Les interfaces de plateformes, elles, ne montrent souvent rien du tout : l’utilisateur voit que sa publication “marche moins bien”, ou bien reçoit un message standardisé lui indiquant que son contenu a “enfreint les standards”, sans que la contribution exact de Système F soit explicitée. Les rapports de transparence exigés par le DSA commencent à donner des indicateurs agrégés sur la proportion de décisions automatisées, mais ils ne modifient pas cette expérience élémentaire : pour l’usager, la *cratialité* se résume à un refus, une baisse de portée, un retrait.
|
||||
|
||||
Du point de vue archicratique, ces interfaces sont des lieux décisifs : elles articulent la *cratialité IA* et la *cratialité humaine.* Selon la manière dont elles sont conçues – plus ou moins explicites, plus ou moins contraignantes, plus ou moins configurables – elles peuvent soit renforcer l’autorité de Système F au point d’en faire un quasi-oracle, soit ménager des marges de requalification, de contestation, de suspension. La matérialité du bouton, de la couleur, de l’alerte est ici pleinement politique.
|
||||
|
||||
### I.4.5. Procédures d’intégration : scripts d’usage et effets disciplinaires
|
||||
|
||||
Enfin, la *cratialité* de Système F se fixe dans les règles internes qui déterminent sa place dans les procédures de décision. Les études de cas sur l’IA dans les administrations et le droit de l’UE sur les décisions automatisées insistent sur ce point : la différence entre un système de “recommandation” et un système de “décision automatisée” tient souvent moins à la technique qu’aux scripts organisationnels qui encadrent son usage.
|
||||
|
||||
Dans un service de protection sociale, il peut être écrit que “les dossiers marqués à haut risque doivent être examinés en priorité” ; dans un tribunal, que “le score de risque ne peut être le seul fondement d’une décision, mais que toute divergence substantielle doit être motivée”. Dans un hôpital, qu’“un patient proposé par l’algorithme pour un programme de soins intensifs ne peut être exclu qu’après justification”; dans un service de recrutement, que “seuls les CV classés au-dessus d’un certain seuil seront examinés humainement”.
|
||||
|
||||
Chacune de ces règles transforme un score en quasi-obligation : l’agent, la médecin, le juge, le recruteur ne se contentent plus de consulter Système F ; ils doivent se positionner par rapport à lui, éventuellement s’en justifier. C’est là que se loge, souvent, l’“*humain dans la boucle*” invoqué par les chartes d’IA responsable : un humain reste dans la boucle, mais placé en position d’avoir à expliquer pourquoi il ne suit pas la recommandation, plutôt que de décider de manière primaire.
|
||||
|
||||
Les travaux sur SyRI et sur le scandale des allocations montrent en outre que ces scripts ne sont pas toujours explicités, ni même stabilisés : certains agents témoignent d’une pression implicite à suivre les signaux produits par le système, sous peine d’être jugés “laxistes” ou “inefficaces”. Dans le cas de COMPAS, la décision Loomis autorise l’usage de l’outil comme aide à la décision, mais sans offrir de critères clairs sur la manière dont les juges devraient articuler le score avec leur appréciation propre : la *cratialité* *se faufile ainsi entre prescription et simple “support”, en laissant la responsabilité dernière porter par les individus*.
|
||||
|
||||
Du point de vue de l’*archicratie*, cette couche procédurale est cruciale parce qu’elle fixe, en pratique, ce que “vaut” Système F : s’il doit être suivi par défaut, s’il peut être contredit, s’il déclenche automatiquement des contrôles ou des sanctions, s’il ouvre ou non un droit à un examen contradictoire. Dans de nombreuses configurations actuelles, cette valeur est déterminée par des circulaires internes, des guides utilisateurs, des formations *ad hoc*, rarement débattus sur une scène publique.
|
||||
|
||||
### I.4.6. Une *cratialité hypertopique*
|
||||
|
||||
*Reprise des archives régulatrices, pipelines de transformation, modèles et paramètres, interfaces, scripts d’usage* : si l’on assemble ces strates, la *cratialité* de Système F apparaît comme une *chaîne dense, continue, extraordinairement efficace*. Elle fait circuler des signaux depuis les bases de données jusqu’aux décisions prises au guichet, au tribunal, à l’hôpital, dans l’entreprise, sur la plateforme. Elle est fortement topologisée – située dans des centres de données, des services informatiques, des consoles administratives –, mais elle ne dispose pas, dans la plupart des cas, de lieux où elle se montre comme telle.
|
||||
|
||||
Pour les personnes affectées – bénéficiaires, patients, justiciables, candidat·es, usagers – la *cratialité* de Système F se présente avant tout comme une force “*hypertopique*” : un *vecteur d’effets sans visibilité de sa propre structure*. On peut ressentir ses conséquences (un contrôle inattendu, une radiation, un refus de prestation, une incarcération prolongée, une non-sélection, un contenu invisibilisé), sans jamais pouvoir désigner précisément le “*par quoi et comment*” qui a conduit à la situation.
|
||||
|
||||
L’*enjeu de l’épreuve de détectabilité*, appliquée à la *cratialité*, est dès lors double. D’une part, *rendre descriptible cette chaîne, en montrant qu’elle n’est ni magique ni diffuse, mais composée de décisions localisées, techniquement et institutionnellement situées*. D’autre part, *ouvrir la possibilité d’un déplacement : faire exister des scènes où cette cratialité peut être exposée, discutée, reconfigurée, et ne plus opérer de manière invisible*.
|
||||
|
||||
C’est à cette condition seulement que la puissance calculatoire de Système F peut être insérée dans un ordre archicratique — c’est-à-dire dans un ordre où la manière dont le pouvoir prend forme dans les dispositifs reste, elle aussi, amenée à l’épreuve.
|
||||
|
||||
## I.5. Archicration existante mais lacunaire
|
||||
|
||||
Après l’*arcalité déclarée et implicite*, après la *cratialité* de Système F, reste à interroger ce qui tient lieu, aujourd’hui, d’*archicration* : des *scènes d’épreuve où l’on pourrait amener la régulation algorithmique en visibilité, la contester, la transformer*. Dans la grammaire de la thèse, une archicration n’est pas un simple “dispositif de contrôle” : c’est un *lieu institué où les fondements (arcalité) et les instruments (cratialité) peuvent être mis en discussion par des acteurs concernés, dans des formes réglées, avec des effets possibles sur l’architecture du système*.
|
||||
|
||||
À première vue, l’écosystème de Système F semble en être riche : *comités d’éthique de l’IA*, *conseils de gouvernance des données*, *audits de biais*, *autorités de protection des données*, *agences sectorielles*, *juges*, *mécanismes de recours*, désormais complétés par les *obligations de transparence et de plainte du Digital Services Act*, par des lois sectorielles comme le *Local Law 144* de New York sur les outils automatisés de recrutement, ou par des dispositifs singuliers comme le *Meta Oversight Board*.
|
||||
|
||||
Mais, dès qu’on reformule les questions dans les termes archicratiques — *qui peut voir quoi ? qui peut contester quoi ? avec quels effets sur le système lui-même ?* — le paysage se transforme. Beaucoup de ces dispositifs produisent des avis, des rapports, des sanctions ponctuelles, des formulaires de recours ; très peu organisent une véritable comparution de Système F devant ceux qu’il affecte.
|
||||
|
||||
### I.5.1. Cartographie rapide des prétendants à la scène
|
||||
|
||||
On peut, pour commencer, distinguer quatre grandes familles de dispositifs qui, chacune à leur manière, prétendent jouer un rôle d’instance d’épreuve pour l’IA :
|
||||
|
||||
1. *Comités, chartes, conseils d’experts*
|
||||
|
||||
Comités internes d’éthique de l’IA dans les grandes entreprises technologiques, commissions *ad hoc* dans les administrations, groupes d’experts de haut niveau comme celui qui a élaboré les Lignes directrices pour une IA digne de confiance au niveau européen. Ils produisent des principes, des recommandations, des “bonnes pratiques”.
|
||||
|
||||
2. *Audits et évaluations techniques*
|
||||
|
||||
Audits de biais sur les outils de recrutement imposés par le Local Law 144 à New York (obligation de réaliser un audit annuel, de publier un résumé, d’informer les candidats).
|
||||
|
||||
Évaluations d’impact sur les droits fondamentaux ou sur les risques, demandées par certains régulateurs et expérimentées dans le cadre de l’AI Act européen et de rapports comme *Algorithmic Rule* ou le *Handbook: AI and Public Administration*.
|
||||
|
||||
3. *Autorités de régulation et juridictions*
|
||||
|
||||
Autorités de protection des données, conseils pour l’égalité et organismes antidiscrimination, autorités sectorielles, institutions européennes (Commission, FRA, etc.) qui ont enquêté sur les systèmes de profilage dans le social, la police ou la fiscalité.
|
||||
|
||||
Cours nationales et européennes, comme le tribunal de La Haye dans SyRI, ou la chaîne de procédures qui a suivi le scandale des allocations familiales aux Pays-Bas.
|
||||
|
||||
4. *Voies de recours et mécanismes de réclamation*
|
||||
|
||||
Droit au recours des allocataires, des justiciables, des patients, des candidats à l’emploi, des utilisateurs de plateformes.
|
||||
|
||||
Mécanismes internes de plainte et de contestation imposés par le Digital Services Act (obligation pour les grandes plateformes de prévoir des procédures de traitement des signalements et des recours, de publier des rapports annuels sur la modération, en précisant notamment la part de décisions automatisées et les taux d’erreurs).
|
||||
|
||||
Dispositifs spécifiques comme le *Meta Oversight Board*, qui réexamine un nombre limité de décisions de modération emblématiques et publie des décisions motivées et des recommandations.
|
||||
|
||||
Dans ce maillage, les éléments d’une *archicration authentique* sont présents : *lieux de délibération, expertises, procédures contradictoires, sanctions possibles*. Mais leur articulation et leur accessibilité restent profondément inégales. Surtout, la plupart de ces dispositifs s’adressent avant tout aux organisations et aux concepteurs, beaucoup moins aux personnes directement affectées par Système F.
|
||||
|
||||
### I.5.2. Comités et chartes : scènes sans publics
|
||||
|
||||
Les comités d’éthique de l’IA et les groupes d’experts ont joué un rôle central dans la formulation des grands principes qui structurent l’*arcalité déclarée des systèmes* — équité, transparence, robustesse, responsabilité, etc. Les AI Principles de Google et Microsoft, les Lignes directrices européennes pour une IA digne de confiance, ou encore les rapports nationaux sur “l’IA et les libertés” en sont des exemples typiques.
|
||||
|
||||
Ces instances ont bien une dimension quasi archicratique : elles mettent en scène, dans un cercle de spécialistes, des questions de fond (“qu’est-ce qu’une IA digne de confiance ?”, “quels sont les risques majeurs pour les droits fondamentaux ?”). Elles produisent des textes publics, organisent des consultations, parfois invitent la société civile à réagir. Mais, du point de vue de Système F, elles restent à un niveau très général :
|
||||
|
||||
- elles ne se prononcent que rarement sur un système concret inséré dans des chaînes cratiales spécifiques (fraude sociale, tri de CV, gestion des risques de santé, etc.) ;
|
||||
|
||||
- elles ne réunissent qu’à la marge les personnes directement affectées par ces dispositifs (allocataires, patients, justiciables, candidats, usagers) ;
|
||||
|
||||
- elles n’ont pas, sauf exception, de pouvoir d’injonction ou de suspension sur les systèmes en question.
|
||||
|
||||
Autrement dit, ces comités produisent des scènes de discours normatif situées très en amont, mais ils n’organisent pas l’épreuve d’un Système F déterminé. Ils contribuent à la fondation discursive de l’“IA responsable”, sans pour autant mettre en visibilité la *cratialité effective* des dispositifs déjà déployés.
|
||||
|
||||
### I.5.3. Audits de biais et évaluations d’impact : scènes confinées, biaisées par les conflits d’intérêts
|
||||
|
||||
Une deuxième famille de dispositifs, plus proche des chaînes réelles de Système F, est celle des *audits de biais* et des *évaluations d’impact algorithmiques*. Dans de nombreux pays, cette famille est en plein essor : New York, avec le *Local Law 144*, impose des audits de biais pour les outils automatisés de recrutement ; le Canada a généralisé l’“*Algorithmic Impact Assessment*” pour les systèmes de décision automatisée dans l’administration ; des guides de bonnes pratiques, produits par des organisations internationales, des agences publiques et des *think tanks*, enjoignent désormais administrations et entreprises à “évaluer” leurs systèmes de profilage avant ou pendant leur déploiement.
|
||||
|
||||
Dans l’esprit, on pourrait croire tenir enfin une *archicration* structurée : un tiers examine un système, mesure ses effets, formule des recommandations, éventuellement sous le regard de l’autorité ou du public. Un rapport est produit, parfois publié ; des chiffres sont discutés ; des engagements d’amélioration sont pris. Mais dès qu’on regarde de près *qui audite, sur quoi, avec quelles marges de manœuvre*, apparaissent des tensions lourdes, qui tiennent moins de la sophistication statistique que de la *configuration des intérêts en présence*.
|
||||
|
||||
Premièrement, la plupart des régimes d’audit existants reposent sur un modèle classique de *relation client–prestataire*. C’est l’organisation qui déploie Système F – employeur, administration, plateforme – qui commande, finance et choisit son auditeur. Le *Local Law 144* de New York illustre bien cette logique : les employeurs doivent faire réaliser un audit annuel par un tiers “indépendant” de leurs outils de décision automatisée en matière d’emploi, et publier un résumé des résultats. Sur le papier, l’exigence d’indépendance est posée ; dans la pratique, rien n’empêche la constitution d’un marché de cabinets spécialisés dont la survie dépend de la capacité à produire des audits compatibles avec les attentes de leurs clients. Les premières analyses de ce régime soulignent un nombre limité d’audits effectivement réalisés, une tentation d’interpréter les exigences de manière minimaliste, et un *risque de* “*vice de conformité*” : *l’audit devient un examen du respect formel des prescriptions, non une épreuve substantielle du dispositif et de ses usages*.
|
||||
|
||||
Deuxièmement, la montée en puissance d’une véritable “industrie de l’audit de l’IA” introduit un *conflit d’intérêts structurel*. De *grandes firmes de conseil* – parfois les mêmes qui développent, vendent ou intègrent des solutions d’IA – *se positionnent comme auditeurs des systèmes qu’elles contribuent par ailleurs à diffuser*. Des organismes de normalisation, comme le British Standards Institution, ont explicitement mis en garde contre cette situation : un nombre significatif d’acteurs qui commercialisent des audits d’IA sont aussi producteurs de technologies, ce qui alimente des doutes sur leur indépendance et sur la rigueur des évaluations ; des initiatives de standardisation cherchent désormais à encadrer ces pratiques. Dans le même sens, les appels à des audits “holistiques” – qui évalueraient non seulement les performances techniques, mais aussi les présupposés normatifs, les effets sociaux et les mécanismes de gouvernance – insistent sur la *nécessité d’auditeurs* “*libres de tout conflit d’intérêts*”, *sans quoi la procédure se réduit à une validation de façade*.
|
||||
|
||||
Troisièmement, les *évaluations d’impact algorithmiques* mises en place dans le secteur public prolongent souvent, sous une forme plus sophistiquée, la *logique de l’auto-évaluation*. Lorsqu’un ministère ou une agence réalise lui-même “son” évaluation d’impact avant de déployer un système de profilage ou de tri, il se trouve en position de juger la pertinence d’un dispositif qu’il a conçu, financé, promu et qu’il espère présenter comme vecteur de modernisation. Les travaux pionniers sur les *Algorithmic Impact Assessments* insistent, à l’inverse, sur la nécessité de dispositifs véritablement contradictoires : participation forte des publics concernés, consultations publiques substantielles, possibilité pour des acteurs externes (ONG, chercheurs, journalistes) de demander des compléments ou de contester des évaluations jugées insuffisantes. Certaines analyses des cadres européens vont dans le même sens, en plaidant pour des droits d’accès aux données, aux modèles et aux documents de conception, faute de quoi aucun écosystème d’audit réellement indépendant ne peut émerger.
|
||||
|
||||
Si l’on recompose ces éléments dans notre langue archicratique, on voit se dessiner une *typologie de conflits d’intérêts qui affectent directement la scène d’épreuve*. Les conflits sont *financiers*, lorsque l’auditeur dépend économiquement, de manière répétée, du client qu’il est censé contrôler ; *organisationnels*, lorsque l’audit est confié à des structures internes, à des filiales ou à des partenaires stratégiques qui partagent les mêmes objectifs de déploiement ; *cognitifs*, enfin, lorsque audités et auditeurs appartiennent au même petit milieu technico-juridique, avec des catégories de pensée, des indicateurs et des horizons de pertinence largement communs. Dans ces trois cas, *l’instance censée jouer le rôle de tiers contradicteur se trouve, à divers degrés, alignée avec les intérêts et les cadrages de ceux qui conçoivent et exploitent Système F*.
|
||||
|
||||
Le cadrage même des audits accentue cette dérive. Les textes juridiques et les guides méthodologiques encouragent parfois une vision très étroite de l’objet audité. Le *Local Law 144*, par exemple, impose de mesurer des écarts de taux de sélection selon le genre et la “race/ethnicité” dans les outils de recrutement, mais ne couvre pas d’autres dimensions pourtant protégées par le droit (âge, handicap) ou manifestement pertinentes (origine sociale, statut migratoire, langue). Dans ce contexte, l’organisation a tout intérêt à limiter l’exercice à ce qui est strictement requis, à traiter l’audit comme une *check-list* de ratios, et à laisser hors champ les questions plus profondes de fonction de coût, de proxy ou de composition des jeux de données – c’est-à-dire précisément l’*arcalité implicite* que notre cas cherche à mettre au jour.
|
||||
|
||||
Au terme de cette séquence, les *audits de biais* et *évaluations d’impact* apparaissent comme des *archicrations tronquées*. Il y a bien, formellement, une scène : un rapport est rédigé, parfois rendu public ; des chiffres sont produits ; des recommandations sont formulées. Mais les personnes directement affectées par Système F – allocataires, justiciables, patients, candidat·es, utilisateurs de plateformes – en sont largement absentes, ou réduites au statut de “parties prenantes” abstraites ; les choix les plus déterminants (fonctions de coût, proxies, seuils, composition des jeux de données) restent souvent hors du périmètre audité, au profit d’indicateurs aisément mesurables ; les *conflits d’intérêts structurels*, enfin, minent la capacité de l’auditeur à assumer le rôle de *tiers contradicteur* que l’*archicration* exigerait.
|
||||
|
||||
Dans notre grammaire archicratique, ces dispositifs constituent donc des épreuves techniques sans scène véritable : l’algorithme est certes testé, mais la collectivité ne dispose pas d’un lieu institué où confronter les résultats, interroger les axiomes, contester les compromis retenus, exiger des transformations. L’audit remplit principalement une fonction de légitimation – “le système a été évalué” – plus qu’une fonction de mise en débat. Autrement dit : la *cratialité* de Système F est brièvement éclairée par quelques faisceaux d’expertise, mais l’*arcalité implicite* reste soustraite à la comparution, et la scène demeure largement capturée par ceux qui ont intérêt à maintenir le dispositif intact.
|
||||
|
||||
### I.5.4. Autorités et tribunaux : scènes fortes, mais rares et *ex post*
|
||||
|
||||
Les autorités de régulation et les juridictions offrent, à première vue, les formes les plus accomplies d’*archicration* : procédures contradictoires, auditions, décisions motivées, sanctions, parfois réparation.
|
||||
|
||||
L’arrêt SyRI du tribunal de La Haye est emblématique : le dispositif de profilage de fraude aux prestations y est décrit, mis en rapport avec l’article 8 de la CEDH, et finalement jugé disproportionné, en raison notamment du manque de transparence, du ciblage de quartiers défavorisés et de la difficulté pour les personnes profilées de contester le système.
|
||||
Dans le scandale des allocations pour la garde d’enfants, les enquêtes de l’Autorité de protection des données (AP), les rapports parlementaires et, finalement, la crise politique qui a conduit à la démission du gouvernement Rutte illustrent ce que peut être une scène d’épreuve à grande échelle : les pratiques de profilage, les critères utilisés (dont la double nationalité), les effets sur des milliers de familles sont mis au jour, décrits, condamnés, et donnent lieu à un vaste plan de compensation.
|
||||
|
||||
Au niveau européen, le DSA commence à être appliqué comme base juridique pour sanctionner des plateformes qui manquent à leurs obligations de transparence, comme dans le cas récent de l’amende infligée à X (anciennement Twitter) pour violation de ses devoirs de transparence et de lutte contre les “*dark patterns*”.
|
||||
|
||||
Ces scènes ont un effet archicratique réel : elles forcent les systèmes à comparaître, révèlent des pratiques jusque-là invisibles, imposent des réformes. Mais elles ont aussi des limites structurelles :
|
||||
|
||||
- Elles interviennent tard, après des années d’usage, lorsque les dommages sont déjà massifs, comme dans le *toeslagenaffaire* (surendettement, perte de logement, placement d’enfants).
|
||||
|
||||
- Elles restent focalisées sur certains aspects juridiques (vie privée, discrimination, transparence) sans pouvoir, à elles seules, reconfigurer l’ensemble de la chaîne cratiale de Système F.
|
||||
|
||||
- Elles donnent une place indirecte aux personnes affectées (plaignants, associations, ONG), mais ces dernières n’ont ni la maîtrise de l’agenda, ni la garantie que la logique même du modèle sera transformée.
|
||||
|
||||
On pourrait dire, en termes archicratiques, que ces procédures sont des *scènes de rattrapage* : elles produisent des effets puissants, mais elles ne transforment pas encore la régulation algorithmique en régime ordinaire de comparution. Système F n’y vient qu’en cas de crise, non comme un acteur continuellement justiciable.
|
||||
|
||||
### I.5.5. Recours individuels et plaintes : scènes fermées, réponses standardisées
|
||||
|
||||
Reste la question des recours : que peut faire, dans l’état actuel des choses, un individu ciblé par Système F ?
|
||||
|
||||
Dans les politiques sociales, un allocataire qui voit sa prestation suspendue ou refusée peut en principe exercer un recours administratif ou contentieux. Pourtant, comme l’ont montré les enquêtes sur le scandale néerlandais, ces voies ont été largement inopérantes face à des décisions massives et standardisées, fondées sur des profils de risque opaques. Des parents ont multiplié les recours individuels sans succès, jusqu’à ce que des journalistes, des parlementaires et des autorités de contrôle parviennent à ouvrir le scandale au niveau systémique.
|
||||
Le recours reste structuré comme si la décision avait été prise par un agent individuel, dans un dossier singulier ; il n’offre aucune prise pour contester la logique même du système de profilage.
|
||||
|
||||
Dans le recrutement, des candidats peuvent saisir les autorités anti-discrimination ou engager des actions en justice, comme dans les affaires récentes où des candidats ont attaqué des fournisseurs d’outils de tri automatisé pour discrimination raciale ou fondée sur le handicap.
|
||||
|
||||
Mais même les dispositifs les plus avancés, comme le *Local Law 144*, se concentrent sur le respect d’obligations de procédure (audit, transparence minimale), non sur l’ouverture d’une scène où les candidats pourraient discuter des critères incorporés dans l’outil. Une fois que l’employeur peut montrer qu’un audit a été réalisé et qu’un résumé est en ligne, la possibilité de contester la structure même de l’outil reste très limitée.
|
||||
|
||||
Pour les plateformes, le DSA impose l’existence de *mécanismes internes de réclamation* et, pour les très grandes plateformes, la *mise en place de systèmes de traitement des notifications de contenus illégaux et de plaintes contre les décisions de modération*, ainsi que des *mécanismes de règlement extrajudiciaire des litiges*.
|
||||
|
||||
En pratique, ces dispositifs prennent la forme de formulaires en ligne, de délais de réponse, de messages standardisés. Ils permettent parfois de corriger des erreurs manifestes (restauration d’un contenu, réouverture d’un compte), mais ils n’ouvrent presque jamais une discussion sur les critères de modération eux-mêmes. L’utilisateur reste face à une interface laconique ; le rôle de Système F dans la décision (score de toxicité, détection de désinformation, etc.) est rarement explicité.
|
||||
|
||||
Le *Meta Oversight Board* constitue une exception partielle : il *publie des décisions motivées*, *analyse la conformité des politiques de Meta aux droits humains*, *formule des recommandations publiques*, parfois très critiques, sur certains *aspects de la modération* et de la *hiérarchisation des contenus*.
|
||||
|
||||
Mais il ne traite qu’un nombre infime de cas, sélectionnés parce qu’ils soulèvent des questions emblématiques ; il n’a pas de pouvoir direct sur la conception des systèmes de recommandation ou sur l’ensemble des algorithmes qui régulent la visibilité. Sa scène est réelle, mais fortement débitée : quelques affaires par an, dans un océan de décisions automatisées quotidiennes.
|
||||
|
||||
Dans la santé, enfin, les patients disposent de droits d’accès à leur dossier et, dans certains pays, peuvent saisir des médiateurs ou des commissions d’éthique clinique. Les travaux sur l’algorithme d’Obermeyer et al. montrent que la prise de conscience de ses effets discriminatoires est venue de chercheurs en épidémiologie et en médecine, non de recours individuels de patients.
|
||||
Là encore, la scène d’épreuve reste centrée sur la relation médecin–patient ; Système F y apparaît, au mieux, comme un outil contextuel, rarement comme objet principal de la contestation.
|
||||
|
||||
On voit se dessiner un trait commun : les mécanismes de recours existants permettent de contester les effets (une suspension de prestation, une peine, un refus d’embauche, un retrait de contenu), beaucoup plus difficilement le dispositif qui les produit. Ils ouvrent surtout des scènes de réclamation, non des scènes d’*archicration*.
|
||||
|
||||
### I.5.6. *Archicrations fantômes* et *oblitération de la scène*
|
||||
|
||||
Si l’on rassemble ces éléments, le diagnostic archicratique sur l’“*archicration existante*” de Système F devient plus précis.
|
||||
|
||||
- Oui, il existe des *instances qui ressemblent à des scènes* : *comités d’experts, audits, autorités de régulation, tribunaux, mécanismes de plainte, organes comme l’Oversight Board*.
|
||||
|
||||
- Oui, certaines de ces *instances produisent des effets tangibles* : *arrêt de SyRI, révélation et compensation dans le scandale des allocations, sanctions financières sous le DSA, ajustements ou abandons de certains outils* (comme le système de recrutement d’Amazon).
|
||||
|
||||
- Mais, pour l’essentiel, ces *instances restent partielles, sectorisées, tardives et pauvres en participation directe des personnes affectées*.
|
||||
|
||||
Du point de vue de l’*archicratie*, cela signifie que :
|
||||
|
||||
- L’*arcalité* de Système F existe, mais elle demeure largement *fantomatique* : les fondements implicites (proxies, fonctions de coût, hiérarchies des erreurs) ne sont presque jamais mis en scène comme tels. Les grands principes d’“IA digne de confiance” ou de “lutte contre la fraude” sont proclamés, mais leurs traductions opératoires ne sont pas exposées devant ceux qu’elles engagent.
|
||||
|
||||
- La *cratialité* est puissante, finement articulée, mais *hypertopique* : elle concentre ses opérations dans des architectures techniques et organisationnelles peu visibles, qui produisent des effets massifs sans qu’il soit possible, pour un individu, de remonter aisément la chaîne du “*par quoi et comment*”.
|
||||
|
||||
- L’*archicration*, enfin, est *lacunaire* : elle se manifeste soit sous forme de procédures internes, d’audits, de comités qui ne sont pas de vraies scènes publiques ; soit sous forme de grandes affaires contentieuses ou de scandales médiatiques, qui jouent le rôle de scènes d’exception plutôt que d’instances ordinaires de mise à l’épreuve.
|
||||
|
||||
On peut, avec la thèse, parler ici d’*archicrations fantômes* : des dispositifs qui empruntent l’allure des scènes (commissions, formulaires, recours), mais qui ne disposent ni de la consistance, ni de l’ouverture, ni de la réflexivité nécessaires pour faire effectivement comparaître Système F. Ils maintiennent l’impression d’une possibilité de recours, sans organiser véritablement la confrontation des fondements, des instruments et des effets.
|
||||
|
||||
La première conclusion de l’épreuve de détectabilité est ainsi nette : dans l’état actuel des usages de Système F, la régulation algorithmique est, pour une large part, hors scène. Les scènes qui existent sont soit trop en amont (principes généraux), soit trop en aval (scandales, contentieux), soit trop étroites (audits techniques fermés, formulaires de plainte standardisés). La suite du cas d’étude consistera à replacer cette *oblitération archicratique* dans la longue histoire des régimes régulateurs, puis à examiner ce que pourrait signifier, pour un système d’IA de ce type, une véritable réouverture de la scène : non plus des reculs ponctuels, mais une politique explicite des épreuves, où Système F serait tenu de rendre des comptes, non seulement sur ses performances, mais sur les fondements et les formes de pouvoir qu’il met en œuvre.
|
||||
|
||||
##
|
||||
273
src/content/cas-ia/chapitre-2.mdx
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
title: "Chapitre II — Épreuve topologique"
|
||||
edition: "cas-ia"
|
||||
status: "application"
|
||||
level: 1
|
||||
version: "0.1.0"
|
||||
concepts: []
|
||||
links: []
|
||||
order: 30
|
||||
summary: ""
|
||||
source:
|
||||
kind: docx
|
||||
path: "sources/docx/cas-ia/Cas_Pratique-Archicratie_et_gouvernance_des_systemes_IA-Chapitre_2_Epreuve_Topologique.docx"
|
||||
---
|
||||
# II. Épreuve topologique : hypotopies, hypertopies, atopies des scènes IA
|
||||
|
||||
L’épreuve de détectabilité nous a permis de reconstituer, pour Système F, la distribution des trois prises archicratiques : *arcalités déclarées et implicites, cratialités en chaîne, archicrations rares et fragmentaires*. L’épreuve topologique déplace maintenant la focale : il ne s’agit plus seulement de savoir où se trouvent *arcalité, cratialité* et *archicration*, mais dans quels types de scènes elles se laissent – ou non – approcher. Elle interroge la configuration concrète des lieux où la régulation algorithmique apparaît, se dit, se discute, se justifie, se conteste. Autrement dit : non seulement *quoi* et *comment*, mais *où* et *avec qui*.
|
||||
|
||||
Dans la thèse, la topologie archicratique désigne cette manière de lire un ordre régulateur à partir de la forme de ses scènes : *synchrotopies*, quand l’*archicration tient ensemble, de façon relativement stable, des prises arcalitaires et cratiales en présence de publics divers* ; *hypotopies*, quand la *scène existe, mais sous une forme tellement appauvrie qu’elle n’offre presque aucune prise réelle* ; *hypertopies*, lorsque la *scène est concentrée dans quelques lieux fermés où se décident l’essentiel des orientations, loin des personnes affectées* ; *atopies*, enfin, lorsque des *dispositifs jouent théâtralement la scène* (consultations, boîtes à idées, *feedbacks* symboliques), *sans connexion effective avec les lieux de décision*. La topologie n’est donc pas un simple “plan” des espaces physiques ou numériques : c’est une cartographie des situations scéniques où le pouvoir régulateur accepte – ou refuse – de se rendre visible.
|
||||
|
||||
Or l’écosystème de Système F est typiquement un espace topologiquement différencié. D’un côté, des *scènes locales d’usage* : guichets transformés en interfaces, portails en ligne, tableaux de bord, applications mobiles, formulaires de recours, boutons “signaler” ou “noter”. *Ces scènes sont souvent les seuls lieux où les personnes affectées par Système F peuvent ressentir quelque chose de sa présence* : un score, un code couleur, un refus, une chute de visibilité, un message standardisé. Du point de vue archicratique, elles ressemblent à des *hypotopies* : ce sont bien des scènes – il y a une interface, parfois un droit de réclamation, un espace minimal d’adresse –, mais elles sont *pauvres en prises, déconnectées des lieux où se décident l’architecture du système, les choix de proxies, les fonctions de coût, les seuils*. Par endroits, elles basculent même dans l’*atopie* : *faux dialogues, consultations sans effet, boîtes à idées numériques dont les contributions ne remontent jamais vers les lieux de conception*.
|
||||
|
||||
À l’autre extrémité, Système F se cristallise dans des *scènes institutionnelles* où se jouent les décisions structurantes : *comités de pilotage, boards techniques, réunions de design, arbitrages budgétaires, cellules “d’innovation” au sein des ministères ou des grandes entreprises, cabinets de conseil et de prestataires*. Ce sont des scènes très denses en prises cratiales : on y discute des architectures, des choix de déploiement, des objectifs d’optimisation, des métriques de performance, de la conformité juridique, parfois même des enjeux d’acceptabilité sociale. Mais ces *scènes* sont *fermées* : la plupart du temps, n’y participent que des *experts techniques, des responsables hiérarchiques, des juristes et quelques représentants institutionnels*. Les personnes directement affectées par Système F – allocataires, patients, justiciables, candidats, usagers – n’y apparaissent que sous la forme d’“utilisateurs finaux”, de “profils de risque” ou de “cas d’usage”. Ces lieux relèvent de l’*hypertopie cratiale* : ce sont des s*cènes effectives, mais concentrées, saturées de pouvoir, inaccessibles pour ceux qui subissent les décisions qui y sont prises*.
|
||||
|
||||
Entre ces deux polarités, une troisième famille de scènes se dessine : les *scènes judiciaires et quasi-judiciaires*. Cours et tribunaux, autorités de protection des données, régulateurs sectoriels, instances comme l’*Oversight Board*, mécanismes de règlement extrajudiciaire des litiges instaurés par le Digital Services Act. Ce sont des lieux où Système F, ou certains de ses avatars, peuvent être introduits comme objet de litige : *refus de prestation sociale, décision automatisée contestée, modération de contenu jugée abusive, sélection algorithmique à l’embauche, pratiques discriminatoires en santé*. On y trouve des éléments majeurs d’*archicration* : *procédure contradictoire, possibilité de produire des preuves, décisions motivées, sanctions*. Mais ces scènes sont souvent tronquées du point de vue archicratique : le juge ou l’instance n’ont pas toujours accès aux paramètres, aux données, aux logs ; ils se heurtent au *secret commercial*, à l’*opacité technique*, à l’*indisponibilité de certaines informations*. L’algorithme apparaît alors dans la scène, mais partiellement : la chaîne cratiale reste, en grande partie, hors champ. La scène d’épreuve est réelle, mais incomplète.
|
||||
|
||||
L’épreuve topologique appliquée à Système F va consister à organiser ce paysage, non pas en ajoutant un vocabulaire supplémentaire, mais en qualifiant les formes scéniques où la régulation algorithmique se manifeste. Dans un premier temps (II.1), nous prendrons au sérieux les scènes locales d’usage : guichets, interfaces, formulaires, dispositifs de “feedback”. Nous montrerons comment elles combinent, le plus souvent, *hypotopies* (scènes pauvres en prises) et *archicrations fantômes* (recours et consultations sans prise sur la structure du système). Dans un second temps (II.2), nous déplacerons le regard vers les *scènes institutionnelles de conception et de pilotage*, pour caractériser ce que l’on peut appeler une *hypertopie cratiale* : une *concentration scénique du pouvoir de configuration, sous forme de réunions, de comités et de boards largement fermés aux publics concernés*. Dans un troisième temps (II.3), nous interrogerons les *scènes judiciaires et quasi-judiciaires* où l’IA apparaît dans les contentieux, en demandant jusqu’où ces scènes parviennent – ou non – à recomposer une *archicration* complète incluant l’accès aux données, aux modèles et aux traces d’exécution.
|
||||
|
||||
Au terme de cette épreuve, une synthèse topologique (II.4) permettra de rendre visible, sous une forme compacte, la manière dont Système F distribue ses scènes : lignes de guichet, interfaces numériques, comités techniques, tribunaux, chacune étant lue à travers les trois prises archicratique (*arcalité* / *cratialité* / *archicration*), le type topologique (*hypotopie, hypertopie, atopie*) et son *degré d’ouverture ou de fermeture*. L’objectif n’est pas de plaquer un schéma préexistant sur l’IA, mais de montrer, très concrètement, que ce type de dispositif tend à dégrader la scène : en multipliant les *pseudo-espaces d’expression sans effet, en concentrant la décision dans des hypertopies techniques, en laissant les arènes judiciaires lutter avec des objets partiellement invisibles*. C’est cette dégradation topologique que la suite du cas d’étude cherchera à caractériser et, surtout, à retourner : *que faudrait-il, pour que Système F soit inséré dans une topologie réellement archicratique des scènes ?*
|
||||
|
||||
## II.1. Scènes locales d’usage : *hypotopies* et *archicrations fantômes*
|
||||
|
||||
Là où Système F devient perceptible pour la plupart des personnes, ce n’est ni dans les *data centers*, ni dans les comités de pilotage, ni dans les rapports d’audit, mais dans des scènes beaucoup plus modestes : un écran de guichet, un portail en ligne, un SMS automatique, un formulaire de recours, un bouton “signaler”, une boîte de dialogue “évaluez votre expérience”. Ce sont des scènes, au sens strict : il y a une interface, une adresse possible, parfois un droit minimal de réponse. Mais ce sont des scènes pauvres, prises dans un rapport extrêmement dissymétrique avec les lieux où Système F est conçu, paramétré, déployé. Topologiquement, elles relèvent de l’*hypotopie* et, lorsqu’elles se contentent de simuler un dialogue sans prise réelle, de l’*atopie*.
|
||||
|
||||
### II.1.1. Guichets devenus interfaces : la scène réduite au formulaire
|
||||
|
||||
Dans les régimes de protection sociale que nous avons évoqués dans la Partie I, l’introduction de Système F ne supprime pas le guichet ; elle le transforme. Là où se trouvaient autrefois des bureaux, des agents, des piles de dossiers papier, on rencontre de plus en plus souvent des *interfaces* : *écran partagé entre l’agent et l’allocataire, portail sur lequel ce dernier doit déposer ses justificatifs, suivre l’avancement de son dossier, répondre à des notifications*.
|
||||
|
||||
Du point de vue de l’allocataire, la scène se réduit à une *série d’actions codifiées* : *remplir des champs, téléverser des documents, cliquer sur “valider”, consulter un statut* (“en cours d’instruction”, “refusé”, “suspendu”), *parfois recevoir un message standardisé*. Système F est présent, mais en creux : il se manifeste par l’*ordre d’apparition des dossiers*, la *vitesse de traitement*, un *score de risque invisible*, un *basculement automatique d’un statut à un autre*.
|
||||
|
||||
Du point de vue de l’agent de guichet, la scène a aussi changé : là où l’on triait les dossiers “à vue” ou selon des procédures explicites, l’écran présente désormais une *file de cas pré-classés*, souvent accompagnés de *codes couleur*, *de priorités*, *d’alertes*. L’agent peut parfois *ajouter une note*, *corriger un champ*, *signaler une anomalie* ; mais l’*architecture globale de la décision* (quels dossiers arrivent, dans quel ordre, avec quel niveau d’urgence) *lui échappe en grande partie*. La *cratialité* de Système F traverse la scène, sans y apparaître vraiment.
|
||||
|
||||
Topologiquement, cette situation correspond à une *hypotopie* : il y a bien une scène – des personnes présentes, des échanges, une interface, des décisions qui se prennent – mais elle est pauvre en prises sur la régulation algorithmique. L’allocataire ne voit ni les variables qui le caractérisent dans Système F, ni le score qui a déclenché un contrôle ; l’agent lui-même n’a souvent qu’un accès très partiel aux raisons techniques du classement. La scène sert à exécuter des décisions déjà pré-structurées ailleurs, non à interroger la logique qui les organise.
|
||||
|
||||
On pourrait imaginer, en théorie, un guichet où l’allocataire pourrait demander : “*quel rôle précis a joué le système automatique dans ma suspension ? quels critères ont été appliqués ? quels sont les taux d’erreur habituels ?*” En pratique, ces questions n’ont souvent pas de place dans la scène : ni l’interface, ni la formation des agents, ni les procédures n’ont prévu qu’on puisse les poser – et encore moins y répondre. L’*archicration reste hors champ*.
|
||||
|
||||
### II.1.2. Recours numériques : de la réclamation au simulacre d’*archicration*
|
||||
|
||||
Lorsque la décision est défavorable – une prestation suspendue, un dossier classé sans suite, un refus d’allocation, une radiation –, la scène se déplace vers les *procédures de recours*. Elles sont, de plus en plus, numérisées : *formulaires en ligne, espaces personnels où l’on peut “contester” une décision, champs de texte libre limité en nombre de caractères, cases à cocher pour indiquer un motif* (“erreur de calcul”, “changement de situation”, “décision injustifiée”, etc.).
|
||||
|
||||
Sur le papier, ces dispositifs matérialisent une forme d’*archicration* : ils offrent à la personne concernée une *possibilité de s’adresser à l’institution*, de *présenter ses arguments*, d’*obtenir une révision*. Mais, lorsqu’on les lit à travers la grille archicratique, beaucoup apparaissent comme des *archicrations fantômes*.
|
||||
|
||||
D’abord, parce qu’ils sont structurés pour traiter des réclamations individuelles sur le résultat, non pour ouvrir une discussion sur le dispositif. Le formulaire invite à dire “je n’aurais pas dû être suspendu”, “mes revenus ont été mal pris en compte”, “vous n’avez pas considéré tel document”, mais il n’offre aucune case, aucune catégorie, aucun canal pour dire : “l’algorithme qui m’a classé comme fraudeur repose sur des hypothèses inacceptables”, “la nationalité ne devrait pas être utilisée comme facteur de risque”, “les erreurs du système sont concentrées sur des personnes dans ma situation”. La scène est calibrée pour corriger des erreurs perçues comme accidentelles, non pour instruire des critiques sur la structure même de Système F.
|
||||
|
||||
Ensuite, parce que la trajectoire de ces recours est souvent opaque. Une fois le formulaire envoyé, l’allocataire reçoit un accusé de réception automatique, puis une réponse laconique confirmant ou non la décision initiale. *Le recours a-t-il été lu par un humain ? Par un second modèle ? Par un agent qui ne fait que vérifier la présence de certains justificatifs ? Les éléments soumis ont-ils une chance de remonter vers les équipes qui conçoivent et paramètrent Système F ?* La scène existe, mais elle est décrochée de la chaîne cratiale.
|
||||
|
||||
Enfin, parce qu’il n’y a pas, dans la plupart des cas, de *mise en publicité des recours* : ni statistiques agrégées sur le nombre de contestations liées aux décisions co-produites par Système F, ni analyses régulières des motifs de mécontentement, ni articulation explicite entre ces données et la reconfiguration des modèles. La *scène de recours reste cloisonnée*, sans devenir une scène où l’*arcalité implicite* et la *cratialité* pourraient être rejouées.
|
||||
|
||||
Du point de vue topologique, ces dispositifs relèvent d’une forme mixte : *hypotopiques, parce qu’ils offrent très peu de prises réelles sur la régulation algorithmique* ; et déjà partiellement *atopiques*, dès lors qu’ils se contentent de *jouer le rôle de “voix des usagers” sans que cette voix rencontre des lieux de décision*. La scène est là, mais comme *décor procédural*, non comme espace d’épreuve.
|
||||
|
||||
La scène de recours est donc globalement hypotopique : elle existe bien, mais avec une densité de prises tellement faible qu’elle ne peut presque jamais infléchir la régulation algorithmique. Il s’agit bien souvent au mieux d’une correction ponctuelle et discrète.
|
||||
|
||||
### II.1.3. Feedbacks, étoiles, likes, boutons “signaler” : l’atopie comme style
|
||||
|
||||
Un troisième type de scènes locales d’usage est celui des *dispositifs de feedback continu* : étoiles attribuées à un service, notation d’une interaction, boutons “j’aime” / “je n’aime pas”, icônes “signaler ce contenu”, “ce résultat est utile / ne l’est pas”, “résultats inappropriés”. Ils sont omniprésents dans les plateformes numériques, mais aussi de plus en plus dans les services publics et les applications professionnelles.
|
||||
|
||||
À première vue, ces dispositifs prolongent une ambition archicratique : *rendre les systèmes sensibles à l’expérience des usagers, intégrer en continu des retours, corriger les dérives*. L’utilisateur de Système F – bénéficiaire, patient, conducteur ou passager d’une plateforme de mobilité, client d’un service, internaute exposé à des contenus – se voit offrir un petit geste : cliquer sur une étoile, cocher une case, rédiger un bref commentaire, signaler un abus. *Autant de micro-prises qui donnent l’impression d’une scène toujours disponible*.
|
||||
|
||||
Mais là encore, la lecture topologique révèle souvent une *atopie* : *un usage du langage de la scène sans articulation réelle avec les lieux où la régulation se décide.* Dans les *systèmes de notation réciproque* (chauffeurs / passagers, vendeurs / acheteurs, travailleurs de plateforme / clients), l’étoile donnée par un individu est très rarement pensée comme un *acte de mise à l’épreuve d’une norme*. Elle est conçue comme un *signal quantifiable*, immédiatement intégré à un *score global* qui servira ensuite à ordonner des files d’attente, à attribuer des courses, à exclure des travailleurs jugés “peu fiables”. La *scène de feedback* n’est pas l’endroit où les critères de qualité de service se discutent ; elle est un *mécanisme de discipline diffuse* : chacun sait qu’il peut être “noté”, mais ne sait pas vraiment comment les notes sont agrégées, interprétées et utilisées.
|
||||
|
||||
Sur les plateformes de contenus, le bouton “signaler” promet au contraire une *capacité à faire remonter des problèmes* : contenu haineux, illégal, trompeur, dangereux. En pratique, l’utilisateur ne voit presque jamais ce qu’il advient de son signalement : celui-ci part dans une chaîne cratiale obscure – combinaison de filtres automatisés, de priorisations, de requalifications humaines – pour revenir, parfois, sous la forme d’un message standard (“nous avons examiné votre signalement et décidé de…”), sans explication sur les critères appliqués, sur la place exacte de Système F dans la décision, ni sur la manière dont ce signalement contribue à reconfigurer les modèles.
|
||||
|
||||
Sur les interfaces de certains outils d’IA, les boutons “pouce en haut / pouce en bas”, “utile / non utile”, “trop sévère / trop permissif” jouent un rôle similaire : ils promettent une *co-construction des comportements du modèle*, mais ne donnent ni visibilité sur la manière dont ces retours sont utilisés, ni possibilité d’élargir la scène à d’autres acteurs que l’utilisateur individuel. Là encore, nous sommes devant une scène minimale – un geste, un symbole, un canal – mais qui ne s’adosse à aucune *archicration* identifiable.
|
||||
|
||||
Ce qui caractérise ces dispositifs, du point de vue archicratique, c’est donc leur ambiguïté : ils sont présentés comme des instruments de participation, alors qu’ils fonctionnent surtout comme des capteurs supplémentaires dans la chaîne cratiale de Système F. Ils peuvent améliorer certains paramètres (réduire des erreurs manifestes, affiner des modèles de recommandation), mais ils ne créent pas de scène où les fondements et les effets du système seraient mis à l’épreuve avec les publics concernés. *Ce sont des scènes sans monde : des atopies*.
|
||||
|
||||
Lorsque le *feedback* se réduit à un geste symbolique sans trajectoire identifiable vers les lieux de décision, on ne se trouve alors plus seulement dans une *hypotopie*, mais dans une *atopie* : *une scène jouée pour elle-même, déconnectée des décisions effectives et des répercussions affectives*.
|
||||
|
||||
### II.1.4. *Hypotopies* et *archicrations fantômes* : première coupe topologique
|
||||
|
||||
Si l’on rassemble ces trois familles de scènes locales – *guichets devenus interfaces, recours numériques, dispositifs de feedback* –, un motif commun se dessine.
|
||||
|
||||
1. Elles sont *proches des personnes affectées* : c’est là que Système F est ressenti, au moment où un dossier bascule, où un contenu disparaît, où une notation tombe, où une décision est confirmée ou refusée.
|
||||
|
||||
2. Elles *offrent bien des formes de scène* : présence d’un agent, d’une interface, d’un canal de parole, d’un geste de notation ou de signalement.
|
||||
|
||||
3. Mais elles sont *décrochées des lieux où se configurent la cratialité et l’arcalité implicite du système* : les choix de proxies, de fonctions de coût, de seuils, de stratégies de déploiement, de politiques de modération ne sont presque jamais discutés ni requalifiés à partir de ce qui s’y joue.
|
||||
|
||||
Topologiquement, ces scènes sont donc *hypotopiques* : elles existent, mais avec un *très faible nombre de prises effectives sur la régulation*. Et lorsqu’elles promettent une participation forte (*feedback* continu, consultations en ligne, enquêtes de satisfaction, boîtes à idées numériques) sans offrir de trajectoire identifiable des contributions vers les lieux de décision, elles basculent dans l’*atopie* : il y a bien un théâtre, des gestes, un vocabulaire de la co-construction, mais pas de connexion stable avec la chaîne cratiale qui la rend effective.
|
||||
|
||||
C’est ce déficit topologique – cette combinaison de scènes pauvres en prises et de pseudo-scènes sans pouvoir réel – qui explique, pour une part, la persistance d’*archicrations fantômes* autour de Système F. La suite de la Partie II montrera que cette pauvreté n’est pas un simple problème “local” à corriger par quelques améliorations d’interface : elle est le reflet d’une configuration d’ensemble où la scène, au sens archicratique, est structurellement reléguée.
|
||||
|
||||
## II.2. *Scènes institutionnelles* : *hypertopie cratiale*
|
||||
|
||||
Les *scènes locales d’usage* donnent à voir Système F sous forme de *reflets* : un écran de guichet, un portail de recours, un bouton “signaler”. La véritable scène, celle où se combinent les choix de fondement et les décisions opératoires, se déplace ailleurs : dans des *salles de réunion*, des *comités de pilotage,* des *“boards” responsables de l’IA,* des *cellules de conformité ou d’innovation*. C’est là que se décide le *design du modèle*, le *périmètre des données*, les *fonctions de coût*, les *seuils*, les *conditions de déploiement*, les *mécanismes de supervision humaine*. Du point de vue topologique, ces lieux concentrent une *densité exceptionnelle de prises cratiales* et, de plus en plus, d’*énoncés arcaux explicites* – *principes, standards, matrices de risques*. Mais ils sont *fermés* : les publics affectés n’y sont presque jamais présents, ni même représentés autrement que sous la forme de “*personae*”. Ce sont des *hypertopies cratiales*.
|
||||
|
||||
### II.2.1. Là où Système F se décide : comités, *workshops*, “*steering boards*”
|
||||
|
||||
Dans une caisse sociale qui envisage d’intégrer un module de Système F pour prioriser les contrôles, la scène institutionnelle typique prend la forme d’un *comité projet* : autour de la table, des responsables métiers (fraude, prestation, contrôle), des informaticiens, un juriste spécialisé en protection des données, parfois un représentant de la direction de la stratégie ou de l’inspection interne. C’est dans cette configuration que l’on discute des “*cas d’usage*” envisagés, des *sources de données* que l’on estime mobilisables, des *critères de risque* jugés pertinents, des *modalités de “pilotage”* (périodes de test, indicateurs de performance, seuils de déclenchement des contrôles).
|
||||
|
||||
Un schéma comparable se retrouve dans un hôpital qui veut déployer un outil de tri des patients à haut risque, dans un ministère de la justice qui envisage un système d’aide à la décision pénale, ou dans une grande entreprise qui souhaite industrialiser l’utilisation de Système F pour le tri des candidatures. Dans tous ces cas, *le cœur de la décision se situe dans des réunions internes* où l’on *arbitre entre plusieurs architectures possibles*, où l’on *discute des proxies envisageables* (historique de coûts, variables socio-démographiques, signaux comportementaux), où l’on *fixe des seuils de déclenchement et des règles d’escalade*. Les personnes dont les trajectoires seront directement affectées – allocataires, patients, justiciables, candidats – sont absentes ; au mieux, elles sont présentes sous forme de *catégories* (“usagers vulnérables”, “publics à risque”, “talents”) ou de *statistiques*.
|
||||
|
||||
Cette structure se retrouve, à un autre niveau, chez les fournisseurs de Système F. Les grands acteurs du *cloud* et des modèles de fondation ont mis en place des comités internes de gouvernance de l’IA : chez Microsoft, le comité *Aether* (*AI, Ethics, and Effects in Engineering and Research*) conseille la direction sur les risques éthiques, juridiques et sociétaux, appuyé par un standard interne de “*Responsible AI*” que les équipes produits doivent respecter. Google décrit un processus de gouvernance couvrant le développement du modèle, le déploiement des applications et la surveillance post-lancement, avec des comités formels qui examinent les nouveaux projets au regard de ses *AI Principles*, complétés par des exercices de *red teaming* et des *revues croisées*.
|
||||
|
||||
Dans ces scènes, les décisions structurantes sur Système F sont prises : *quel type de modèle sera proposé par API, avec quels garde-fous, quelles limitations de cas d’usage ; quels critères de “sensibilité” imposent une revue approfondie* ; *quelles demandes de clients (ministères, banques, hôpitaux) doivent être acceptées, négociées, refusées*. Là encore, les experts présents sont nombreux – ingénieurs, juristes, spécialistes de la conformité, parfois chercheurs en sciences sociales – mais les publics concernés sont absents en tant que tels.
|
||||
|
||||
Topologiquement, on voit apparaître un *déplacement de la scène* : ce n’est plus au guichet, dans la salle d’audience, au bureau de recrutement que Système F se décide, mais dans ces *arènes internes où se tissent ensemble la stratégie, le droit, la technique, le marketing*.
|
||||
|
||||
### II.2.2. Une gouvernance annoncée “*responsable*” comme *scène saturée* de *cratialité*
|
||||
|
||||
Pour comprendre le caractère *hypertopique* de ces scènes institutionnelles, il faut prendre au sérieux la montée en puissance de la *gouvernance “responsable” de l’IA*. Au cours des dernières années, une littérature abondante a proposé des cadres de gouvernance articulant *principes éthiques*, *structures d’organisation* (comités, responsables IA, cellules d’accompagnement) *et procédures* (revues de risques, audits internes, évaluations d’impact).
|
||||
|
||||
Les grands fournisseurs de Système F ont intégré ces cadres en interne. Microsoft insiste sur le fait que son *Responsible AI Standard* s’applique à toutes les équipes produits, que les cas d’usage “sensibles” doivent être portés devant des groupes de travail spécialisés, et que le comité *Aether* peut, en dernière instance, se prononcer sur des projets à haut risque, en lien direct avec la direction. Google met en avant des comités formels chargés de vérifier que les projets respectent ses *AI Principles*, qui couvrent notamment les questions de bénéfice social, d’évitement des biais injustes, de sécurité et de responsabilité.
|
||||
|
||||
Dans le secteur public, des dispositifs analogues apparaissent : le gouvernement canadien a rendu obligatoire un *Algorithmic Impact Assessment* pour tout système de décision automatisée, sous la forme d’un questionnaire structuré qui détermine un niveau de risque et déclenche des exigences de gouvernance (revue par des comités, publication de résumés, documentation renforcée). Des organisations internationales et des ONG ont proposé des typologies de mécanismes d’“*algorithmic accountability*” — *responsabilité algorithmique* — dans le secteur public (audit, évaluations d’impact, transparence, consultations), qui se traduisent souvent par la *création de cellules transversales et de comités d’examen des projets*.
|
||||
|
||||
Ces structures incarnent une *cratialité scénique* : elles sont des lieux où des personnes se réunissent, où des dossiers sont présentés, où des avis sont rendus, où certains projets sont acceptés, d’autres renvoyés, parfois abandonnés. Elles ne sont pas de simples scripts : ce sont des scènes où la trajectoire de Système F est effectivement infléchie. Mais elles fonctionnent selon une logique d’*hypertopie* :
|
||||
|
||||
- elles *concentrent un grand nombre de prises* – définition des cas d’usage autorisés, choix des métriques de risque, arbitrages entre principes, décisions de go/no-go – dans un petit nombre de lieux fermés ;
|
||||
|
||||
- elles *organisent l’accès à ces scènes selon des critères internes* (appartenance à l’entreprise, à l’administration, à une communauté de pratique) ;
|
||||
|
||||
- les publics affectés ne sont présents qu’à travers des proxies (études d’impact, “voix de l’utilisateur” reportée par des intermédiaires), rarement comme participants directs ;
|
||||
|
||||
- elles se *situent à l’interface entre la stratégie* (marchés, opportunités, cas d’usage), le *droit* (conformité au RGPD, à l’AI Act, aux directives sectorielles), et la *technique* (choix de modèles, d’architectures, de paramètres), de sorte que c’est là que s’articulent désormais *arcalité déclarée* (principes, chartes) *et cratialité* (instruments, procédures).
|
||||
|
||||
Autrement dit, la gouvernance “responsable” de Système F n’est pas extérieure à la *cratialité* : elle en est une couche supplémentaire, qui déplace vers ces comités le pouvoir de décider ce qui est “acceptable”, “conforme”, “aligné avec les principes”. *La scène existe, mais elle est réservée aux experts.*
|
||||
|
||||
### II.2.3. *Hypertopie régulatoire* : AI Act, autorités, AI Office
|
||||
|
||||
À cette gouvernance interne s’ajoute une couche régulatoire qui, elle aussi, se concrétise dans des scènes institutionnelles fermées ou semi-fermées. L’AI Act européen organise un dispositif de gouvernance articulant un AI Office au sein de la Commission, des autorités nationales de surveillance du marché, des comités consultatifs d’experts et des mécanismes de coordination entre États membres.
|
||||
|
||||
Pour les systèmes d’IA considérés comme “à haut risque”, les fournisseurs doivent mettre en place un système de gestion des risques couvrant tout le cycle de vie, documenter leurs modèles, assurer une gouvernance des données (représentativité, qualité, absence d’erreurs dans la mesure du possible), garantir des mécanismes de supervision humaine efficaces, tenir des journaux d’événements, et coopérer avec les autorités.
|
||||
|
||||
Concrètement, ces obligations donnent lieu, dans les entreprises et les administrations, à la constitution de structures dédiées : responsables produit pour l’IA, unités de conformité, “*AI governance boards*” qui suivent l’état d’avancement des projets, examinent les dossiers techniques, préparent les échanges avec les régulateurs. Des guides récents pour la gouvernance de l’IA recommandent de définir des principes internes, de documenter l’ensemble des politiques relatives au design, au déploiement et à l’exploitation des modèles, et de coordonner ces efforts sous une instance de supervision dédiée.
|
||||
|
||||
Là encore, la scène est réelle : des équipes se réunissent pour remplir des questionnaires d’évaluation d’impact, préparer des audits, décider si un cas d’usage tombe ou non dans une catégorie de risque, définir le périmètre des journaux à conserver, négocier avec les autorités sur la qualification d’un système ou la proportionnalité des obligations. Ce sont des lieux où l’architecture de Système F est littéralement “*mise en forme*” *pour répondre aux cadres juridiques*.
|
||||
|
||||
Mais cette *hypertopie régulatoire* ne corrige pas spontanément le déficit archicratique des scènes locales. Elle peut renforcer, dans certains cas, l’exigence de documentation et de supervision humaine ; elle peut créer de nouveaux motifs de contentieux, comme on le voit déjà avec les premiers litiges autour de l’AI Act ou des obligations de transparence pour les plateformes.
|
||||
Ce qu’elle ne fait pas, par elle-même, c’est ouvrir ces scènes aux publics affectés : les *procédures restent principalement l’affaire des fournisseurs, des intégrateurs, des régulateurs, éventuellement de quelques représentants de la société civile intégrés à des groupes d’experts*. Les allocataires, les patients, les justiciables, les candidats n’apparaissent qu’indirectement, via des évaluations d’impact ou des consultations ponctuelles.
|
||||
|
||||
### II.2.4. Lecture topologique : la scène archicratique capturée par l’*hypertopie*
|
||||
|
||||
Du point de vue de l’épreuve topologique, ces *scènes institutionnelles* présentent une configuration paradoxale.
|
||||
|
||||
D’un côté, elles *rassemblent ce qui manque aux scènes locales* : ici, les fondements sont explicitement discutés (au moins sous la forme de principes et de matrices de risques), les instruments sont détaillés (modèles, données, paramètres), des arbitrages sont opérés, des décisions sont prises qui engagent la trajectoire de Système F. Ce sont, au sens archicratique, des scènes potentielles : on y parle de ce qui fonde, de ce qui opère, de ce qui est acceptable.
|
||||
|
||||
De l’autre, elles sont fermées : la liste des participants est limitée, les publics concernés ne sont présents que sous forme d’abstractions, les conflits de normativité (par exemple entre les intérêts économiques du fournisseur, les objectifs politiques de l’administration et les droits des personnes affectées) sont réglés dans un cercle restreint. L’information circule de manière asymétrique : les expériences des scènes locales (erreurs, injustices, effets pervers) remontent peu, sauf lorsqu’elles éclatent en scandales ou en contentieux ; les décisions prises dans ces hypertopies redescendent sous la forme de modèles “prêts à l’emploi”, de paramètres par défaut, de seuils automatisés, de formulaires de recours formatés.
|
||||
|
||||
On peut dire, en reprenant la terminologie de la thèse, que la scène archicratique a été capturée par l’hypertopie cratiale. Au lieu de s’organiser autour d’espaces où les différents mondes concernés par Système F se rencontreraient (bénéficiaires, praticiens, concepteurs, régulateurs, chercheurs, associations), la mise en forme du pouvoir algorithmique se joue dans des cénacles d’experts spécialisés, au croisement de la technique, du droit et de la gestion. La densité de prises y est maximale ; la pluralité des publics y est minimale.
|
||||
|
||||
Ce constat ne signifie pas que ces scènes seraient inutiles ou purement cyniques : elles sont indispensables à la mise en conformité, à la réduction de certains risques, à la prise de conscience interne des enjeux. Mais, tant qu’elles restent organisées comme des hypertopies fermées, elles ne constituent pas des archicrations au sens plein : elles ne rendent pas Système F justiciable devant ceux qu’il affecte, elles ne transforment pas la gouvernance de l’IA en scène de confrontation des arcalités et des cratialités.
|
||||
|
||||
La Partie II montrera que c’est précisément dans la tension entre ces hypertopies cratiales et les hypotopies/atopies des scènes locales que se dessine le paysage topologique propre aux systèmes d’IA contemporains : un paysage où la scène existe, mais où elle est à la fois concentrée (dans quelques arènes expertes) et dégradée (dans les lieux ordinaires où se jouent les vies).
|
||||
|
||||
## II.3. Scènes judiciaires et quasi-judiciaires
|
||||
|
||||
Les *scènes judiciaires et quasi-judiciaires* sont, en principe, les lieux par excellence de l’*archicration* : *espaces réglés où une décision peut être contestée, où des preuves sont produites, où des arguments s’affrontent, où une instance tranche en motivant son jugement*. Si Système F devait être mis en cause quelque part, ce serait ici : lorsqu’un refus de prestation sociale est contesté, lorsqu’une décision automatisée est attaquée, lorsqu’une suspension de compte ou un retrait de contenu est porté en justice ou devant une autorité indépendante.
|
||||
|
||||
De fait, les systèmes d’IA et, plus largement, les dispositifs algorithmiques, apparaissent de plus en plus souvent dans ces scènes : dans les litiges autour des dispositifs de profilage social néerlandais, dans les recours contre des décisions automatisées au titre du RGPD, dans les affaires de modération de contenus tranchées par des tribunaux nationaux ou par l’*Oversight Board* de Meta, dans les procédures ouvertes par des autorités contre des plateformes qui manquent à leurs obligations de transparence ou de diligence. Mais, lorsque l’on regarde ces scènes à travers la grille archicratique, une question revient avec insistance : *le juge, ou l’instance quasi-judiciaire, voit-il vraiment Système F ? A-t-il accès aux paramètres, aux données, aux logs, aux arbitrages d’erreurs ?* Si la réponse est non ou partielle, la scène reste incomplète : l’*archicration est tronquée*.
|
||||
|
||||
### II.3.1. Contentieux sociaux : le système est jugé… tard, et par morceaux
|
||||
|
||||
Les affaires néerlandaises liées au profilage social sont exemplaires. Dans le’exemple de SyRI, le tribunal de district de La Haye a été saisi par une coalition d’ONG et de syndicats qui contestaient la conformité du système au droit au respect de la vie privée garanti par l’article 8 de la CEDH. Le jugement de 2020 décrit SyRI comme un *dispositif de croisement massif de données issues de multiples administrations, produisant des “rapports de risque” sur certaines zones ou populations, sans transparence suffisante ni garanties contre les discriminations*. Le tribunal conclut que la législation encadrant SyRI ne respecte pas le “*juste équilibre*” entre la lutte contre la fraude et la protection des droits, et en interdit l’usage.
|
||||
|
||||
Dans le scandale des allocations pour la garde d’enfants, ce sont des années de litiges individuels, de plaintes, de rapports de l’Autorité de protection des données, d’enquêtes parlementaires qui ont fini par mettre au jour l’ampleur des *pratiques de profilage* : utilisation de la double nationalité comme facteur de risque, concentration des contrôles sur certaines familles, impossibilité pour les parents de comprendre pourquoi ils étaient ciblés et sommés de rembourser. Les contentieux ont fini par prendre une dimension quasi-systémique, conduisant à la démission du gouvernement et à un vaste plan de compensation.
|
||||
|
||||
Ces *scènes judiciaires* ont un effet archicratique réel : elles *obligent l’administration à exposer, au moins partiellement, le fonctionnement de ses dispositifs* ; elles *rendent publics certains critères* ; elles *prononcent des condamnations* ; elles *engagent des réformes*. Mais elles interviennent tard, après la production de dommages parfois considérables, et elles restent souvent focalisées sur un segment de Système F (un système particulier de *scoring*, une catégorie de données, une base légale), sans reconstituer la chaîne cratiale entière.
|
||||
|
||||
Du point de vue topologique, ces scènes sont donc fortes, mais intermittentes : ce sont des moments où Système F est forcé d’apparaître, mais seulement lorsqu’un scandale ou un contentieux de grande ampleur éclate. L’*archicration reste événementielle, non structurelle*.
|
||||
|
||||
### II.3.2. Décisions automatisées et RGPD : une visibilité juridique sans visibilité technique
|
||||
|
||||
Le RGPD et les droits qu’il consacre – *droit d’accès, droit à l’explication, droit d’opposition, droit de ne pas faire l’objet d’une décision exclusivement automatisée produisant des effets juridiques significatifs* – fournissent un autre cadre d’apparition des systèmes d’IA sur la scène judiciaire. Des individus contestent des décisions en invoquant le caractère automatisé du traitement, l’absence d’information claire sur la logique des algorithmes, la difficulté d’exercer effectivement leurs droits.
|
||||
|
||||
Les autorités de protection des données ont commencé à instruire des dossiers où des systèmes de *scoring* ou de *profilage* sont au cœur du litige : *applications de notation sociale, systèmes de tri de candidatures, outils de ciblage publicitaire, plateformes de livraison ou de mobilité utilisant des algorithmes pour évaluer les performances et attribuer des tâches*. Dans certains cas, la justice est saisie après une décision de l’autorité, soit par les entreprises qui la contestent, soit par les plaignants qui estiment les mesures insuffisantes.
|
||||
|
||||
Mais, dans la pratique, ces scènes se heurtent à un obstacle récurrent : le *fossé entre la visibilité juridique du traitement et la visibilité technique* de Système F. Le RGPD permet à la personne concernée d’obtenir des “*informations utiles quant à la logique*” d’un traitement automatisé ; les autorités peuvent *exiger des explications détaillées* ; les juges peuvent *ordonner la communication de documents techniques*. Cependant :
|
||||
|
||||
- Les *informations fournies restent souvent très générales* (description de la finalité, liste de catégories de données, mention d’une utilisation de profilage), *sans entrer dans le détail des modèles, des proxies, des seuils, des arbitrages de coût*.
|
||||
|
||||
- Les *entreprises invoquent fréquemment le secret des affaires pour limiter la divulgation de certains éléments* (architecture exacte du modèle, paramètres, méthodes de calibration), *ce qui restreint les capacités d’expertise contradictoire*.
|
||||
|
||||
- Les *juges*, sauf à s’entourer d’experts techniques, *ne disposent pas toujours des outils pour interpréter les logs, les matrices de confusion, les rapports de validation qui leur seraient éventuellement communiqués.*
|
||||
|
||||
La scène judiciaire existe, mais Système F n’y est présent qu’en silhouette : on connaît sa finalité, on sait qu’il y a un algorithme, on discute de sa base légale et de ses effets, mais on ne reconstitue pas intégralement la *cratialité*. L’*archicration* est, là encore, tronquée : c’est moins l’architecture du système qui comparaît que ses conséquences et son habillage juridique.
|
||||
|
||||
### II.3.3. Modération de contenus, *Oversight Board* et DSA : scènes emblématiques mais parcellaires
|
||||
|
||||
Dans le domaine de la modération et de la curation de contenus, les *scènes quasi-judiciaires* se multiplient : *recours d’utilisateurs contre des suspensions de compte*, *actions en justice contre des plateformes pour sur-modération ou sous-modération*, *recours devant les autorités au titre du droit à la liberté d’expression* ou de la *protection contre les contenus haineux*, *procédures instruits dans le cadre du Digital Services Act*.
|
||||
|
||||
Le *Meta Oversight Board* occupe une position singulière : instance indépendante financée par un trust, composée d’experts et de personnalités diverses, chargée de réexaminer un petit nombre de décisions de modération de Facebook et Instagram, à la demande d’utilisateurs ou de la plateforme elle-même. Dans ces procédures, l’algorithme de recommandation ou les systèmes de détection automatique sont parfois explicitement mentionnés : ils ont pu déclencher une première étape de retrait, ou influencer la visibilité d’un contenu. Les décisions du Board analysent alors la conformité de la décision globale aux règles de la plateforme et aux standards internationaux des droits humains, et formulent des recommandations sur les politiques de modération et leur mise en œuvre.
|
||||
|
||||
Le DSA, de son côté, crée des *obligations de transparence et de diligence pour les très grandes plateformes* : publication de rapports sur les contenus retirés ou limités, description des systèmes de recommandation, mise en place de mécanismes internes de plainte et de règlements extrajudiciaires des litiges, audits indépendants. Les premières affaires instruites au titre du DSA montrent que la Commission et les autorités nationales entendent examiner la manière dont les mesures automatisées sont utilisées pour détecter, hiérarchiser ou supprimer des contenus, notamment lorsqu’il s’agit de discours politique, de désinformation ou de contenus ciblant des groupes vulnérables.
|
||||
|
||||
Ces dispositifs produisent des *scènes quasi-judiciaires* où les plateformes doivent expliquer – au moins partiellement – *comment leurs systèmes fonctionnent, pourquoi un contenu a été retiré ou maintenu, comment les utilisateurs peuvent contester ces décisions*. Mais ici aussi, la visibilité reste partielle :
|
||||
|
||||
- Les *décisions de l’Oversight Board* portent sur un très petit nombre de cas, choisis en fonction de leur importance symbolique ; elles n’offrent qu’un éclairage ponctuel sur la manière dont les algorithmes de Meta structurent l’exposition aux contenus.
|
||||
|
||||
- Les *rapports de transparence exigés* par le DSA agrègent des chiffres (nombre de contenus modérés, proportion de décisions automatisées vs humaines, volumes de recours) et décrivent les systèmes de manière très synthétique, sans exposer les paramètres concrets de Système F.
|
||||
|
||||
- Les *mécanismes de plainte et de règlement extrajudiciaire* permettent de corriger des décisions individuelles, mais ils ne s’accompagnent pas automatiquement d’une capacité pour les plaignants à déclencher une révision profonde des modèles.
|
||||
|
||||
L’impression, du point de vue archicratique, est celle de scènes emblématiques mais parcellaires : elles jouent un rôle de vitrine et peuvent générer des inflexions importantes, mais elles ne suffisent pas à constituer un régime ordinaire de comparution pour Système F.
|
||||
|
||||
### II.3.4. Un accès incomplet à la *cratialité* : logs, données, paramètres en pointillés
|
||||
|
||||
Ce qui traverse toutes ces scènes, c’est la question des conditions d’accès à la *cratialité*. Pour qu’une *archicration* soit complète, il ne suffit pas que la décision soit contestable en droit ; il faut que les instruments qui la produisent puissent être amenés sur scène dans leur *structure opérationnelle* : *données d’entraînement, variables choisies, fonctions de coût, seuils, logs d’exécution, métriques d’erreurs, modifications successives*.
|
||||
|
||||
Or la plupart des contentieux impliquant Système F se heurtent à des barrières récurrentes :
|
||||
|
||||
- *Secret des affaires et propriété intellectuelle* : les entreprises invoquent la protection de leurs secrets industriels pour refuser de divulguer certains éléments ; les juges doivent arbitrer entre cette protection et le droit à un procès équitable, sans toujours disposer de mécanismes robustes (experts tiers, accès limité mais réel, obligations de documentation approfondie).
|
||||
|
||||
- *Fragmentation de l’information* : même lorsque des éléments sont communiqués, ils le sont souvent par fragments – un descriptif de la finalité, une liste de variables, des extraits de code, un rapport d’audit interne – sans qu’un travail de reconstitution complète de la chaîne cratiale soit réalisé dans la procédure.
|
||||
|
||||
- *Capacités d’expertise* : juges, avocats, autorités, associations disposent de ressources inégales pour analyser les modèles, comprendre les rapports techniques, interpréter les logs ; les plaignants individuels, eux, n’ont généralement accès qu’à des informations très abstraites sur la “logique” du traitement.
|
||||
|
||||
- *Temporalité* : les systèmes évoluent rapidement ; au moment où un contentieux est arrivé à maturité, le modèle a parfois été modifié, remplacé, recalibré, ce qui complique la tâche de statuer sur un état donné de Système F.
|
||||
|
||||
Le résultat, du point de vue topologique, est que la *scène judiciaire* ou *quasi-judiciaire* voit Système F, mais comme à travers une vitre dépolie : on devine une architecture, on identifie certaines variables, on mesure des effets discriminatoires ou disproportionnés, mais la mécanique fine reste hors de portée. L’*archicration est ouverte en droit, mais entravée en fait.*
|
||||
|
||||
### II.3.5. Topologie d’une *archicration tronquée*
|
||||
|
||||
Si l’on replace ces *scènes judiciaires et quasi-judiciaires* dans la carte tracée par la Partie II, on obtient une image contrastée :
|
||||
|
||||
- Par rapport aux *hypotopies* des guichets et des interfaces, les tribunaux, autorités et instances quasi-judiciaires constituent un gain net : ils *offrent des procédures, des droits, des possibilités de mise en publicité, des décisions motivées, parfois des sanctions et des réparations*.
|
||||
|
||||
- Par rapport aux *hypertopies cratiales* des comités de gouvernance interne, ils *introduisent un élément extérieur, une mise en cause par des acteurs qui ne participent pas à la conception de Système F et peuvent, au moins en partie, l’obliger à se justifier*.
|
||||
|
||||
Mais ces scènes portent les marques d’une *archicration tronquée* :
|
||||
|
||||
- tronquée vers l’amont, faute d’un accès systématique aux choix de design, aux fonctions de coût et aux jeux de données qui structurent Système F ;
|
||||
|
||||
- tronquée vers l’aval, car les décisions de justice ou les recommandations quasi-judiciaires n’entraînent pas toujours une transformation profonde et durable des systèmes, mais plutôt des ajustements circonscrits, des promesses de réforme, des mesures de compensation.
|
||||
|
||||
Topologiquement, elles occupent une position intermédiaire entre l’*hypotopie* et la *synchrotopie* : des scènes fortes, mais fragmentaires ; des moments d’*archicration*, mais sans la continuité ni la profondeur nécessaires pour transformer l’ensemble de la configuration archicratique de Système F.
|
||||
|
||||
C’est sur cette base que la synthèse topologique (II.4) pourra être conduite : en montrant comment, entre les *hypotopies locales*, les *hypertopies cratiales* et ces *archicrations partielles*, se dessine un paysage où la scène n’est ni absente ni pleinement instituée, mais morcelée, déphasée, décalée par rapport aux lieux où la régulation algorithmique déploie effectivement ses effets.
|
||||
|
||||
## II.4. Synthèse topologique
|
||||
|
||||
L’épreuve de détectabilité avait permis de montrer comment Système F distribue, dans ses différents segments, *arcalités déclarées et implicites*, *cratialités en chaîne* et *archicrations lacunaires*. L’épreuve topologique révèle maintenant que cette distribution n’est pas homogène : elle prend la forme d’une dégradation structurée de la scène. Si l’on reprend les catégories introduites au chapitre 1 – synchrotopies, hypotopies, hypertopies, atopies – on constate que Système F se déploie dans un espace où la synchrotopie archicratique (scènes tenues, denses en prises, ouvertes à des publics pluriels) est quasiment absente, tandis que trois configurations dominent :
|
||||
|
||||
- *aux points de contact avec les personnes affectées* (guichets, interfaces, formulaires de recours, feedbacks), des *hypotopies* et des *atopies* : scènes pauvres en prises sur la régulation algorithmique, ou pseudo-scènes où l’on joue la participation sans prise réelle sur Système F ;
|
||||
|
||||
- *au centre de gravité organisationnel* (comités de pilotage, boards techniques, cellules de gouvernance de l’IA, unités de conformité), des *hypertopies cratiales* : scènes très denses en prises, mais fermées, réservées à des cercles d’experts et de décideurs ;
|
||||
|
||||
- dans les *arènes de mise en cause* (tribunaux, autorités, instances quasi-judiciaires), des *archicrations partielles* : scènes fortes mais fragmentaires, où Système F apparaît souvent en silhouette, faute d’accès complet à sa *cratialité*.
|
||||
|
||||
Autrement dit, le système IA typifié par Système F est bien un dispositif scénique, mais topologiquement dégradé. La scène n’a pas disparu ; elle a été déplacée, concentrée, morcelée. Les *hypotopies locales* rendent la présence de Système F sensible (un refus, un classement, un retrait, une notation), sans offrir de prise pour contester la manière dont ces effets sont produits. Les *hypertopies institutionnelles* prennent en charge la “*mise en forme*” du pouvoir algorithmique (design, paramétrage, gestion des risques), mais en maintenant les publics affectés à distance. Les *scènes judiciaires et quasi-judiciaires* rouvrent ponctuellement l’espace de contestation, sans parvenir à transformer cette intermittence en régime ordinaire de comparution.
|
||||
|
||||
Ce paysage répond exactement au diagnostic posé dans l’essai-thèse, concernant l’*autarchicratie* : un *méta-régime où la régulation tend à devenir son propre juge, où les appareils de calcul, de standardisation et de pilotage se ferment sur eux-mêmes, en reléguant la scène – au sens démocratique – à des marges appauvries (recours individuels, formulaires, feedbacks) ou à des épisodes de crise (scandales, contentieux emblématiques).* L’IA de fondation intégrée à des chaînes de décision publiques et privées ne crée pas *ex nihilo* cette configuration ; elle intensifie des tendances déjà à l’œuvre dans les méta-régimes techno-logistique, scripturo-bureaucratique et marchand : *externalisation des arbitrages dans des infrastructures techniques*, *multiplication de points de contact sans véritable scène*, *concentration des décisions structurantes dans des cénacles spécialisés*.
|
||||
|
||||
Du point de vue archicratique, cette topologie a deux conséquences majeures. Premièrement, elle explique pourquoi la critique de Système F oscille souvent entre deux registres incomplets : une *dénonciation des effets* (biais, injustices, opacités) *qui reste prisonnière des hypotopies locales*, et une *focalisation sur les normes juridiques et les principes éthiques dans les hypertopies de gouvernance*, sans que les deux niveaux se rencontrent vraiment dans une scène commune.
|
||||
|
||||
Deuxièmement, elle montre que la question d’une *politique des épreuves viables*, telle que proposée dans la conclusion de la thèse, ne peut pas se réduire à ajouter des procédures ou des principes ; elle suppose un *reprofilage topologique* : épaissir certaines scènes, en ouvrir d’autres, *désaturer les hypertopies*, *relier les archicrations judiciaires aux expériences des guichets et des interfaces*.
|
||||
|
||||
On peut résumer cette configuration sous la forme d’un tableau, qui n’est pas une grille normative, mais une carte de travail pour la suite du cas pratique :
|
||||
|
||||
| Type de scène | Rôle pour les personnes concernées | Prise arcalité (A) | Prise cratialité (C) | Prise archicration (A’) | Type topo | Ouverture / fermeture |
|
||||
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||
| Guichet / interface de service (social, santé, justice, RH, plateformes) | Lieu où l’on dépose un dossier, consulte un statut, reçoit une réponse, subit un classement, voit un contenu disparaître ou être promu | A déclarée minimale (formules de mission, messages standard) ; A implicite invisible pour l’usager | C présente par ses effets (scores, priorités, codes couleur), mais non explicitable depuis la scène | A’ quasi absente : pas de mise en épreuve du dispositif, seulement des marges de contournement informelles | Hypotopie | Ouverte aux usagers, mais pauvre en prises sur Système F |
|
||||
| Recours numériques / feedbacks (formulaires, étoiles, “signaler”) | Lieu où l’on conteste une décision, où l’on exprime une insatisfaction, où l’on “note” un service ou un contenu | A réduite à des catégories pré-codées (“motifs de recours”, items de satisfaction) | C sollicitée comme capteur (les *feedbacks* alimentent Système F), mais non exposée comme telle | A’ fantomatique : l’idée de scène est mise en forme, sans garantie de remontée vers les lieux de décision | Hypotopie / Atopie | Ouverte, mais trajectoires des contributions opaques, pouvoir réel incertain |
|
||||
| Comités de projet, boards techniques, gouvernance “IA responsable”, conformité AI Act | Lieux où se décident les cas d’usage, les architectures, les proxies, les seuils, les stratégies de déploiement, la gestion des risques | A explicite (principes internes, matrices de risques, traductions locales de normes éthiques et juridiques) | C centrale : design du modèle, choix des données, paramétrage, intégration dans les procédures | A’ potentielle mais capturée : débats et arbitrages internes, sans présence directe des publics affectés | Hypertopie cratiale | Fermée, réservée à des experts et décideurs, forte capacité de décision |
|
||||
| Scènes judiciaires et quasi-judiciaires (tribunaux, autorités, régulateurs, Oversight Board) | Lieux où l’on conteste des effets de Système F (refus, discriminations, censures), où des obligations sont interprétées et imposées | A reconstituée a posteriori : qualification juridique, examen des finalités, rappel des droits fondamentaux | C partiellement accessible : éléments techniques communiqués par fragments, sous contraintes de secret et de compétence | A’ réelle mais tronquée : procédure contradictoire, décisions motivées, sans accès systématique à l’ensemble de la chaîne cratiale | Entre hypotopie et synchrotopie | Semi-ouverte : accès conditionné, forte institutionnalisation, capacité d’inflexion mais intermittente |
|
||||
|
||||
Ce tableau ne remplace pas l’analyse, il la condense. Il fait apparaître, d’un seul coup d’œil, le motif central : Système F s’inscrit dans un régime où les *scènes ouvertes aux personnes concernées sont topologiquement appauvries* (*hypotopies* et *atopies*), tandis que les *scènes riches en prises se trouvent concentrées dans des hypertopies cratiales et régulatoires qui restent largement hors de leur portée*. Les *scènes judiciaires et quasi-judiciaires* jouent un rôle d’*intermédiation*, mais sans parvenir, dans l’état actuel des choses, à recomposer une *synchrotopie archicratique co-viable*.
|
||||
|
||||
Cette carte topologique ne surgit pas de nulle part : elle condense des motifs déjà repérés dans les méta-régimes techno-logistique, scripturo-bureaucratique et marchand. La Partie III se donnera précisément pour tâche de replacer Système F dans cette archéogénèse, afin de montrer que l’IA n’inaugure pas un monde entièrement nouveau, mais recombine des puissances régulatrices déjà à l’œuvre – en les poussant vers un méta-régime *autarchicratique numérique*.
|
||||
|
||||
La suite du cas pratique pourra s’appuyer sur cette synthèse topologique pour deux mouvements complémentaires : replacer ce paysage dans l’*archéogénèse des méta-régimes régulateurs* (Partie III), puis explorer ce que signifierait, concrètement, une réouverture archicratique de Système F, c’est-à-dire une transformation simultanée des prises (A/C/A’) et des lieux (topologie scénique) dans lesquels se joue sa puissance régulatrice.
|
||||
|
||||
##
|
||||