5 Commits

Author SHA1 Message Date
927d8b6f85 anchors: update baseline
Some checks failed
CI / build-and-anchors (push) Failing after 32s
2026-01-20 15:01:18 +01:00
5149bdec89 docs: add quickstart + reference manual + ticket contract
Some checks failed
CI / build-and-anchors (push) Failing after 32s
2026-01-20 14:57:42 +01:00
5f34a1d393 ui: propose modal opens ticket in new tab only 2026-01-20 14:57:26 +01:00
84c295d4ce ci: build + anchors + inline-js guard
Some checks failed
CI / build-and-anchors (push) Failing after 5m4s
2026-01-20 14:19:11 +01:00
04d6db10af Merge pull request 'propose: exact paragraph + apply-ticket guardrails' (#33) from feat/proposer-exact-paragraph into master
Reviewed-on: #33
2026-01-20 12:59:18 +01:00
10 changed files with 503 additions and 13 deletions

35
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,35 @@
name: CI
on:
push:
branches: ["**"]
pull_request:
branches: ["master"]
jobs:
build-and-anchors:
runs-on: ubuntu-latest
container:
image: node:20-bookworm-slim
steps:
- name: Install git (needed by checkout)
run: |
apt-get update
apt-get install -y --no-install-recommends git ca-certificates
git --version
- name: Checkout
uses: actions/checkout@v4
- name: Install deps
run: npm ci
- name: Inline scripts syntax check
run: node scripts/check-inline-js.mjs
- name: Build
run: npm run build
- name: Anchors contract
run: npm run test:anchors

View File

@@ -41,3 +41,27 @@ All commands are run from the root of the project, from a terminal:
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
# Archicratie — Web Edition (Atelier éditorial)
Ce repo contient le site Astro “Archicratie Web Edition” et sa machine éditoriale :
- Proposer (tickets Gitea pré-remplis depuis les paragraphes)
- Apply-ticket (application sûre des corrections)
- Contrat dancres (citabilité) + churn test
- CI Gitea Actions + runner DS220+
## Démarrage rapide
- `npm install`
- `npm run dev`
## Documentation (référence)
- **Quickstart (10 min)** : `docs/QUICKSTART.md`
- **Manuel de référence** : `docs/MANUEL_REFERENCE.md`
- **Contrat de tickets** : `docs/CONTRAT_TICKETS.md`
## Commandes clés
- Tests complets : `npm test`
- Test ancres : `npm run test:anchors`
- Appliquer un ticket :
- `node scripts/apply-ticket.mjs <N> --dry-run`
- `node scripts/apply-ticket.mjs <N>`

29
docs/CONTRAT_TICKETS.md Normal file
View File

@@ -0,0 +1,29 @@
# Contrat de ticket — Proposer / Apply-ticket / Auto-label
Ce document fixe le format minimal et les invariants des tickets dédition.
Objectif : parsing fiable par scripts + workflows.
## Invariants (non négociables)
Doivent toujours exister dans le body du ticket :
- `Chemin: /.../`
- `URL locale: ...#...` (utile pour audit humain)
- `Ancre: #p-...`
- `Type: type/...`
- `State: state/...`
- `Proposition (remplacer par):`
## Texte actuel : best effort
Priorité :
1) `Texte actuel (copie exacte du paragraphe):`
2) sinon `Texte actuel (extrait):` + note de troncature
> Même si le texte actuel est un extrait, lancre + chemin rendent le ticket opposable.
## Catégorie (optionnelle)
- `Category: cat/...` (ou vide)
## Pourquoi ce contrat ?
- `apply-ticket.mjs` dépend de repères textuels stables
- `auto-label-issues` dépend de `Type/State/Category`
- on veut éviter des tickets “illisibles machine” qui cassent lindustrialisation

206
docs/MANUEL_REFERENCE.md Normal file
View File

@@ -0,0 +1,206 @@
# Manuel de référence — Archicratie Web Edition (Site + Gitea + Runner)
Ce manuel explique comment utiliser et maintenir loutil dédition :
- Site Astro “Archicratie Web Edition”
- Proposer (tickets Gitea pré-remplis)
- Apply-ticket (application semi-automatique dans le contenu)
- Contrat dancres (citabilité) + tests
- CI Gitea Actions + ds220-runner (DS220+)
> Objectif : une boucle éditoriale reproductible, opposable, sûre, et testée.
---
## 0) Glossaire minimal (anti-jargon)
- **PR** (Pull Request) : demande de fusion dune branche vers `master`.
- **PAT** (Personal Access Token) : jeton daccès Gitea utilisé comme “mot de passe” pour API/Git.
- **Anchor / Ancre** : identifiant stable dun paragraphe (ex: `p-8-0e65838d`) utilisable dans une URL `#...`.
- **Churn** : variation des ancres dune page entre deux builds (mesure de stabilité).
- **dist/** : sortie buildée (artefact), jamais la “source de vérité”.
- **CI** : automatisation (build + tests) à chaque push/PR.
---
## 1) Pré-requis (poste local)
### Outils
- Node.js + npm
- Git
- (Optionnel) Python3 pour scripts de debug ponctuels
### Accès
- Compte Gitea avec droits sur le repo
- Un **PAT** Gitea
- Accès au site local (dev) et/ou build (dist)
---
## 2) Vue densemble : la boucle éditoriale (rituel)
### Boucle standard (la “cadence courte”)
1) Sur le site, cliquer **Proposer** sur un paragraphe.
2) Choisir Type (Correction / Fact-check), puis Category.
3) Gitea ouvre un ticket pré-rempli (Chemin/URL/Ancre + texte actuel).
4) Rédiger la **Proposition (remplacer par)** + Justification.
5) En local :
- `node scripts/apply-ticket.mjs <num>` (dabord `--dry-run`)
- `npm run test:anchors`
- `npm run build`
6) Commit + push + PR si nécessaire.
---
## 3) Le site “Proposer / Citer / ¶” (para-tools)
### Où ça vit ?
- `src/layouts/EditionLayout.astro` injecte un script qui :
- ajoute “¶” (lien dancre), “Citer”, “Proposer”
- construit lURL de création dissue Gitea
- embarque si possible le **paragraphe exact** (sinon un extrait)
- `src/components/ProposeModal.astro` gère la modal 2 étapes :
- Type (correction/fact-check)
- Category (cat/style, cat/lexique, etc.)
- ouvre ensuite lissue en **nouvel onglet** sans quitter le site
### Invariants (non négociables)
- **Ancre + URL + Chemin** doivent toujours être présents.
- Le mode “texte exact” est *best effort* :
- si trop long → fallback extrait + note
- priorité absolue : rester robuste et cliquable.
### Limites / règles de taille
- **FULL_TEXT_SOFT_LIMIT** : taille max pour tenter dembarquer “copie exacte”
- **URL_HARD_LIMIT** : si lURL devient trop longue → repasse en extrait
> But : éviter des issues amputées + éviter des URLs trop longues / fragiles.
---
## 4) Format des tickets (machine-readable)
Le parsing des tickets est fait par `scripts/apply-ticket.mjs` + workflow auto-label.
Donc le contenu doit rester “parsable”.
Sections attendues (au minimum) :
- `Chemin: /.../`
- `Ancre: #p-...`
- `Texte actuel (...)` (idéalement “copie exacte”, sinon “extrait”)
- `Proposition (remplacer par): ...`
- `Justification: ...` (peut être vide, mais le champ doit exister si possible)
> Voir aussi : docs/CONTRAT_TICKETS.md
---
## 5) Appliquer un ticket en local : `apply-ticket`
### Commandes
- Dry run (recommandé) :
- `node scripts/apply-ticket.mjs <N> --dry-run`
- Application réelle :
- `node scripts/apply-ticket.mjs <N>`
### Comportements de sûreté (guardrails)
- `--dry-run` :
- **aucun fichier écrit**
- **aucun backup créé**
- affiche BEFORE/AFTER (extrait)
- Mode écriture :
- crée un backup `.bak.issue-<N>` uniquement si écriture
- match “best effort” mais refuse si score insuffisant
### Après application
- `git diff -- <fichier>`
- `git add <fichier>`
- `git commit -m "edit: apply ticket #N (...)"`
---
## 6) Contrat de citabilité : ancres + churn test
### Pourquoi ?
Les ancres sont le “socle de citabilité”. On veut prévenir les liens morts.
### Scripts
- `npm run test:anchors`
- compare les ancres de `dist/` avec une baseline
- calcule le churn et signale les pages modifiées
- `npm run test:anchors:update`
- met à jour la baseline (à faire seulement quand cest volontaire)
> Important : `dist/` est un artefact ; la baseline sert à mesurer, pas à “éditer dist”.
---
## 7) Garde-fou JS inline : `check-inline-js`
Pourquoi : éviter quun JS inline cassé supprime les boutons et fasse disparaître “Proposer/Citer”.
- `node scripts/check-inline-js.mjs`
- intégré dans `npm test`
---
## 8) CI (Gitea Actions)
### Objectif
Sur chaque push/PR : vérifier automatiquement que :
- build OK
- ancres OK
- JS inline OK
### Workflow
- `.gitea/workflows/ci.yml`
### Commande “contrat”
- `npm test`
- exécute : build + anchors + inline-js
---
## 9) ds220-runner (DS220+ / Gitea act-runner)
### Comportement normal
Le runner lance des jobs dans des conteneurs éphémères qui démarrent/stop.
DSM peut notifier “conteneur arrêté de manière inattendue” : cest souvent un faux positif “bruyant”.
### À surveiller vraiment
- Jobs en échec côté Gitea Actions
- logs mentionnant :
- auth API (401)
- labels manquants
- npm ci/build en échec
---
## 10) Dépannage (symptômes → causes fréquentes)
### “Proposer” ouvre deux onglets / remplace longlet courant
Cause : double handler click / manque de `stopPropagation` / mauvais fallback.
Fix : le flux doit ouvrir **nouvel onglet uniquement**, et sinon prompt.
### “Proposer/Citer/¶” disparaissent
Cause : JS inline cassé → exception ou erreur syntaxe.
Fix : `node scripts/check-inline-js.mjs` + vérifier `dist/.../index.html` console.
### apply-ticket : “Proposition introuvable”
Cause : ticket sans section “Proposition (remplacer par):”.
Fix : respecter le contrat de ticket.
### apply-ticket : “Match trop faible”
Cause : texte actuel absent / trop tronqué / mismatch.
Fix :
- privilégier “Texte actuel (copie exacte du paragraphe)”
- sinon le script récupère via `dist` (si possible).
---
## 11) Règles de contribution (discipline de repo)
- Toute modif éditoriale passe par ticket → apply-ticket → tests.
- Toute modif structurelle (layouts, rendu, MDX) doit être suivie de :
- `npm test`
- éventuellement `npm run test:anchors:update` si changement volontaire dancres.
- Ne jamais éditer `dist/` à la main.
---
## 12) Commandes utiles (raccourcis)
- Dev : `npm run dev`
- Build : `npm run build`
- Tests : `npm test`
- Anchors : `npm run test:anchors`
- Apply ticket : `node scripts/apply-ticket.mjs <N> --dry-run`

80
docs/QUICKSTART.md Normal file
View File

@@ -0,0 +1,80 @@
# Quickstart — 10 minutes (Proposer → Ticket → Apply → Tests)
## 1) Pré-requis
- Node.js + npm
- Accès Gitea (compte) + PAT si usage API (apply-ticket)
## 2) Lancer le site
en bash :
npm install
npm run dev
Dans le navigateur, url : http://localhost:4321
## 3) Proposer une correction
Ouvre une page (ex: Prologue).
Sur un paragraphe : clique Proposer.
Choisis :
Type : Correction / Fact-check
Category (optionnel)
Un nouvel onglet Gitea souvre sur une issue pré-remplie.
Rédige :
Proposition (remplacer par):
Justification:
## 4) Appliquer le ticket en local (<NUMERO> = numéro du ticket non pas id-paragraphe)
En bash :
# dry-run (recommandé)
node scripts/apply-ticket.mjs <NUMERO> --dry-run
# appliquer
node scripts/apply-ticket.mjs <NUMERO>
git diff
git add <fichier>
git commit -m "edit: apply ticket #<NUMERO> (...)"
## 5) Vérifier avant push
npm test
## 6) Règle dor
Ne jamais éditer dist/ à la main.
Toujours garder Chemin + Ancre + Proposition dans le ticket.
Déplacer/ajouter les deux docs que je tai donnés
- `docs/MANUEL_REFERENCE.md`
- `docs/CONTRAT_TICKETS.md`
*(Tu peux reprendre mes versions telles quelles.)*
---
## Commandes terminal P0 (copier-coller)
En bash
mkdir -p docs
# crée/édite les fichiers avec ton éditeur habituel
# README.md
# docs/QUICKSTART.md
# docs/MANUEL_REFERENCE.md
# docs/CONTRAT_TICKETS.md
git status -sb
npm test
git add docs/QUICKSTART.md docs/MANUEL_REFERENCE.md docs/CONTRAT_TICKETS.md
git commit -m "docs: add quickstart + reference manual + ticket contract"
git push

View File

@@ -10,6 +10,7 @@
"postbuild": "npx pagefind --site dist",
"import": "node scripts/import-docx.mjs",
"apply:ticket": "node scripts/apply-ticket.mjs",
"test": "npm run build && npm run test:anchors && node scripts/check-inline-js.mjs",
"test:anchors": "node scripts/check-anchors.mjs",
"test:anchors:update": "node scripts/check-anchors.mjs --update"
},

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { spawnSync } from "node:child_process";
const ROOT = process.cwd();
const SRC = path.join(ROOT, "src");
async function* walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) yield* walk(full);
else yield full;
}
}
function extractInlineScripts(astroText) {
// capture <script ... is:inline ...> ... </script>
const re = /<script\b[^>]*\bis:inline\b[^>]*>([\s\S]*?)<\/script>/gi;
const out = [];
let m;
while ((m = re.exec(astroText))) out.push(m[1] ?? "");
return out;
}
function checkSyntax(js, label) {
const tmp = path.join(os.tmpdir(), `inline-js-check-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
const payload = `// ${label}\n${js}\n`;
return fs.writeFile(tmp, payload, "utf-8").then(() => {
const r = spawnSync(process.execPath, ["--check", tmp], { encoding: "utf-8" });
fs.unlink(tmp).catch(() => {});
if (r.status !== 0) {
const msg = (r.stderr || r.stdout || "").trim();
throw new Error(`${label}\n${msg}`);
}
});
}
async function main() {
const targets = [];
for await (const f of walk(SRC)) {
if (f.endsWith(".astro")) targets.push(f);
}
let checked = 0;
for (const file of targets) {
const txt = await fs.readFile(file, "utf-8");
const scripts = extractInlineScripts(txt);
if (!scripts.length) continue;
for (let i = 0; i < scripts.length; i++) {
const js = (scripts[i] || "").trim();
if (!js) continue;
const label = `${path.relative(ROOT, file)} :: <script is:inline> #${i + 1}`;
await checkSyntax(js, label);
checked++;
}
}
console.log(`OK inline-js: scripts checked=${checked}`);
}
main().catch((e) => {
console.error("❌ inline-js syntax check failed");
console.error(e?.message || e);
process.exit(1);
});

View File

@@ -103,6 +103,7 @@
const upsertLine = (text, key, value) => {
const re = new RegExp(`^\\s*${esc(key)}\\s*:\\s*.*$`, "mi");
// value vide => supprimer la ligne si elle existe
if (!value) {
if (!re.test(text)) return text;
return (
@@ -113,8 +114,10 @@
);
}
// remplace si existe
if (re.test(text)) return text.replace(re, `${key}: ${value}`);
// sinon append
const sep = text && !text.endsWith("\n") ? "\n" : "";
return text + sep + `${key}: ${value}\n`;
};
@@ -148,23 +151,47 @@
if (next === body) return;
const prev = u.toString();
u.searchParams.set("body", next);
// garde-fou URL
if (u.toString().length > URL_HARD_LIMIT) {
// revert
u.searchParams.set("body", body);
return;
}
};
// ✅ Ouvre EN NOUVEL ONGLET sans jamais remplacer longlet courant
const openInNewTab = (url) => {
// 1) tente window.open
try {
const w = window.open(url, "_blank", "noopener,noreferrer");
if (w) return true;
} catch {}
// 2) fallback "anchor click" (souvent mieux toléré)
try {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
return true;
} catch {}
// 3) dernier recours: on ne quitte PAS la page
window.prompt("Popup bloquée. Copiez ce lien pour ouvrir le ticket :", url);
return false;
};
const openWith = (url) => {
pending = url;
kind = "";
dlg.dataset.step = "1";
if (typeof dlg.showModal === "function") dlg.showModal();
else window.open(url.toString(), "_blank", "noopener,noreferrer");
else openInNewTab(url.toString());
};
// Intercepte UNIQUEMENT les liens marqués data-propose
@@ -173,6 +200,7 @@
if (!a) return;
e.preventDefault();
e.stopPropagation();
const rawUrl = a.dataset.url || a.getAttribute("href") || "";
if (!rawUrl || rawUrl === "#") return;
@@ -191,7 +219,7 @@
openWith(u);
} catch {
window.open(rawUrl, "_blank", "noopener,noreferrer");
openInNewTab(rawUrl);
}
});
@@ -232,14 +260,14 @@
const cat = btn.getAttribute("data-category") || "";
let body = pending.searchParams.get("body") || "";
body = upsertLine(body, "Category", cat);
pending.searchParams.set("body", body);
const u = pending.toString();
dlg.close();
const w = window.open(u, "_blank", "noopener,noreferrer");
if (!w) window.location.href = u;
// ✅ ouvre en nouvel onglet, sans jamais remplacer longlet courant
openInNewTab(u);
});
})();
</script>

View File

@@ -76,6 +76,26 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
history.replaceState(null, "", window.location.pathname + window.location.hash);
}
// ✅ Hotfix compat citabilité :
// si l'URL pointe vers un ancien hash (#p-8-xxxxxxxx) qui n'existe plus,
// on retombe sur le paragraphe actuel du même index (#p-8-YYYYYYYY).
(function resolveLegacyParagraphHash() {
const h = window.location.hash || "";
const m = h.match(/^#p-(\d+)-[0-9a-f]{8}$/i);
if (!m) return;
const oldId = h.slice(1);
if (document.getElementById(oldId)) return; // l'ancre existe, rien à faire
const idx = m[1];
const prefix = `p-${idx}-`;
const replacement = document.querySelector(`[id^="${prefix}"]`);
if (!replacement) return;
history.replaceState(null, "", `${window.location.pathname}#${replacement.id}`);
replacement.scrollIntoView({ block: "start" });
})();
const docTitle = document.body.dataset.docTitle || document.title;
const docVersion = document.body.dataset.docVersion || "";
@@ -90,7 +110,7 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
const quoteBlock = (s) =>
String(s || "")
.split(/\r?\n/)
.map((l) => `> ${l}`.trimEnd())
.map((l) => (`> ${l}`).trimEnd())
.join("\n");
function buildIssueURL(anchorId, fullText, excerpt) {
@@ -128,7 +148,7 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
`Texte actuel (extrait):`,
quoteBlock(excerpt || ""),
``,
`Note: paragraphe long → extrait (texte complet copié au clic si possible).`,
`Note: paragraphe long → extrait (pour éviter une URL trop longue).`,
];
const footer = [
@@ -214,9 +234,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
// la modal lit data-url en priorité (garde aussi href).
propose.dataset.url = issueUrl;
// Option B : texte complet disponible au clic (presse-papier + upgrade)
propose.dataset.full = raw;
tools.appendChild(propose);
}

View File

@@ -12,7 +12,7 @@
"p-5-85126fa5",
"p-6-3515039d",
"p-7-64a0ca9c",
"p-8-e7075fe3",
"p-8-0e65838d",
"p-9-5ff70fb7",
"p-10-e250e810",
"p-11-594bf307",