fix: …
Some checks failed
CI / build-and-anchors (push) Failing after 2m6s
SMOKE / smoke (push) Successful in 13s

This commit is contained in:
2026-02-23 12:07:01 +01:00
parent ae809e0152
commit 68c3416594
19 changed files with 930 additions and 240 deletions

View File

@@ -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 -->
/...

View File

@@ -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 -->
/...

View File

@@ -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

View File

@@ -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

4
.gitignore vendored
View File

@@ -3,6 +3,10 @@
.env.*
!.env.example
# dev-only
public/_auth/whoami
public/_auth/whoami/*
# --- local backups ---
*.bak
*.bak.*

473
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@astrojs/mdx": "^4.3.13",
"astro": "^5.16.11"
"astro": "^5.17.3"
},
"devDependencies": {
"@astrojs/sitemap": "^3.7.0",
@@ -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.17.3",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.17.3.tgz",
"integrity": "sha512-69dcfPe8LsHzklwj+hl+vunWUbpMB6pmg35mACjetxbJeUNNys90JaBM8ZiwsPK689SAj/4Zqb1ayaANls9/MA==",
"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",

View File

@@ -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.17.3"
},
"devDependencies": {
"@astrojs/sitemap": "^3.7.0",

View File

@@ -60,10 +60,12 @@ function getAlias(aliases, pageKey, oldId) {
// supporte:
// 1) { "<pageKey>": { "<old>": "<new>" } }
// 2) { "<old>": "<new>" }
const a1 = aliases?.[pageKey]?.[oldId];
if (a1) return a1;
const k1 = String(pageKey || "");
const k2 = k1 ? ("/" + k1.replace(/^\/+|\/+$/g, "") + "/") : "";
const a1 = (aliases?.[k1]?.[oldId]) || (k2 ? aliases?.[k2]?.[oldId] : "");
if (a1) return String(a1);
const a2 = aliases?.[oldId];
if (a2) return a2;
if (a2) return String(a2);
return "";
}

View File

@@ -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}`);
}
@@ -117,7 +112,7 @@ async function main() {
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++;

View 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);
});

View File

@@ -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;

View 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})`);

View File

@@ -1,8 +1,5 @@
schema: 1
# optionnel (si présent, doit matcher le chemin du fichier)
page: archicratie/archicrat-ia/prologue
paras:
p-0-d7974f88:
refs:

View File

@@ -144,15 +144,14 @@
const canReaders = inGroup(groups, "readers");
const canEditors = inGroup(groups, "editors");
access.canUsers = Boolean((info?.ok && (canReaders || canEditors)) || (isDev() && !info?.ok));
const whoamiSkipped = Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped);
access.canUsers = Boolean((info?.ok && (canReaders || canEditors)) || whoamiSkipped);
access.ready = true;
if (btnMediaSubmit) btnMediaSubmit.disabled = !access.canUsers;
if (btnSend) btnSend.disabled = !access.canUsers;
if (btnRefSubmit) btnRefSubmit.disabled = !access.canUsers;
// si pas d'accès, on informe (soft)
if (!access.canUsers) {
if (msgHead) {
@@ -162,12 +161,13 @@
}
}
}).catch(() => {
// fallback dev
// fallback dev (cohérent: media + ref + comment)
access.ready = true;
if (isDev()) {
if (Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped)) {
access.canUsers = true;
if (btnMediaSubmit) btnMediaSubmit.disabled = false;
if (btnSend) btnSend.disabled = false;
if (btnRefSubmit) btnRefSubmit.disabled = false;
}
});
@@ -209,8 +209,12 @@
async function loadIndex() {
if (_idxP) return _idxP;
_idxP = (async () => {
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" }).catch(() => null);
if (res && res.ok) return await res.json();
try {
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" });
if (res && res.ok) return await res.json();
} catch {}
// ✅ antifragile: ne pas “cacher” un échec pour toujours (dev/HMR/boot race)
_idxP = null;
return null;
})();
return _idxP;
@@ -564,6 +568,14 @@
hideMsg(msgComment);
const idx = await loadIndex();
// ✅ message soft si lindex est indisponible (sans écraser le message dauth)
if (!idx && msgHead && msgHead.hidden) {
msgHead.hidden = false;
msgHead.textContent = "Index annotations indisponible (annotations-index.json).";
msgHead.dataset.kind = "info";
}
const data = idx?.pages?.[pageKey]?.paras?.[currentParaId] || null;
renderLevel2(data);

View File

@@ -30,6 +30,13 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
// ✅ OPTIONNEL : bridge serveur (proxy same-origin)
const ISSUE_BRIDGE_PATH = import.meta.env.PUBLIC_ISSUE_BRIDGE_PATH ?? "";
// ✅ Auth whoami (same-origin) — configurable, antifragile en dev
const WHOAMI_PATH = import.meta.env.PUBLIC_WHOAMI_PATH ?? "/_auth/whoami";
// Par défaut: en DEV local on SKIP pour éviter le spam 404.
// Pour tester lauth en dev: export PUBLIC_WHOAMI_IN_DEV=1
const WHOAMI_IN_DEV = (import.meta.env.PUBLIC_WHOAMI_IN_DEV ?? "") === "1";
const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ?? "") === "1";
---
<!doctype html>
@@ -52,54 +59,104 @@ const ISSUE_BRIDGE_PATH = import.meta.env.PUBLIC_ISSUE_BRIDGE_PATH ?? "";
<meta data-pagefind-meta={`version:${String(version ?? "")}`} />
{/* ✅ BOOT EARLY : SidePanel dépend de ces globals. */}
<script is:inline define:vars={{ IS_DEV, GITEA_BASE, GITEA_OWNER, GITEA_REPO, ISSUE_BRIDGE_PATH }}>
<script
is:inline
define:vars={{
IS_DEV,
GITEA_BASE,
GITEA_OWNER,
GITEA_REPO,
ISSUE_BRIDGE_PATH,
WHOAMI_PATH,
WHOAMI_IN_DEV,
WHOAMI_FORCE_LOCALHOST,
}}
>
(() => {
const __DEV__ = Boolean(IS_DEV);
window.__archiFlags = Object.assign({}, window.__archiFlags, { dev: __DEV__ });
// ✅ anti double-init (HMR / inclusion accidentelle)
if (window.__archiBootOnce === 1) return;
window.__archiBootOnce = 1;
const base = String(GITEA_BASE || "").replace(/\/+$/, "");
const owner = String(GITEA_OWNER || "");
const repo = String(GITEA_REPO || "");
const giteaReady = Boolean(base && owner && repo);
window.__archiGitea = { ready: giteaReady, base, owner, repo };
var __DEV__ = Boolean(IS_DEV);
const rawBridge = String(ISSUE_BRIDGE_PATH || "").trim();
const normBridge = rawBridge
// ===== Gitea globals =====
var base = String(GITEA_BASE || "").replace(/\/+$/, "");
var owner = String(GITEA_OWNER || "");
var repo = String(GITEA_REPO || "");
window.__archiGitea = {
ready: Boolean(base && owner && repo),
base, owner, repo
};
// ===== optional issue bridge (same-origin proxy) =====
var rawBridge = String(ISSUE_BRIDGE_PATH || "").trim();
var normBridge = rawBridge
? (rawBridge.startsWith("/") ? rawBridge : ("/" + rawBridge.replace(/^\/+/, ""))).replace(/\/+$/, "")
: "";
window.__archiIssueBridge = { ready: Boolean(normBridge), path: normBridge };
const WHOAMI_PATH = "/_auth/whoami";
const REQUIRED_GROUP = "editors";
const READ_GROUP = "readers";
// ===== whoami config =====
var __WHOAMI_PATH__ = String(WHOAMI_PATH || "/_auth/whoami");
var __WHOAMI_IN_DEV__ = Boolean(WHOAMI_IN_DEV);
// En dev: par défaut on SKIP (=> pas de spam 404). Override via PUBLIC_WHOAMI_IN_DEV=1.
var SHOULD_FETCH_WHOAMI = (!__DEV__) || __WHOAMI_IN_DEV__;
window.__archiFlags = Object.assign({}, window.__archiFlags, {
dev: __DEV__,
whoamiPath: __WHOAMI_PATH__,
whoamiInDev: __WHOAMI_IN_DEV__,
whoamiFetch: SHOULD_FETCH_WHOAMI,
});
var REQUIRED_GROUP = "editors";
var READ_GROUP = "readers";
function parseWhoamiLine(text, key) {
const re = new RegExp(`^${key}:\\s*(.*)$`, "mi");
const m = String(text || "").match(re);
return (m?.[1] ?? "").trim();
var re = new RegExp("^" + key + ":\\s*(.*)$", "mi");
var m = String(text || "").match(re);
return (m && m[1] ? m[1] : "").trim();
}
function inGroup(groups, g) {
const gg = String(g || "").toLowerCase();
var gg = String(g || "").toLowerCase();
return Array.isArray(groups) && groups.some((x) => String(x).toLowerCase() === gg);
}
// ===== Auth info promise (single source of truth) =====
if (!window.__archiAuthInfoP) {
window.__archiAuthInfoP = (async () => {
const res = await fetch(`${WHOAMI_PATH}?_=${Date.now()}`, {
credentials: "include",
cache: "no-store",
redirect: "manual",
headers: { Accept: "text/plain" },
}).catch(() => null);
// ✅ dev default: skip
if (!SHOULD_FETCH_WHOAMI) {
return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
}
var res = null;
try {
res = await fetch(__WHOAMI_PATH__ + "?_=" + Date.now(), {
credentials: "include",
cache: "no-store",
redirect: "manual",
headers: { Accept: "text/plain" },
});
} catch {
res = null;
}
if (!res) return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
if (res.type === "opaqueredirect") return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
if (res.status >= 300 && res.status < 400) return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
if (res.status === 404) return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
const text = await res.text().catch(() => "");
const looksLikeWhoami = /Remote-(User|Groups|Email|Name)\s*:/i.test(text);
if (!res.ok || !looksLikeWhoami) return { ok: false, user: "", name: "", email: "", groups: [], raw: text };
var text = "";
try { text = await res.text(); } catch { text = ""; }
const groups = parseWhoamiLine(text, "Remote-Groups")
var looksLikeWhoami = /Remote-(User|Groups|Email|Name)\s*:/i.test(text);
if (!res.ok || !looksLikeWhoami) {
return { ok: false, user: "", name: "", email: "", groups: [], raw: text };
}
var groups = parseWhoamiLine(text, "Remote-Groups")
.split(/[;,]/)
.map((s) => s.trim())
.filter(Boolean)
@@ -116,18 +173,22 @@ const ISSUE_BRIDGE_PATH = import.meta.env.PUBLIC_ISSUE_BRIDGE_PATH ?? "";
})().catch(() => ({ ok: false, user: "", name: "", email: "", groups: [], raw: "" }));
}
// readers + editors (strict)
if (!window.__archiCanReadP) {
window.__archiCanReadP = window.__archiAuthInfoP.then((info) =>
Boolean(info.ok && (inGroup(info.groups, READ_GROUP) || inGroup(info.groups, REQUIRED_GROUP)))
Boolean(info && info.ok && (inGroup(info.groups, READ_GROUP) || inGroup(info.groups, REQUIRED_GROUP)))
);
}
// editors gate for "Proposer"
if (!window.__archiIsEditorP) {
window.__archiIsEditorP = window.__archiAuthInfoP
.then((info) => Boolean(inGroup(info.groups, REQUIRED_GROUP) || (__DEV__ && !info.ok)))
.catch(() => false);
// ✅ DEV fallback: si whoami absent/KO => Proposer autorisé (comme ton intention initiale)
.then((info) => Boolean(inGroup(info.groups, REQUIRED_GROUP) || (__DEV__ && !(info && info.ok))))
.catch(() => Boolean(__DEV__));
}
})();
</script>
</head>
@@ -950,11 +1011,13 @@ const ISSUE_BRIDGE_PATH = import.meta.env.PUBLIC_ISSUE_BRIDGE_PATH ?? "";
safe("propose-gate", () => {
if (!giteaReady) return;
const p = window.__archiIsEditorP || Promise.resolve(false);
p.then((ok) => {
document.querySelectorAll(".para-propose").forEach((el) => {
if (ok) showEl(el);
else el.remove();
else hideEl(el); // ✅ jamais remove => antifragile
});
}).catch((err) => {
console.warn("[proposer] gate failed; keeping Proposer hidden", err);

View File

@@ -0,0 +1,197 @@
import type { APIRoute } from "astro";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { parse as parseYAML } from "yaml";
const CWD = process.cwd();
const ANNO_DIR = path.join(CWD, "src", "annotations");
// Strict en CI (ou override explicite)
const STRICT =
process.env.ANNOTATIONS_STRICT === "1" ||
process.env.CI === "1" ||
process.env.CI === "true";
async function exists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function walk(dir: string): Promise<string[]> {
const out: string[] = [];
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 isPlainObject(x: unknown): x is Record<string, unknown> {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function normalizePageKey(s: unknown): string {
return String(s ?? "")
.replace(/^\/+/, "")
.replace(/\/+$/, "")
.trim();
}
function inferPageKeyFromFile(inDirAbs: string, fileAbs: string): string {
const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/");
return rel.replace(/\.(ya?ml|json)$/i, "");
}
function parseDoc(raw: string, fileAbs: string): unknown {
if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
return parseYAML(raw);
}
function hardFailOrCollect(errors: string[], msg: string): void {
if (STRICT) throw new Error(msg);
errors.push(msg);
}
function sanitizeEntry(
fileRel: string,
paraId: string,
entry: unknown,
errors: string[]
): Record<string, unknown> {
if (entry == null) return {};
if (!isPlainObject(entry)) {
hardFailOrCollect(errors, `${fileRel}: paras.${paraId} must be an object`);
return {};
}
const e: Record<string, unknown> = { ...entry };
const arrayFields = [
"refs",
"authors",
"quotes",
"media",
"comments_editorial",
] as const;
for (const k of arrayFields) {
if (e[k] == null) continue;
if (!Array.isArray(e[k])) {
errors.push(`${fileRel}: paras.${paraId}.${k} must be an array (coerced to [])`);
e[k] = [];
}
}
return e;
}
export const GET: APIRoute = async () => {
if (!(await exists(ANNO_DIR))) {
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
pages: {},
stats: { pages: 0, paras: 0, errors: 0 },
errors: [] as string[],
};
return new Response(JSON.stringify(out), {
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
});
}
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
const pages: Record<string, { paras: Record<string, Record<string, unknown>> }> =
Object.create(null);
const errors: string[] = [];
let paraCount = 0;
for (const f of files) {
const fileRel = path.relative(CWD, f).replace(/\\/g, "/");
const pageKey = normalizePageKey(inferPageKeyFromFile(ANNO_DIR, f));
if (!pageKey) {
hardFailOrCollect(errors, `${fileRel}: cannot infer page key`);
continue;
}
let doc: unknown;
try {
const raw = await fs.readFile(f, "utf8");
doc = parseDoc(raw, f);
} catch (e) {
hardFailOrCollect(errors, `${fileRel}: parse failed: ${String((e as any)?.message ?? e)}`);
continue;
}
if (!isPlainObject(doc) || (doc as any).schema !== 1) {
hardFailOrCollect(errors, `${fileRel}: schema must be 1`);
continue;
}
if ((doc as any).page != null) {
const declared = normalizePageKey((doc as any).page);
if (declared !== pageKey) {
hardFailOrCollect(
errors,
`${fileRel}: page mismatch (page="${declared}" vs path="${pageKey}")`
);
}
}
const parasAny = (doc as any).paras;
if (!isPlainObject(parasAny)) {
hardFailOrCollect(errors, `${fileRel}: missing object key "paras"`);
continue;
}
if (pages[pageKey]) {
hardFailOrCollect(errors, `${fileRel}: duplicate page "${pageKey}" (only one file per page)`);
continue;
}
const parasOut: Record<string, Record<string, unknown>> = Object.create(null);
for (const [paraId, entry] of Object.entries(parasAny)) {
if (!/^p-\d+-/i.test(paraId)) {
hardFailOrCollect(errors, `${fileRel}: invalid para id "${paraId}"`);
continue;
}
parasOut[paraId] = sanitizeEntry(fileRel, paraId, entry, errors);
}
pages[pageKey] = { paras: parasOut };
paraCount += Object.keys(parasOut).length;
}
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
pages,
stats: {
pages: Object.keys(pages).length,
paras: paraCount,
errors: errors.length,
},
errors,
};
return new Response(JSON.stringify(out), {
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
});
};

View File

@@ -0,0 +1,42 @@
import type { APIRoute } from "astro";
import * as fs from "node:fs/promises";
import * as path from "node:path";
export const prerender = true;
async function exists(p: string) {
try { await fs.access(p); return true; } catch { return false; }
}
export const GET: APIRoute = async () => {
const distFile = path.join(process.cwd(), "dist", "para-index.json");
// Si dist existe (ex: après un build), on renvoie le vrai fichier.
if (await exists(distFile)) {
const raw = await fs.readFile(distFile, "utf8");
return new Response(raw, {
status: 200,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
},
});
}
// Sinon stub (dev sans build) : pas derreur, pas de crash, pas de 404.
const stub = {
schema: 1,
generatedAt: new Date().toISOString(),
items: [],
byId: {},
note: "para-index not built yet (run: npm run build to generate dist/para-index.json)",
};
return new Response(JSON.stringify(stub), {
status: 200,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
},
});
};

View File

@@ -1,8 +1,4 @@
{
"archicratie/00-demarrage/index.html": [
"p-0-d64c1c39",
"p-1-3f750540"
],
"archicrat-ia/chapitre-1/index.html": [
"p-0-8d27a7f5",
"p-1-8a6c18bf",