Compare commits
8 Commits
fix/canoni
...
chore/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ef538a0c4 | |||
| 7b135a4707 | |||
| 0cb8a54195 | |||
| a7a333397d | |||
| eb1d444776 | |||
| 68c3416594 | |||
| ae809e0152 | |||
| 9bbebf5886 |
@@ -3,7 +3,7 @@ name: "Correction paragraphe"
|
|||||||
about: "Proposer une correction ciblée (un paragraphe) avec justification."
|
about: "Proposer une correction ciblée (un paragraphe) avec justification."
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chemin (ex: /archicratie/prologue/)
|
## Chemin (ex: /archicrat-ia/prologue/)
|
||||||
<!-- obligatoire -->
|
<!-- obligatoire -->
|
||||||
/...
|
/...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: "Vérification factuelle / sources"
|
|||||||
about: "Signaler une assertion à sourcer ou à corriger (preuves, références)."
|
about: "Signaler une assertion à sourcer ou à corriger (preuves, références)."
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chemin (ex: /archicratie/prologue/)
|
## Chemin (ex: /archicrat-ia/prologue/)
|
||||||
<!-- obligatoire -->
|
<!-- obligatoire -->
|
||||||
/...
|
/...
|
||||||
|
|
||||||
|
|||||||
259
.gitea/workflows/anno-apply-pr.yml
Normal file
259
.gitea/workflows/anno-apply-pr.yml
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
apply-approved:
|
||||||
|
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: 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/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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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/anno.env
|
||||||
|
|
||||||
|
- name: Gate on label state/approved
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/anno.env
|
||||||
|
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
||||||
|
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||||
|
echo 'SKIP=1' >> /tmp/anno.env
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/anno.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
- name: Apply ticket on bot branch (strict+verify, commit)
|
||||||
|
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 -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
|
||||||
|
|
||||||
|
tail -n 160 "$LOG" || true
|
||||||
|
|
||||||
|
END_SHA="$(git rev-parse HEAD)"
|
||||||
|
if [[ "$RC" -ne 0 ]]; then
|
||||||
|
echo "APPLY_RC=$RC" >> /tmp/anno.env
|
||||||
|
exit "$RC"
|
||||||
|
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 if no-op (already applied)
|
||||||
|
env:
|
||||||
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/anno.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||||
|
[[ "${NOOP:-0}" == "1" ]] || exit 0
|
||||||
|
|
||||||
|
MSG="ℹ️ Ticket #${ISSUE_NUMBER} : rien à appliquer (déjà présent / dédupliqué)."
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/anno.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||||
|
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> 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
|
||||||
|
env:
|
||||||
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/anno.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || 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"
|
||||||
98
.gitea/workflows/anno-reject.yml
Normal file
98
.gitea/workflows/anno-reject.yml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: Anno Reject
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled]
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
reject:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Derive context
|
||||||
|
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") : "");
|
||||||
|
if (!cloneUrl) throw new Error("No repository url");
|
||||||
|
|
||||||
|
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 issueNumber = ev?.issue?.number || ev?.issue?.index;
|
||||||
|
if (!issueNumber) throw new Error("No issue number");
|
||||||
|
|
||||||
|
const labelName = ev?.label?.name || ev?.label || "";
|
||||||
|
const u = new URL(cloneUrl);
|
||||||
|
|
||||||
|
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(u.origin)}`
|
||||||
|
].join("\n") + "\n");
|
||||||
|
NODE
|
||||||
|
|
||||||
|
- name: Gate on label state/rejected
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/reject.env
|
||||||
|
if [[ "$LABEL_NAME" != "state/rejected" ]]; then
|
||||||
|
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "✅ reject issue=$ISSUE_NUMBER"
|
||||||
|
|
||||||
|
- name: Comment + close issue
|
||||||
|
env:
|
||||||
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/reject.env
|
||||||
|
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||||
|
|
||||||
|
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"}'
|
||||||
@@ -79,22 +79,7 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
- name: Inline scripts syntax check
|
- name: Full test suite (CI=1)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
node scripts/check-inline-js.mjs
|
npm run ci
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|||||||
@@ -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
4
.gitignore
vendored
@@ -3,6 +3,10 @@
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# dev-only
|
||||||
|
public/_auth/whoami
|
||||||
|
public/_auth/whoami/*
|
||||||
|
|
||||||
# --- local backups ---
|
# --- local backups ---
|
||||||
*.bak
|
*.bak
|
||||||
*.bak.*
|
*.bak.*
|
||||||
|
|||||||
473
package-lock.json
generated
473
package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.3.13",
|
"@astrojs/mdx": "^4.3.13",
|
||||||
"astro": "^5.16.11"
|
"astro": "^5.17.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/sitemap": "^3.7.0",
|
"@astrojs/sitemap": "^3.7.0",
|
||||||
@@ -1905,9 +1905,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/astro": {
|
"node_modules/astro": {
|
||||||
"version": "5.16.11",
|
"version": "5.17.3",
|
||||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/astro/-/astro-5.17.3.tgz",
|
||||||
"integrity": "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA==",
|
"integrity": "sha512-69dcfPe8LsHzklwj+hl+vunWUbpMB6pmg35mACjetxbJeUNNys90JaBM8ZiwsPK689SAj/4Zqb1ayaANls9/MA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.13.0",
|
"@astrojs/compiler": "^2.13.0",
|
||||||
@@ -1933,7 +1933,7 @@
|
|||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"dset": "^3.1.4",
|
"dset": "^3.1.4",
|
||||||
"es-module-lexer": "^1.7.0",
|
"es-module-lexer": "^1.7.0",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.27.3",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"flattie": "^1.1.1",
|
"flattie": "^1.1.1",
|
||||||
"fontace": "~0.4.0",
|
"fontace": "~0.4.0",
|
||||||
@@ -1954,16 +1954,16 @@
|
|||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"rehype": "^13.0.2",
|
"rehype": "^13.0.2",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
"shiki": "^3.20.0",
|
"shiki": "^3.21.0",
|
||||||
"smol-toml": "^1.6.0",
|
"smol-toml": "^1.6.0",
|
||||||
"svgo": "^4.0.0",
|
"svgo": "^4.0.0",
|
||||||
"tinyexec": "^1.0.2",
|
"tinyexec": "^1.0.2",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"tsconfck": "^3.1.6",
|
"tsconfck": "^3.1.6",
|
||||||
"ultrahtml": "^1.6.0",
|
"ultrahtml": "^1.6.0",
|
||||||
"unifont": "~0.7.1",
|
"unifont": "~0.7.3",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"unstorage": "^1.17.3",
|
"unstorage": "^1.17.4",
|
||||||
"vfile": "^6.0.3",
|
"vfile": "^6.0.3",
|
||||||
"vite": "^6.4.1",
|
"vite": "^6.4.1",
|
||||||
"vitefu": "^1.1.1",
|
"vitefu": "^1.1.1",
|
||||||
@@ -1990,6 +1990,463 @@
|
|||||||
"sharp": "^0.34.0"
|
"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": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -4,32 +4,29 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "node scripts/write-dev-whoami.mjs && astro dev",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
|
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"build:clean": "npm run clean && npm run build",
|
"build:clean": "npm run clean && npm run build",
|
||||||
|
"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",
|
||||||
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && npx pagefind --site dist",
|
|
||||||
|
|
||||||
"import": "node scripts/import-docx.mjs",
|
"import": "node scripts/import-docx.mjs",
|
||||||
"apply:ticket": "node scripts/apply-ticket.mjs",
|
"apply:ticket": "node scripts/apply-ticket.mjs",
|
||||||
|
|
||||||
"audit:dist": "node scripts/audit-dist.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:aliases": "node scripts/check-anchor-aliases.mjs",
|
||||||
"test:anchors": "node scripts/check-anchors.mjs",
|
"test:anchors": "node scripts/check-anchors.mjs",
|
||||||
"test:anchors:update": "node scripts/check-anchors.mjs --update",
|
"test:anchors:update": "node scripts/check-anchors.mjs --update",
|
||||||
|
"test:annotations": "node scripts/check-annotations.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 && node scripts/check-inline-js.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"
|
"ci": "CI=1 npm test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.3.13",
|
"@astrojs/mdx": "^4.3.13",
|
||||||
"astro": "^5.16.11"
|
"astro": "^5.17.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/sitemap": "^3.7.0",
|
"@astrojs/sitemap": "^3.7.0",
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ function getAlias(aliases, pageKey, oldId) {
|
|||||||
// supporte:
|
// supporte:
|
||||||
// 1) { "<pageKey>": { "<old>": "<new>" } }
|
// 1) { "<pageKey>": { "<old>": "<new>" } }
|
||||||
// 2) { "<old>": "<new>" }
|
// 2) { "<old>": "<new>" }
|
||||||
const a1 = aliases?.[pageKey]?.[oldId];
|
const k1 = String(pageKey || "");
|
||||||
if (a1) return a1;
|
const k2 = k1 ? ("/" + k1.replace(/^\/+|\/+$/g, "") + "/") : "";
|
||||||
|
const a1 = (aliases?.[k1]?.[oldId]) || (k2 ? aliases?.[k2]?.[oldId] : "");
|
||||||
|
if (a1) return String(a1);
|
||||||
const a2 = aliases?.[oldId];
|
const a2 = aliases?.[oldId];
|
||||||
if (a2) return a2;
|
if (a2) return String(a2);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,24 @@ const STRICT = argv.includes("--strict") || process.env.CI === "1" || process.en
|
|||||||
function escRe(s) {
|
function escRe(s) {
|
||||||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
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) {
|
function countIdAttr(html, id) {
|
||||||
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "gi");
|
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "gi");
|
||||||
let c = 0;
|
let c = 0;
|
||||||
@@ -22,7 +40,6 @@ function countIdAttr(html, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findStartTagWithId(html, id) {
|
function findStartTagWithId(html, id) {
|
||||||
// 1er élément qui porte id="..."
|
|
||||||
const re = new RegExp(
|
const re = new RegExp(
|
||||||
`<([a-zA-Z0-9:-]+)\\b[^>]*\\bid=(["'])${escRe(id)}\\2[^>]*>`,
|
`<([a-zA-Z0-9:-]+)\\b[^>]*\\bid=(["'])${escRe(id)}\\2[^>]*>`,
|
||||||
"i"
|
"i"
|
||||||
@@ -36,34 +53,10 @@ function isInjectedAliasSpan(html, id) {
|
|||||||
const found = findStartTagWithId(html, id);
|
const found = findStartTagWithId(html, id);
|
||||||
if (!found) return false;
|
if (!found) return false;
|
||||||
if (found.tagName !== "span") return false;
|
if (found.tagName !== "span") return false;
|
||||||
// class="... para-alias ..."
|
|
||||||
return /\bclass=(["'])(?:(?!\1).)*\bpara-alias\b(?:(?!\1).)*\1/i.test(found.tag);
|
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) {
|
function injectBeforeId(html, newId, injectHtml) {
|
||||||
// insère juste avant la balise qui porte id="newId"
|
|
||||||
const re = new RegExp(
|
const re = new RegExp(
|
||||||
`(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`,
|
`(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`,
|
||||||
"i"
|
"i"
|
||||||
@@ -82,6 +75,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const raw = await fs.readFile(ALIASES_PATH, "utf-8");
|
const raw = await fs.readFile(ALIASES_PATH, "utf-8");
|
||||||
|
|
||||||
/** @type {Record<string, Record<string,string>>} */
|
/** @type {Record<string, Record<string,string>>} */
|
||||||
let aliases;
|
let aliases;
|
||||||
try {
|
try {
|
||||||
@@ -89,6 +83,7 @@ async function main() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`JSON invalide: ${ALIASES_PATH} (${e?.message || e})`);
|
throw new Error(`JSON invalide: ${ALIASES_PATH} (${e?.message || e})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!aliases || typeof aliases !== "object" || Array.isArray(aliases)) {
|
if (!aliases || typeof aliases !== "object" || Array.isArray(aliases)) {
|
||||||
throw new Error(`Format invalide: attendu { route: { oldId: newId } } dans ${ALIASES_PATH}`);
|
throw new Error(`Format invalide: attendu { route: { oldId: newId } } dans ${ALIASES_PATH}`);
|
||||||
}
|
}
|
||||||
@@ -117,7 +112,7 @@ async function main() {
|
|||||||
|
|
||||||
if (entries.length === 0) continue;
|
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");
|
const htmlPath = path.join(DIST_ROOT, rel, "index.html");
|
||||||
|
|
||||||
if (!(await exists(htmlPath))) {
|
if (!(await exists(htmlPath))) {
|
||||||
@@ -135,24 +130,8 @@ async function main() {
|
|||||||
if (!oldId || !newId) continue;
|
if (!oldId || !newId) continue;
|
||||||
|
|
||||||
const oldCount = countIdAttr(html, oldId);
|
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">)
|
// ✅ déjà injecté => idempotent
|
||||||
// => 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
|
|
||||||
if (oldCount > 0 && isInjectedAliasSpan(html, oldId)) {
|
if (oldCount > 0 && isInjectedAliasSpan(html, oldId)) {
|
||||||
if (STRICT && oldCount !== 1) {
|
if (STRICT && oldCount !== 1) {
|
||||||
throw new Error(`oldId dupliqué (${oldCount}) alors qu'il est censé être unique: ${route} id=${oldId}`);
|
throw new Error(`oldId dupliqué (${oldCount}) alors qu'il est censé être unique: ${route} id=${oldId}`);
|
||||||
@@ -160,18 +139,23 @@ async function main() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// avant l'injection, après hasId(newId)
|
// ⛔️ oldId existe déjà "en vrai" => alias inutile/inversé
|
||||||
const newCount = countIdAttr(html, newId);
|
if (oldCount > 0) {
|
||||||
if (newCount !== 1) {
|
const found = findStartTagWithId(html, oldId);
|
||||||
const msg = `⚠️ newId non-unique (${newCount}) : ${route} new=${newId} (injection ambiguë)`;
|
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);
|
if (STRICT) throw new Error(msg);
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
warnCount++;
|
warnCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasId(html, newId)) {
|
// newId doit exister UNE fois (sinon injection ambiguë)
|
||||||
const msg = `⚠️ newId introuvable: ${route} old=${oldId} -> new=${newId}`;
|
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);
|
if (STRICT) throw new Error(msg);
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
warnCount++;
|
warnCount++;
|
||||||
|
|||||||
31
scripts/purge-dist-dev-whoami.mjs
Normal file
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);
|
||||||
|
});
|
||||||
@@ -205,7 +205,7 @@ for (const [route, mapping] of Object.entries(data)) {
|
|||||||
newId,
|
newId,
|
||||||
htmlPath,
|
htmlPath,
|
||||||
msg:
|
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}`,
|
`Saw: ${seen}`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
26
scripts/write-dev-whoami.mjs
Normal file
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})`);
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
schema: 1
|
schema: 1
|
||||||
|
|
||||||
# optionnel (si présent, doit matcher le chemin du fichier)
|
|
||||||
page: archicratie/archicrat-ia/prologue
|
|
||||||
|
|
||||||
paras:
|
paras:
|
||||||
p-0-d7974f88:
|
p-0-d7974f88:
|
||||||
refs:
|
refs:
|
||||||
@@ -50,10 +47,4 @@ paras:
|
|||||||
- text: "Si l’on voulait chercher quelque chose comme une vision du monde chez Kafka..."
|
- text: "Si l’on voulait chercher quelque chose comme une vision du monde chez Kafka..."
|
||||||
source: "Bernard Lahire, Franz Kafka, p.475+"
|
source: "Bernard Lahire, Franz Kafka, p.475+"
|
||||||
|
|
||||||
media:
|
|
||||||
- type: "video"
|
|
||||||
src: "/media/prologue/p-1-2ef25f29/bien_commun.mp4"
|
|
||||||
caption: "Entretien avec Bernard Lahire"
|
|
||||||
credit: "Cairn.info"
|
|
||||||
|
|
||||||
comments_editorial: []
|
comments_editorial: []
|
||||||
|
|||||||
@@ -144,15 +144,14 @@
|
|||||||
const canReaders = inGroup(groups, "readers");
|
const canReaders = inGroup(groups, "readers");
|
||||||
const canEditors = inGroup(groups, "editors");
|
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;
|
access.ready = true;
|
||||||
|
|
||||||
if (btnMediaSubmit) btnMediaSubmit.disabled = !access.canUsers;
|
if (btnMediaSubmit) btnMediaSubmit.disabled = !access.canUsers;
|
||||||
if (btnSend) btnSend.disabled = !access.canUsers;
|
if (btnSend) btnSend.disabled = !access.canUsers;
|
||||||
|
|
||||||
if (btnRefSubmit) btnRefSubmit.disabled = !access.canUsers;
|
if (btnRefSubmit) btnRefSubmit.disabled = !access.canUsers;
|
||||||
|
|
||||||
|
|
||||||
// si pas d'accès, on informe (soft)
|
// si pas d'accès, on informe (soft)
|
||||||
if (!access.canUsers) {
|
if (!access.canUsers) {
|
||||||
if (msgHead) {
|
if (msgHead) {
|
||||||
@@ -162,12 +161,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// fallback dev
|
// fallback dev (cohérent: media + ref + comment)
|
||||||
access.ready = true;
|
access.ready = true;
|
||||||
if (isDev()) {
|
if (Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped)) {
|
||||||
access.canUsers = true;
|
access.canUsers = true;
|
||||||
if (btnMediaSubmit) btnMediaSubmit.disabled = false;
|
if (btnMediaSubmit) btnMediaSubmit.disabled = false;
|
||||||
if (btnSend) btnSend.disabled = false;
|
if (btnSend) btnSend.disabled = false;
|
||||||
|
if (btnRefSubmit) btnRefSubmit.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,8 +209,12 @@
|
|||||||
async function loadIndex() {
|
async function loadIndex() {
|
||||||
if (_idxP) return _idxP;
|
if (_idxP) return _idxP;
|
||||||
_idxP = (async () => {
|
_idxP = (async () => {
|
||||||
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" }).catch(() => null);
|
try {
|
||||||
|
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" });
|
||||||
if (res && res.ok) return await res.json();
|
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 null;
|
||||||
})();
|
})();
|
||||||
return _idxP;
|
return _idxP;
|
||||||
@@ -564,6 +568,14 @@
|
|||||||
hideMsg(msgComment);
|
hideMsg(msgComment);
|
||||||
|
|
||||||
const idx = await loadIndex();
|
const idx = await loadIndex();
|
||||||
|
|
||||||
|
// ✅ message soft si l’index est indisponible (sans écraser le message d’auth)
|
||||||
|
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;
|
const data = idx?.pages?.[pageKey]?.paras?.[currentParaId] || null;
|
||||||
|
|
||||||
renderLevel2(data);
|
renderLevel2(data);
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
|
|
||||||
// ✅ OPTIONNEL : bridge serveur (proxy same-origin)
|
// ✅ OPTIONNEL : bridge serveur (proxy same-origin)
|
||||||
const ISSUE_BRIDGE_PATH = import.meta.env.PUBLIC_ISSUE_BRIDGE_PATH ?? "";
|
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 l’auth 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>
|
<!doctype html>
|
||||||
@@ -52,54 +59,104 @@ const ISSUE_BRIDGE_PATH = import.meta.env.PUBLIC_ISSUE_BRIDGE_PATH ?? "";
|
|||||||
<meta data-pagefind-meta={`version:${String(version ?? "")}`} />
|
<meta data-pagefind-meta={`version:${String(version ?? "")}`} />
|
||||||
|
|
||||||
{/* ✅ BOOT EARLY : SidePanel dépend de ces globals. */}
|
{/* ✅ 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);
|
// ✅ anti double-init (HMR / inclusion accidentelle)
|
||||||
window.__archiFlags = Object.assign({}, window.__archiFlags, { dev: __DEV__ });
|
if (window.__archiBootOnce === 1) return;
|
||||||
|
window.__archiBootOnce = 1;
|
||||||
|
|
||||||
const base = String(GITEA_BASE || "").replace(/\/+$/, "");
|
var __DEV__ = Boolean(IS_DEV);
|
||||||
const owner = String(GITEA_OWNER || "");
|
|
||||||
const repo = String(GITEA_REPO || "");
|
|
||||||
const giteaReady = Boolean(base && owner && repo);
|
|
||||||
window.__archiGitea = { ready: giteaReady, base, owner, repo };
|
|
||||||
|
|
||||||
const rawBridge = String(ISSUE_BRIDGE_PATH || "").trim();
|
// ===== Gitea globals =====
|
||||||
const normBridge = rawBridge
|
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(/\/+$/, "")
|
? (rawBridge.startsWith("/") ? rawBridge : ("/" + rawBridge.replace(/^\/+/, ""))).replace(/\/+$/, "")
|
||||||
: "";
|
: "";
|
||||||
window.__archiIssueBridge = { ready: Boolean(normBridge), path: normBridge };
|
window.__archiIssueBridge = { ready: Boolean(normBridge), path: normBridge };
|
||||||
|
|
||||||
const WHOAMI_PATH = "/_auth/whoami";
|
// ===== whoami config =====
|
||||||
const REQUIRED_GROUP = "editors";
|
var __WHOAMI_PATH__ = String(WHOAMI_PATH || "/_auth/whoami");
|
||||||
const READ_GROUP = "readers";
|
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) {
|
function parseWhoamiLine(text, key) {
|
||||||
const re = new RegExp(`^${key}:\\s*(.*)$`, "mi");
|
var re = new RegExp("^" + key + ":\\s*(.*)$", "mi");
|
||||||
const m = String(text || "").match(re);
|
var m = String(text || "").match(re);
|
||||||
return (m?.[1] ?? "").trim();
|
return (m && m[1] ? m[1] : "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function inGroup(groups, g) {
|
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);
|
return Array.isArray(groups) && groups.some((x) => String(x).toLowerCase() === gg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Auth info promise (single source of truth) =====
|
||||||
if (!window.__archiAuthInfoP) {
|
if (!window.__archiAuthInfoP) {
|
||||||
window.__archiAuthInfoP = (async () => {
|
window.__archiAuthInfoP = (async () => {
|
||||||
const res = await fetch(`${WHOAMI_PATH}?_=${Date.now()}`, {
|
// ✅ 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",
|
credentials: "include",
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
redirect: "manual",
|
redirect: "manual",
|
||||||
headers: { Accept: "text/plain" },
|
headers: { Accept: "text/plain" },
|
||||||
}).catch(() => null);
|
});
|
||||||
|
} catch {
|
||||||
|
res = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!res) return { ok: false, user: "", name: "", email: "", groups: [], raw: "" };
|
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 >= 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(() => "");
|
var text = "";
|
||||||
const looksLikeWhoami = /Remote-(User|Groups|Email|Name)\s*:/i.test(text);
|
try { text = await res.text(); } catch { text = ""; }
|
||||||
if (!res.ok || !looksLikeWhoami) return { ok: false, user: "", name: "", email: "", groups: [], raw: 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(/[;,]/)
|
.split(/[;,]/)
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.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: "" }));
|
})().catch(() => ({ ok: false, user: "", name: "", email: "", groups: [], raw: "" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readers + editors (strict)
|
||||||
if (!window.__archiCanReadP) {
|
if (!window.__archiCanReadP) {
|
||||||
window.__archiCanReadP = window.__archiAuthInfoP.then((info) =>
|
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) {
|
if (!window.__archiIsEditorP) {
|
||||||
window.__archiIsEditorP = window.__archiAuthInfoP
|
window.__archiIsEditorP = window.__archiAuthInfoP
|
||||||
.then((info) => Boolean(inGroup(info.groups, REQUIRED_GROUP) || (__DEV__ && !info.ok)))
|
// ✅ DEV fallback: si whoami absent/KO => Proposer autorisé (comme ton intention initiale)
|
||||||
.catch(() => false);
|
.then((info) => Boolean(inGroup(info.groups, REQUIRED_GROUP) || (__DEV__ && !(info && info.ok))))
|
||||||
|
.catch(() => Boolean(__DEV__));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -950,11 +1011,13 @@ const ISSUE_BRIDGE_PATH = import.meta.env.PUBLIC_ISSUE_BRIDGE_PATH ?? "";
|
|||||||
|
|
||||||
safe("propose-gate", () => {
|
safe("propose-gate", () => {
|
||||||
if (!giteaReady) return;
|
if (!giteaReady) return;
|
||||||
|
|
||||||
const p = window.__archiIsEditorP || Promise.resolve(false);
|
const p = window.__archiIsEditorP || Promise.resolve(false);
|
||||||
|
|
||||||
p.then((ok) => {
|
p.then((ok) => {
|
||||||
document.querySelectorAll(".para-propose").forEach((el) => {
|
document.querySelectorAll(".para-propose").forEach((el) => {
|
||||||
if (ok) showEl(el);
|
if (ok) showEl(el);
|
||||||
else el.remove();
|
else hideEl(el); // ✅ jamais remove => antifragile
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn("[proposer] gate failed; keeping Proposer hidden", err);
|
console.warn("[proposer] gate failed; keeping Proposer hidden", err);
|
||||||
|
|||||||
197
src/pages/annotations-index.json.ts
Normal file
197
src/pages/annotations-index.json.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
42
src/pages/para-index.json.ts
Normal file
42
src/pages/para-index.json.ts
Normal 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 d’erreur, 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"archicratie/00-demarrage/index.html": [
|
|
||||||
"p-0-d64c1c39",
|
|
||||||
"p-1-3f750540"
|
|
||||||
],
|
|
||||||
"archicrat-ia/chapitre-1/index.html": [
|
"archicrat-ia/chapitre-1/index.html": [
|
||||||
"p-0-8d27a7f5",
|
"p-0-8d27a7f5",
|
||||||
"p-1-8a6c18bf",
|
"p-1-8a6c18bf",
|
||||||
|
|||||||
Reference in New Issue
Block a user