Compare commits

..

7 Commits

8 changed files with 429 additions and 122 deletions

View File

@@ -0,0 +1,2 @@
blank_issues_enabled: true
contact_links: []

View File

@@ -0,0 +1,31 @@
---
name: "Correction paragraphe"
about: "Proposer une correction ciblée (un paragraphe) avec justification."
---
## Chemin (ex: /archicratie/prologue/)
<!-- obligatoire -->
/...
## Ancre paragraphe (ex: #p-0-xxxx)
<!-- obligatoire -->
#p-...
## Version affichée (optionnel)
0.1.0
## Texte actuel (copie exacte du paragraphe)
<!-- obligatoire -->
> ...
## Proposition (texte corrigé complet)
<!-- obligatoire -->
...
## Justification
<!-- obligatoire : clarté, cohérence lexicale, non-redondance, rigueur -->
...
## Sources / références (si factuel)
<!-- si tu touches à un fait, donne des références -->
...

View File

@@ -1,66 +0,0 @@
name: "Correction paragraphe"
about: "Proposer une correction ciblée, justifiée, versionnée (paragraphe + ancre)."
title: "[Correction] "
labels: ["type/correction"]
body:
- type: markdown
attributes:
value: |
⚠️ Une correction = **un paragraphe**.
Merci de fournir **Chemin + Ancre** et une **proposition complète**.
- type: input
id: chemin
attributes:
label: "Chemin (ex: /archicratie/prologue/)"
placeholder: "/archicratie/prologue/"
validations:
required: true
- type: input
id: ancre
attributes:
label: "Ancre paragraphe (ex: #p-0-xxxx)"
placeholder: "#p-0-xxxxxxxx"
validations:
required: true
- type: input
id: version
attributes:
label: "Version du texte (affichée sur la page)"
placeholder: "0.1.0"
validations:
required: false
- type: textarea
id: actuel
attributes:
label: "Texte actuel (paragraphe complet)"
description: "Copie-colle le paragraphe entier si possible."
validations:
required: true
- type: textarea
id: proposition
attributes:
label: "Proposition (texte corrigé complet)"
description: "Donne la version corrigée du paragraphe, prête à intégrer."
validations:
required: true
- type: textarea
id: justification
attributes:
label: "Justification"
description: "Pourquoi cette correction ? (clarté, cohérence lexicale, non-redondance, rigueur)."
validations:
required: true
- type: textarea
id: sources
attributes:
label: "Sources / références (si factuel)"
description: "Si la correction touche un fait, fournir des références."
validations:
required: false

View File

@@ -0,0 +1,27 @@
---
name: "Vérification factuelle / sources"
about: "Signaler une assertion à sourcer ou à corriger (preuves, références)."
---
## Chemin (ex: /archicratie/prologue/)
<!-- obligatoire -->
/...
## Ancre paragraphe
<!-- obligatoire -->
#p-...
## Assertion / passage à vérifier (copie exacte)
<!-- obligatoire -->
> ...
## Problème identifié
<!-- obligatoire : manque de source ? inexact ? ambigu ? daté ? -->
...
## Sources proposées
<!-- auteur, titre, éditeur, année, pages, lien -->
...
## Proposition de correction (si applicable)
...

View File

@@ -1,56 +0,0 @@
name: "Vérification factuelle / sources"
about: "Signaler une assertion à sourcer ou à corriger (normes de citation)."
title: "[Fact-check] "
labels: ["type/fact-check", "state/a-sourcer"]
body:
- type: markdown
attributes:
value: |
Objectif : **robustifier** le texte (preuves, références, normes de citation).
Une issue = une assertion principale.
- type: input
id: chemin
attributes:
label: "Chemin (ex: /archicratie/prologue/)"
placeholder: "/archicratie/prologue/"
validations:
required: true
- type: input
id: ancre
attributes:
label: "Ancre paragraphe"
placeholder: "#p-0-xxxxxxxx"
validations:
required: true
- type: textarea
id: assertion
attributes:
label: "Assertion / passage à vérifier (copie exacte)"
validations:
required: true
- type: textarea
id: probleme
attributes:
label: "Problème identifié"
description: "Manque de source ? Inexact ? Ambigu ? Daté ?"
validations:
required: true
- type: textarea
id: sources
attributes:
label: "Sources proposées"
description: "Références (auteur, titre, éditeur, année, pages, lien)."
validations:
required: false
- type: textarea
id: correction
attributes:
label: "Proposition de correction (si applicable)"
validations:
required: false

View File

@@ -0,0 +1,110 @@
name: Auto-label issues
on:
issues:
types: [opened, edited]
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Apply labels from Type/State
env:
FORGE_BASE: ${{ vars.FORGE_API || vars.FORGE_BASE }}
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
REPO_FULL: ${{ gitea.repository }}
EVENT_PATH: ${{ github.event_path }}
run: |
python3 - <<'PY'
import json, os, re, urllib.request, urllib.error
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"]
with open(event_path, "r", encoding="utf-8") as f:
ev = json.load(f)
issue = ev.get("issue") or {}
title = issue.get("title") or ""
body = issue.get("body") or ""
number = issue.get("number") or issue.get("index")
if not number:
raise SystemExit("No issue number/index in event payload")
def pick_line(key):
m = re.search(rf"^{re.escape(key)}:\s*([^\n\r]+)", body, flags=re.M)
return m.group(1).strip() if m else ""
desired = set()
t = pick_line("Type")
s = pick_line("State")
# 1) Type / State explicit (depuis le site)
if t: desired.add(t)
if s: desired.add(s)
# 2) Fallback depuis le titre si besoin
if not t:
if title.startswith("[Correction]"):
desired.add("type/correction")
elif title.startswith("[Fact-check]") or title.startswith("[Vérification]"):
desired.add("type/fact-check")
if not s:
# état par défaut si absent
if "type/fact-check" in desired:
desired.add("state/a-sourcer")
elif "type/correction" in desired:
desired.add("state/recevable")
if not desired:
print("No labels to apply.")
raise SystemExit(0)
api = forge + "/api/v1"
headers = {
"Authorization": f"token {token}",
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-auto-label/1.0",
}
def jreq(method, url, payload=None):
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) 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
# labels repo
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000") 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]
# 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}
final_ids = sorted(current_ids.union(wanted_ids))
# set 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)
print(f"OK labels #{number}: {sorted(desired)}")
PY

View File

@@ -0,0 +1,95 @@
<dialog id="propose-modal" aria-label="Qualifier la proposition">
<form method="dialog" class="box">
<header class="hd">
<h2>Qualifier la proposition</h2>
<button value="cancel" class="x" aria-label="Fermer">✕</button>
</header>
<p class="sub">
Optionnel : choisis une intention (pour tri & traitement éditorial). Sinon, continue sans préciser.
</p>
<div class="grid">
<button type="button" data-category="cat/style">Style / lisibilité</button>
<button type="button" data-category="cat/lexique">Lexique / terminologie</button>
<button type="button" data-category="cat/argument">Argument / structure</button>
<button type="button" data-category="cat/redondance">Redondance</button>
<button type="button" data-category="cat/source">Source / vérification</button>
<button type="button" data-category="">Continuer sans préciser</button>
</div>
<footer class="ft">
<small>Astuce : touche <kbd>Esc</kbd> pour fermer.</small>
</footer>
</form>
</dialog>
<style>
dialog { border: none; padding: 0; border-radius: 16px; width: min(720px, calc(100vw - 2rem)); }
dialog::backdrop { background: rgba(0,0,0,.55); }
.box { padding: 1rem 1rem 0.75rem; }
.hd { display:flex; align-items:center; justify-content:space-between; gap:.75rem; }
.hd h2 { margin:0; font-size:1.1rem; }
.x { border:0; background:transparent; font-size:1.1rem; cursor:pointer; }
.sub { margin:.5rem 0 1rem; opacity:.9; }
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:.5rem; }
.grid button { padding: .75rem .8rem; border-radius: 12px; border: 1px solid rgba(0,0,0,.18); background: rgba(255,255,255,.96); color: #111; cursor: pointer; text-align: left; }
.grid button:hover { background: #fff; }
kbd { padding:0 .35rem; border:1px solid rgba(0,0,0,.2); border-radius:6px; }
</style>
<script is:inline>
(() => {
const dlg = document.getElementById("propose-modal");
if (!dlg) return;
/** @type {URL|null} */
let pending = null;
const upsertLine = (text, key, value) => {
const re = new RegExp(`^${key}:\\s*.*$`, "mi");
// Si "continuer sans préciser" : on ne touche pas au body
if (!value) return text;
if (re.test(text)) return text.replace(re, `${key}: ${value}`);
const sep = text && !text.endsWith("\n") ? "\n" : "";
return text + sep + `${key}: ${value}\n`;
};
const openModalFor = (url) => {
pending = url;
if (typeof dlg.showModal === "function") dlg.showModal();
else window.open(url.toString(), "_blank", "noopener,noreferrer");
};
// Interception des clics sur "Proposer"
document.addEventListener("click", (e) => {
const a = e.target?.closest?.("a[data-propose]");
if (!a) return;
e.preventDefault();
// URL réelle stockée par EditionLayout (propose.dataset.url)
const rawUrl = a.dataset.url || a.getAttribute("href") || "";
if (!rawUrl || rawUrl === "#") return;
try {
openModalFor(new URL(rawUrl, window.location.origin));
} catch {
window.open(rawUrl, "_blank", "noopener,noreferrer");
}
});
// Choix dune catégorie -> injection "Category:" + ouverture Gitea
dlg.addEventListener("click", (e) => {
const btn = e.target?.closest?.("button[data-category]");
if (!btn || !pending) return;
const cat = btn.getAttribute("data-category") || "";
const body = pending.searchParams.get("body") || "";
pending.searchParams.set("body", upsertLine(body, "Category", cat));
dlg.close();
window.open(pending.toString(), "_blank", "noopener,noreferrer");
});
})();
</script>

View File

@@ -0,0 +1,164 @@
---
import ProposeModal from "../components/ProposeModal.astro";
import SiteNav from "../components/SiteNav.astro";
import LevelToggle from "../components/LevelToggle.astro";
import BuildStamp from "../components/BuildStamp.astro";
import "../styles/global.css";
const {
title,
editionLabel,
editionKey,
statusLabel,
statusKey,
level,
version
} = Astro.props;
const lvl = level ?? 1;
const canonical = Astro.site
? new URL(Astro.url.pathname, Astro.site).href
: Astro.url.href;
// Cible Gitea (injectée au build)
const GITEA_BASE = import.meta.env.PUBLIC_GITEA_BASE ?? "";
const GITEA_OWNER = import.meta.env.PUBLIC_GITEA_OWNER ?? "";
const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
---
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title ? `${title} — Archicratie` : "Archicratie"}</title>
<link rel="canonical" href={canonical} />
<link rel="sitemap" href="/sitemap-index.xml" />
<meta data-pagefind-filter="edition[content]" content={String(editionKey ?? editionLabel)} />
<meta data-pagefind-filter="level[content]" content={String(lvl)} />
<meta data-pagefind-filter="status[content]" content={String(statusKey ?? statusLabel)} />
<meta data-pagefind-meta={`edition:${String(editionKey ?? editionLabel)}`} />
<meta data-pagefind-meta={`level:${String(lvl)}`} />
<meta data-pagefind-meta={`status:${String(statusKey ?? statusLabel)}`} />
<meta data-pagefind-meta={`version:${String(version ?? "")}`} />
</head>
<body data-doc-title={title} data-doc-version={version}>
<header>
<SiteNav />
<div class="edition-bar">
<span class="badge"><strong>Édition</strong> : {editionLabel}</span>
<span class="badge"><strong>Statut</strong> : {statusLabel}</span>
<span class="badge"><strong>Niveau</strong> : {lvl}</span>
<span class="badge"><strong>Version</strong> : {version}</span>
<LevelToggle initialLevel={lvl} />
</div>
</header>
<main>
<article class="reading" data-pagefind-body>
<slot />
<BuildStamp />
</article>
</main>
<ProposeModal />
<!-- IMPORTANT: define:vars injecte les constantes dans le JS sans templating fragile -->
<script is:inline define:vars={{ GITEA_BASE, GITEA_OWNER, GITEA_REPO }}>
(() => {
const title = document.body.dataset.docTitle || document.title;
const version = document.body.dataset.docVersion || "";
const giteaReady = Boolean(GITEA_BASE && GITEA_OWNER && GITEA_REPO);
function buildIssueURL(anchorId, excerpt) {
const base = String(GITEA_BASE).replace(/\/+$/, "");
const issue = new URL(`${base}/${GITEA_OWNER}/${GITEA_REPO}/issues/new`);
const local = new URL(window.location.href);
local.hash = anchorId;
const path = local.pathname;
const issueTitle = `[Correction] ${anchorId} — ${title}`;
const body = [
`Chemin: ${path}`,
`URL locale: ${local.toString()}`,
`Ancre: #${anchorId}`,
`Version: ${version || "(non renseignée)"}`,
`Type: type/correction`,
`State: state/recevable`,
``,
`Texte actuel (extrait):`,
`> ${excerpt}`,
``,
`Proposition (remplacer par):`,
``,
`Justification:`,
``,
`---`,
`Note: issue générée depuis le site (pré-remplissage).`
].join("\n");
issue.searchParams.set("title", issueTitle);
issue.searchParams.set("body", body);
return issue.toString();
}
const paras = Array.from(document.querySelectorAll(".reading p[id]"));
for (const p of paras) {
if (p.querySelector(".para-tools")) continue;
const tools = document.createElement("span");
tools.className = "para-tools";
const a = document.createElement("a");
a.className = "para-anchor";
a.href = `#${p.id}`;
a.setAttribute("aria-label", "Lien vers ce paragraphe");
a.textContent = "¶";
const citeBtn = document.createElement("button");
citeBtn.type = "button";
citeBtn.className = "para-cite";
citeBtn.textContent = "Citer";
citeBtn.addEventListener("click", async () => {
const url = new URL(window.location.href);
url.hash = p.id;
const cite = `${title}${version ? ` (v${version})` : ""} — ${url.toString()}`;
try {
await navigator.clipboard.writeText(cite);
const prev = citeBtn.textContent;
citeBtn.textContent = "Copié";
setTimeout(() => (citeBtn.textContent = prev), 900);
} catch {
window.prompt("Copiez la citation :", cite);
}
});
tools.appendChild(a);
tools.appendChild(citeBtn);
if (giteaReady) {
const propose = document.createElement("a");
propose.className = "para-propose";
propose.href = "#";
propose.textContent = "Proposer";
propose.setAttribute("aria-label", "Proposer une correction sur Gitea");
propose.setAttribute("data-propose","1");
tools.appendChild(propose);
}
p.appendChild(tools);
}
})();
</script>
</body>
</html>