chore: add diagrams + scripts + archicrat-ia route
All checks were successful
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 14s

This commit is contained in:
2026-02-20 18:27:25 +01:00
parent ab3758bbc2
commit 78eb9cbb58
19 changed files with 8442 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
// scripts/build-para-index.mjs
import fs from "node:fs/promises";
import path from "node:path";
function parseArgs(argv) {
const out = { inDir: "dist", outFile: "dist/para-index.json" };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--in" && argv[i + 1]) {
out.inDir = argv[++i];
continue;
}
if (a.startsWith("--in=")) {
out.inDir = a.slice("--in=".length);
continue;
}
if (a === "--out" && argv[i + 1]) {
out.outFile = argv[++i];
continue;
}
if (a.startsWith("--out=")) {
out.outFile = a.slice("--out=".length);
continue;
}
}
return out;
}
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function walk(dir) {
const out = [];
const ents = await fs.readdir(dir, { withFileTypes: true });
for (const e of ents) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...(await walk(p)));
else out.push(p);
}
return out;
}
function stripTags(html) {
return String(html || "")
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<[^>]+>/g, " ");
}
function decodeEntities(s) {
// minimal, volontairement (évite dépendances)
return String(s || "")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
}
function normalizeSpaces(s) {
return decodeEntities(s).replace(/\s+/g, " ").trim();
}
function relPageFromIndexHtml(inDirAbs, fileAbs) {
const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/");
if (!/index\.html$/i.test(rel)) return null;
// dist/<page>/index.html -> "/<page>/"
const page = "/" + rel.replace(/index\.html$/i, "");
return page;
}
async function main() {
const { inDir, outFile } = parseArgs(process.argv.slice(2));
const CWD = process.cwd();
const inDirAbs = path.isAbsolute(inDir) ? inDir : path.join(CWD, inDir);
const outAbs = path.isAbsolute(outFile) ? outFile : path.join(CWD, outFile);
// ✅ antifragile: si dist/ (ou inDir) absent -> on SKIP proprement
if (!(await exists(inDirAbs))) {
console.log(` para-index: skip (input missing): ${inDir}`);
process.exit(0);
}
const files = (await walk(inDirAbs)).filter((p) => /index\.html$/i.test(p));
if (!files.length) {
console.log(` para-index: skip (no index.html found in): ${inDir}`);
process.exit(0);
}
const items = [];
const byId = Object.create(null);
// <p ... id="p-...">...</p>
// (regex volontairement stricte sur l'id pour éviter faux positifs)
const reP = /<p\b([^>]*\bid\s*=\s*["'](p-\d+-[^"']+)["'][^>]*)>([\s\S]*?)<\/p>/gi;
for (const f of files) {
const page = relPageFromIndexHtml(inDirAbs, f);
if (!page) continue;
const html = await fs.readFile(f, "utf8");
let m;
while ((m = reP.exec(html))) {
const id = m[2];
const inner = m[3];
if (byId[id] != null) continue; // protège si jamais doublons
const text = normalizeSpaces(stripTags(inner));
if (!text) continue;
byId[id] = items.length;
items.push({ id, page, text });
}
}
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
items,
byId,
};
await fs.mkdir(path.dirname(outAbs), { recursive: true });
await fs.writeFile(outAbs, JSON.stringify(out), "utf8");
console.log(`✅ para-index: items=${items.length} -> ${path.relative(CWD, outAbs)}`);
}
main().catch((e) => {
console.error("FAIL: build-para-index crashed:", e);
process.exit(1);
});

View File

@@ -0,0 +1,173 @@
// 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 inferPageKeyFromFile(fileAbs) {
const rel = path.relative(ANNO_DIR, fileAbs).replace(/\\/g, "/");
return rel.replace(/\.(ya?ml|json)$/i, "");
}
function normalizePageKey(s) {
return String(s || "").replace(/^\/+/, "").replace(/\/+$/, "");
}
function isPlainObject(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
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 a1 = aliases?.[pageKey]?.[oldId];
if (a1) return a1;
const a2 = aliases?.[oldId];
if (a2) return 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 dabord `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));
let pages = 0;
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 = normalizePageKey(inferPageKeyFromFile(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;
}
const distFile = path.join(DIST_DIR, pageKey, "index.html");
if (!(await exists(distFile))) {
failures++;
notes.push(`- MISSING PAGE: dist/${pageKey}/index.html (from ${rel})`);
continue;
}
pages++;
const html = await fs.readFile(distFile, "utf8");
for (const paraId of Object.keys(doc.paras)) {
checked++;
if (!/^p-\d+-/i.test(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"));
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);
});

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* seed-gitea-labels — crée les labels attendus (idempotent)
*
* Usage:
* FORGE_TOKEN=... FORGE_API=http://192.168.1.20:3000 node scripts/seed-gitea-labels.mjs
* (ou FORGE_BASE=https://gitea... si pas de FORGE_API)
*
* Optionnel:
* GITEA_OWNER / GITEA_REPO (sinon auto-détecté via git remote origin)
*/
import { spawnSync } from "node:child_process";
function getEnv(name, fallback = "") {
return (process.env[name] ?? fallback).trim();
}
function inferOwnerRepoFromGit() {
const r = spawnSync("git", ["remote", "get-url", "origin"], { encoding: "utf-8" });
if (r.status !== 0) return null;
const u = (r.stdout || "").trim();
const m = u.match(/[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/);
if (!m?.groups) return null;
return { owner: m.groups.owner, repo: m.groups.repo };
}
async function apiReq(base, token, method, path, payload = null) {
const url = `${base.replace(/\/+$/, "")}/api/v1${path}`;
const headers = {
Authorization: `token ${token}`,
Accept: "application/json",
"User-Agent": "archicratie-seed-labels/1.0",
};
const init = { method, headers };
if (payload != null) {
init.headers["Content-Type"] = "application/json";
init.body = JSON.stringify(payload);
}
const res = await fetch(url, init);
const text = await res.text().catch(() => "");
let json = null;
try { json = text ? JSON.parse(text) : null; } catch {}
if (!res.ok) throw new Error(`HTTP ${res.status} ${method} ${url}\n${text}`);
return json;
}
async function main() {
const token = getEnv("FORGE_TOKEN");
if (!token) throw new Error("FORGE_TOKEN manquant");
const inferred = inferOwnerRepoFromGit() || {};
const owner = getEnv("GITEA_OWNER", inferred.owner || "");
const repo = getEnv("GITEA_REPO", inferred.repo || "");
if (!owner || !repo) throw new Error("Impossible de déterminer owner/repo (GITEA_OWNER/GITEA_REPO ou git remote)");
const base = getEnv("FORGE_API") || getEnv("FORGE_BASE");
if (!base) throw new Error("FORGE_API ou FORGE_BASE manquant");
const wanted = [
// type/*
{ name: "type/comment", color: "1d76db", description: "Commentaire éditorial (site)" },
{ name: "type/media", color: "1d76db", description: "Media à intégrer (image/audio/video)" },
{ name: "type/correction", color: "1d76db", description: "Correction proposée" },
{ name: "type/fact-check", color: "1d76db", description: "Vérification / sourçage" },
// state/*
{ name: "state/a-trier", color: "0e8a16", description: "À trier" },
{ name: "state/recevable", color: "0e8a16", description: "Recevable" },
{ name: "state/a-sourcer", color: "0e8a16", description: "À sourcer" },
// scope/*
{ name: "scope/readers", color: "5319e7", description: "Signalé par lecteur" },
{ name: "scope/editors", color: "5319e7", description: "Signalé par éditeur" },
];
const labels = (await apiReq(base, token, "GET", `/repos/${owner}/${repo}/labels?limit=1000`)) || [];
const existing = new Set(labels.map((x) => x?.name).filter(Boolean));
let created = 0;
for (const L of wanted) {
if (existing.has(L.name)) continue;
await apiReq(base, token, "POST", `/repos/${owner}/${repo}/labels`, {
name: L.name,
color: L.color,
description: L.description,
});
created++;
console.log("✅ created:", L.name);
}
if (created === 0) console.log(" seed: nothing to do (all labels already exist)");
else console.log(`✅ seed done: created=${created}`);
}
main().catch((e) => {
console.error("💥 seed-gitea-labels:", e?.message || e);
process.exit(1);
});