Compare commits
15 Commits
hotfix/fix
...
f9d34110e4
| Author | SHA1 | Date | |
|---|---|---|---|
| f9d34110e4 | |||
|
|
84e9c3ead4 | ||
|
|
72e59175fc | ||
| 81b69ac6d5 | |||
| 513ae72e85 | |||
| 4c4dd1c515 | |||
| 46b15ed6ab | |||
| a015e72f7c | |||
|
|
d5df7d77a0 | ||
| ec3ceee862 | |||
| 867475c3ff | |||
| b024c5557c | |||
| 93306f360d | |||
| 52847d999d | |||
| 06482a9f8d |
@@ -122,21 +122,25 @@ jobs:
|
|||||||
echo "Context:"
|
echo "Context:"
|
||||||
sed -n '1,200p' /tmp/proposer.env
|
sed -n '1,200p' /tmp/proposer.env
|
||||||
|
|
||||||
- name: Early gate
|
- name: Early gate (tolerant on empty issue label payload)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/proposer.env
|
source /tmp/proposer.env
|
||||||
|
|
||||||
|
echo "event=$EVENT_NAME label=${LABEL_NAME:-<empty>}"
|
||||||
|
|
||||||
if [[ "$EVENT_NAME" == "issues" ]]; then
|
if [[ "$EVENT_NAME" == "issues" ]]; then
|
||||||
if [[ "$LABEL_NAME" != "state/approved" ]]; then
|
# Gitea peut fournir un payload "issues/labeled" sans label exploitable.
|
||||||
echo "issues/labeled but label=$LABEL_NAME -> skip"
|
# On ne skip QUE si le label est explicitement présent ET différent de state/approved.
|
||||||
|
if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/approved" ]]; then
|
||||||
|
echo "issues/labeled with explicit non-approved label=$LABEL_NAME -> skip"
|
||||||
echo 'SKIP=1' >> /tmp/proposer.env
|
echo 'SKIP=1' >> /tmp/proposer.env
|
||||||
echo 'SKIP_REASON="label_not_state_approved"' >> /tmp/proposer.env
|
echo 'SKIP_REASON="label_not_state_approved_event"' >> /tmp/proposer.env
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Proceed"
|
echo "Proceed to API-based selection/gating"
|
||||||
|
|
||||||
- name: Checkout default branch
|
- name: Checkout default branch
|
||||||
run: |
|
run: |
|
||||||
@@ -284,6 +288,11 @@ jobs:
|
|||||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||||
[[ "${EVENT_NAME:-}" != "push" ]] || exit 0
|
[[ "${EVENT_NAME:-}" != "push" ]] || exit 0
|
||||||
|
|
||||||
|
if [[ "${SKIP_REASON:-}" == "label_not_state_approved_event" || "${SKIP_REASON:-}" == "label_not_state_approved" ]]; then
|
||||||
|
echo "Skip reason=${SKIP_REASON} -> no comment"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
test -n "${FORGE_TOKEN:-}" || exit 0
|
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||||
|
|
||||||
ISSUE_TO_COMMENT="${ISSUE_NUMBER:-0}"
|
ISSUE_TO_COMMENT="${ISSUE_NUMBER:-0}"
|
||||||
@@ -546,22 +555,20 @@ jobs:
|
|||||||
[[ "${NOOP:-0}" == "0" ]] || exit 0
|
[[ "${NOOP:-0}" == "0" ]] || exit 0
|
||||||
[[ -n "${BRANCH:-}" ]] || { echo "BRANCH unset -> skip PR"; exit 0; }
|
[[ -n "${BRANCH:-}" ]] || { echo "BRANCH unset -> skip PR"; exit 0; }
|
||||||
|
|
||||||
|
test -n "${FORGE_TOKEN:-}" || { echo "Missing FORGE_TOKEN"; exit 1; }
|
||||||
|
|
||||||
if [[ "${TARGET_COUNT:-0}" == "1" ]]; then
|
if [[ "${TARGET_COUNT:-0}" == "1" ]]; then
|
||||||
PR_TITLE="proposer: apply ticket #${TARGET_PRIMARY_ISSUE}"
|
PR_TITLE="proposer: apply ticket #${TARGET_PRIMARY_ISSUE}"
|
||||||
else
|
else
|
||||||
PR_TITLE="proposer: apply ${TARGET_COUNT} tickets on ${TARGET_CHEMIN}"
|
PR_TITLE="proposer: apply ${TARGET_COUNT} tickets on ${TARGET_CHEMIN}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export TITLE="$PR_TITLE"
|
export PR_TITLE TARGET_CHEMIN TARGET_ISSUES BRANCH END_SHA DEFAULT_BRANCH OWNER
|
||||||
export CHEMIN="$TARGET_CHEMIN"
|
|
||||||
export ISSUES="$TARGET_ISSUES"
|
|
||||||
export BRANCH="$BRANCH"
|
|
||||||
export END_SHA="${END_SHA:-unknown}"
|
|
||||||
export DEFAULT_BRANCH="$DEFAULT_BRANCH"
|
|
||||||
export OWNER="$OWNER"
|
|
||||||
|
|
||||||
node --input-type=module - <<'NODE' > /tmp/proposer.pr.json
|
node --input-type=module -e '
|
||||||
const issues = String(process.env.ISSUES || "")
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const issues = String(process.env.TARGET_ISSUES || "")
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@@ -569,7 +576,7 @@ jobs:
|
|||||||
const body = [
|
const body = [
|
||||||
`PR auto depuis ticket${issues.length > 1 ? "s" : ""} ${issues.map((n) => `#${n}`).join(", ")} (state/approved).`,
|
`PR auto depuis ticket${issues.length > 1 ? "s" : ""} ${issues.map((n) => `#${n}`).join(", ")} (state/approved).`,
|
||||||
"",
|
"",
|
||||||
`- Chemin: ${process.env.CHEMIN || "(inconnu)"}`,
|
`- Chemin: ${process.env.TARGET_CHEMIN || "(inconnu)"}`,
|
||||||
"- Tickets:",
|
"- Tickets:",
|
||||||
...issues.map((n) => ` - #${n}`),
|
...issues.map((n) => ` - #${n}`),
|
||||||
`- Branche: ${process.env.BRANCH || ""}`,
|
`- Branche: ${process.env.BRANCH || ""}`,
|
||||||
@@ -578,14 +585,108 @@ jobs:
|
|||||||
"Merge si CI OK."
|
"Merge si CI OK."
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
process.stdout.write(JSON.stringify({
|
fs.writeFileSync(
|
||||||
title: process.env.TITLE || "proposer: apply tickets",
|
"/tmp/proposer.pr.json",
|
||||||
|
JSON.stringify({
|
||||||
|
title: process.env.PR_TITLE || "proposer: apply tickets",
|
||||||
body,
|
body,
|
||||||
base: process.env.DEFAULT_BRANCH || "main",
|
base: process.env.DEFAULT_BRANCH || "main",
|
||||||
head: `${process.env.OWNER}:${process.env.BRANCH}`,
|
head: `${process.env.OWNER}:${process.env.BRANCH}`,
|
||||||
allow_maintainer_edit: true
|
allow_maintainer_edit: true
|
||||||
}));
|
})
|
||||||
NODE
|
);
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "Creating proposer PR..."
|
||||||
|
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 @/tmp/proposer.pr.json)"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "PR created: $PR_URL"
|
||||||
|
|
||||||
|
for ISSUE in $TARGET_ISSUES; do
|
||||||
|
export ISSUE PR_URL
|
||||||
|
|
||||||
|
node --input-type=module -e '
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const issue = process.env.ISSUE || "";
|
||||||
|
const url = process.env.PR_URL || "";
|
||||||
|
const msg =
|
||||||
|
`PR proposer créée pour le ticket #${issue} : ${url}\n\n` +
|
||||||
|
`Le ticket est clôturé automatiquement ; la discussion peut se poursuivre dans la PR.`;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
"/tmp/proposer.issue.close.comment.json",
|
||||||
|
JSON.stringify({ body: msg })
|
||||||
|
);
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "Commenting issue #$ISSUE ..."
|
||||||
|
COMMENT_HTTP="$(curl -sS -o /tmp/proposer.comment.out.json -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE/comments" \
|
||||||
|
--data-binary @/tmp/proposer.issue.close.comment.json || true)"
|
||||||
|
echo "Issue #$ISSUE comment HTTP=$COMMENT_HTTP"
|
||||||
|
|
||||||
|
if [[ ! "$COMMENT_HTTP" =~ ^2 ]]; then
|
||||||
|
echo "Failed to comment issue #$ISSUE"
|
||||||
|
cat /tmp/proposer.comment.out.json || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Closing issue #$ISSUE ..."
|
||||||
|
CLOSE_HTTP="$(curl -sS -o /tmp/proposer.close.out.json -w '%{http_code}' -X PATCH \
|
||||||
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" \
|
||||||
|
--data-binary '{"state":"closed"}' || true)"
|
||||||
|
echo "Issue #$ISSUE close HTTP=$CLOSE_HTTP"
|
||||||
|
|
||||||
|
if [[ ! "$CLOSE_HTTP" =~ ^2 ]]; then
|
||||||
|
echo "Failed to close issue #$ISSUE"
|
||||||
|
cat /tmp/proposer.close.out.json || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Verifying issue #$ISSUE state ..."
|
||||||
|
VERIFY_HTTP="$(curl -sS -o /tmp/proposer.verify.out.json -w '%{http_code}' \
|
||||||
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" || true)"
|
||||||
|
echo "Issue #$ISSUE verify HTTP=$VERIFY_HTTP"
|
||||||
|
|
||||||
|
if [[ ! "$VERIFY_HTTP" =~ ^2 ]]; then
|
||||||
|
echo "Failed to re-read issue #$ISSUE after close"
|
||||||
|
cat /tmp/proposer.verify.out.json || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ISSUE_STATE="$(node --input-type=module -e '
|
||||||
|
import fs from "node:fs";
|
||||||
|
const j = JSON.parse(fs.readFileSync("/tmp/proposer.verify.out.json", "utf8"));
|
||||||
|
console.log(String(j.state || ""));
|
||||||
|
')"
|
||||||
|
|
||||||
|
echo "Issue #$ISSUE state=$ISSUE_STATE"
|
||||||
|
[[ "$ISSUE_STATE" == "closed" ]] || {
|
||||||
|
echo "Issue #$ISSUE is not closed after PATCH"
|
||||||
|
cat /tmp/proposer.verify.out.json || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "PR: $PR_URL"
|
||||||
|
|
||||||
PR_JSON="$(curl -fsS -X POST \
|
PR_JSON="$(curl -fsS -X POST \
|
||||||
-H "Authorization: token $FORGE_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
@@ -593,10 +694,7 @@ jobs:
|
|||||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
|
||||||
--data-binary @/tmp/proposer.pr.json)"
|
--data-binary @/tmp/proposer.pr.json)"
|
||||||
|
|
||||||
PR_URL="$(node --input-type=module -e '
|
PR_URL="$(node --input-type=module -e 'const pr = JSON.parse(process.argv[1] || "{}"); console.log(pr.html_url || pr.url || "");' "$PR_JSON")"
|
||||||
const pr = JSON.parse(process.argv[1] || "{}");
|
|
||||||
console.log(pr.html_url || pr.url || "");
|
|
||||||
' "$PR_JSON")"
|
|
||||||
|
|
||||||
test -n "$PR_URL" || {
|
test -n "$PR_URL" || {
|
||||||
echo "PR URL missing. Raw: $PR_JSON"
|
echo "PR URL missing. Raw: $PR_JSON"
|
||||||
@@ -605,15 +703,21 @@ jobs:
|
|||||||
|
|
||||||
for ISSUE in $TARGET_ISSUES; do
|
for ISSUE in $TARGET_ISSUES; do
|
||||||
export ISSUE PR_URL
|
export ISSUE PR_URL
|
||||||
node --input-type=module - <<'NODE' > /tmp/proposer.issue.close.comment.json
|
|
||||||
|
node --input-type=module -e '
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
const issue = process.env.ISSUE || "";
|
const issue = process.env.ISSUE || "";
|
||||||
const url = process.env.PR_URL || "";
|
const url = process.env.PR_URL || "";
|
||||||
const msg =
|
const msg =
|
||||||
`PR proposer created for ticket #${issue}: ${url}\n\n` +
|
`PR proposer créée pour le ticket #${issue} : ${url}\n\n` +
|
||||||
`The ticket is closed automatically. Discussion can continue in the PR.`;
|
`Le ticket est clôturé automatiquement ; la discussion peut se poursuivre dans la PR.`;
|
||||||
|
|
||||||
process.stdout.write(JSON.stringify({ body: msg }));
|
fs.writeFileSync(
|
||||||
NODE
|
"/tmp/proposer.issue.close.comment.json",
|
||||||
|
JSON.stringify({ body: msg })
|
||||||
|
);
|
||||||
|
'
|
||||||
|
|
||||||
curl -fsS -X POST \
|
curl -fsS -X POST \
|
||||||
-H "Authorization: token $FORGE_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { spawnSync } from "node:child_process";
|
|||||||
*
|
*
|
||||||
* Conçu pour:
|
* Conçu pour:
|
||||||
* - prendre un ticket [Correction]/[Fact-check] (issue) avec Chemin + Ancre + Proposition
|
* - prendre un ticket [Correction]/[Fact-check] (issue) avec Chemin + Ancre + Proposition
|
||||||
* - retrouver le bon paragraphe dans le .mdx
|
* - retrouver le bon paragraphe dans le .mdx/.md
|
||||||
* - remplacer proprement
|
* - remplacer proprement
|
||||||
|
* - ne JAMAIS toucher au frontmatter
|
||||||
* - optionnel: écrire un alias d’ancre old->new (build-time) dans src/anchors/anchor-aliases.json
|
* - optionnel: écrire un alias d’ancre old->new (build-time) dans src/anchors/anchor-aliases.json
|
||||||
* - optionnel: committer automatiquement
|
* - optionnel: committer automatiquement
|
||||||
* - optionnel: fermer le ticket (après commit)
|
* - optionnel: fermer le ticket (après commit)
|
||||||
@@ -137,28 +138,17 @@ function scoreText(candidate, targetText) {
|
|||||||
let hit = 0;
|
let hit = 0;
|
||||||
for (const w of tgtSet) if (blkSet.has(w)) hit++;
|
for (const w of tgtSet) if (blkSet.has(w)) hit++;
|
||||||
|
|
||||||
// Bonus si un long préfixe ressemble
|
|
||||||
const tgtNorm = normalizeText(stripMd(targetText));
|
const tgtNorm = normalizeText(stripMd(targetText));
|
||||||
const blkNorm = normalizeText(stripMd(candidate));
|
const blkNorm = normalizeText(stripMd(candidate));
|
||||||
const prefix = tgtNorm.slice(0, Math.min(180, tgtNorm.length));
|
const prefix = tgtNorm.slice(0, Math.min(180, tgtNorm.length));
|
||||||
const prefixBonus = prefix && blkNorm.includes(prefix) ? 1000 : 0;
|
const prefixBonus = prefix && blkNorm.includes(prefix) ? 1000 : 0;
|
||||||
|
|
||||||
// Ratio bonus (0..100)
|
|
||||||
const ratio = hit / Math.max(1, tgtSet.size);
|
const ratio = hit / Math.max(1, tgtSet.size);
|
||||||
const ratioBonus = Math.round(ratio * 100);
|
const ratioBonus = Math.round(ratio * 100);
|
||||||
|
|
||||||
return prefixBonus + hit + ratioBonus;
|
return prefixBonus + hit + ratioBonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bestBlockMatchIndex(blocks, targetText) {
|
|
||||||
let best = { i: -1, score: -1 };
|
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
|
||||||
const sc = scoreText(blocks[i], targetText);
|
|
||||||
if (sc > best.score) best = { i, score: sc };
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rankedBlockMatches(blocks, targetText, limit = 5) {
|
function rankedBlockMatches(blocks, targetText, limit = 5) {
|
||||||
return blocks
|
return blocks
|
||||||
.map((b, i) => ({
|
.map((b, i) => ({
|
||||||
@@ -170,11 +160,6 @@ function rankedBlockMatches(blocks, targetText, limit = 5) {
|
|||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitParagraphBlocks(mdxText) {
|
|
||||||
const raw = String(mdxText ?? "").replace(/\r\n/g, "\n");
|
|
||||||
return raw.split(/\n{2,}/);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLikelyExcerpt(s) {
|
function isLikelyExcerpt(s) {
|
||||||
const t = String(s || "").trim();
|
const t = String(s || "").trim();
|
||||||
if (!t) return true;
|
if (!t) return true;
|
||||||
@@ -184,6 +169,89 @@ function isLikelyExcerpt(s) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------- frontmatter / structure ------------------------ */
|
||||||
|
|
||||||
|
function normalizeNewlines(s) {
|
||||||
|
return String(s ?? "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMdxFrontmatter(src) {
|
||||||
|
const text = normalizeNewlines(src);
|
||||||
|
const m = text.match(/^---\n[\s\S]*?\n---\n?/);
|
||||||
|
|
||||||
|
if (!m) {
|
||||||
|
return {
|
||||||
|
hasFrontmatter: false,
|
||||||
|
frontmatter: "",
|
||||||
|
body: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontmatter = m[0];
|
||||||
|
const body = text.slice(frontmatter.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasFrontmatter: true,
|
||||||
|
frontmatter,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinMdxFrontmatter(frontmatter, body) {
|
||||||
|
if (!frontmatter) return String(body ?? "");
|
||||||
|
return String(frontmatter) + String(body ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFrontmatterIntegrity({ hadFrontmatter, originalFrontmatter, finalText, filePath }) {
|
||||||
|
if (!hadFrontmatter) return;
|
||||||
|
|
||||||
|
const text = normalizeNewlines(finalText);
|
||||||
|
|
||||||
|
if (!text.startsWith("---\n")) {
|
||||||
|
throw new Error(`Frontmatter perdu pendant la mise à jour de ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text.startsWith(originalFrontmatter)) {
|
||||||
|
throw new Error(`Frontmatter altéré pendant la mise à jour de ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitParagraphBlocksPreserve(bodyText) {
|
||||||
|
const text = normalizeNewlines(bodyText);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return { blocks: [], separators: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = [];
|
||||||
|
const separators = [];
|
||||||
|
|
||||||
|
const re = /(\n{2,})/g;
|
||||||
|
let last = 0;
|
||||||
|
let m;
|
||||||
|
|
||||||
|
while ((m = re.exec(text))) {
|
||||||
|
blocks.push(text.slice(last, m.index));
|
||||||
|
separators.push(m[1]);
|
||||||
|
last = m.index + m[1].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push(text.slice(last));
|
||||||
|
|
||||||
|
return { blocks, separators };
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinParagraphBlocksPreserve(blocks, separators) {
|
||||||
|
if (!Array.isArray(blocks) || blocks.length === 0) return "";
|
||||||
|
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
|
out += blocks[i];
|
||||||
|
if (i < separators.length) out += separators[i];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------ utils système ------------------------------ */
|
/* ------------------------------ utils système ------------------------------ */
|
||||||
|
|
||||||
function run(cmd, args, opts = {}) {
|
function run(cmd, args, opts = {}) {
|
||||||
@@ -263,7 +331,9 @@ function pickSection(body, markers) {
|
|||||||
.map((m) => ({ m, i: text.toLowerCase().indexOf(m.toLowerCase()) }))
|
.map((m) => ({ m, i: text.toLowerCase().indexOf(m.toLowerCase()) }))
|
||||||
.filter((x) => x.i >= 0)
|
.filter((x) => x.i >= 0)
|
||||||
.sort((a, b) => a.i - b.i)[0];
|
.sort((a, b) => a.i - b.i)[0];
|
||||||
|
|
||||||
if (!idx) return "";
|
if (!idx) return "";
|
||||||
|
|
||||||
const start = idx.i + idx.m.length;
|
const start = idx.i + idx.m.length;
|
||||||
const tail = text.slice(start);
|
const tail = text.slice(start);
|
||||||
|
|
||||||
@@ -278,11 +348,13 @@ function pickSection(body, markers) {
|
|||||||
"\n## Proposition",
|
"\n## Proposition",
|
||||||
"\n## Problème",
|
"\n## Problème",
|
||||||
];
|
];
|
||||||
|
|
||||||
let end = tail.length;
|
let end = tail.length;
|
||||||
for (const s of stops) {
|
for (const s of stops) {
|
||||||
const j = tail.toLowerCase().indexOf(s.toLowerCase());
|
const j = tail.toLowerCase().indexOf(s.toLowerCase());
|
||||||
if (j >= 0 && j < end) end = j;
|
if (j >= 0 && j < end) end = j;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tail.slice(0, end).trim();
|
return tail.slice(0, end).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,8 +382,6 @@ function extractAnchorIdAnywhere(text) {
|
|||||||
|
|
||||||
function extractCheminFromAnyUrl(text) {
|
function extractCheminFromAnyUrl(text) {
|
||||||
const s = String(text || "");
|
const s = String(text || "");
|
||||||
// Exemple: http://localhost:4321/archicratie/prologue/#p-3-xxxx
|
|
||||||
// ou: /archicratie/prologue/#p-3-xxxx
|
|
||||||
const m = s.match(/(\/[a-z0-9\-]+\/[a-z0-9\-\/]+\/)#p-\d+-[0-9a-f]{8}/i);
|
const m = s.match(/(\/[a-z0-9\-]+\/[a-z0-9\-\/]+\/)#p-\d+-[0-9a-f]{8}/i);
|
||||||
return m ? m[1] : "";
|
return m ? m[1] : "";
|
||||||
}
|
}
|
||||||
@@ -412,7 +482,7 @@ async function fetchIssue({ forgeApiBase, owner, repo, token, issueNum }) {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"User-Agent": "archicratie-apply-ticket/2.0",
|
"User-Agent": "archicratie-apply-ticket/2.1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -428,7 +498,7 @@ async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment
|
|||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": "archicratie-apply-ticket/2.0",
|
"User-Agent": "archicratie-apply-ticket/2.1",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (comment) {
|
if (comment) {
|
||||||
@@ -437,7 +507,11 @@ async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = `${base}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
|
const url = `${base}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
|
||||||
const res = await fetch(url, { method: "PATCH", headers, body: JSON.stringify({ state: "closed" }) });
|
const res = await fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ state: "closed" }),
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const t = await res.text().catch(() => "");
|
const t = await res.text().catch(() => "");
|
||||||
@@ -541,10 +615,9 @@ async function main() {
|
|||||||
console.log(`🔎 Fetch ticket #${issueNum} from ${owner}/${repo} …`);
|
console.log(`🔎 Fetch ticket #${issueNum} from ${owner}/${repo} …`);
|
||||||
const issue = await fetchIssue({ forgeApiBase, owner, repo, token, issueNum });
|
const issue = await fetchIssue({ forgeApiBase, owner, repo, token, issueNum });
|
||||||
|
|
||||||
// Guard PR (Pull Request = "Demande d'ajout" = pas un ticket éditorial)
|
|
||||||
if (issue?.pull_request) {
|
if (issue?.pull_request) {
|
||||||
console.error(`❌ #${issueNum} est une Pull Request (demande d’ajout), pas un ticket éditorial.`);
|
console.error(`❌ #${issueNum} est une Pull Request (demande d’ajout), pas un ticket éditorial.`);
|
||||||
console.error(`➡️ Ouvre un ticket [Correction]/[Fact-check] depuis le site (Proposer), puis relance apply-ticket sur ce numéro.`);
|
console.error("➡️ Ouvre un ticket [Correction]/[Fact-check] depuis le site (Proposer), puis relance apply-ticket sur ce numéro.");
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +638,6 @@ async function main() {
|
|||||||
ancre = (ancre || "").trim();
|
ancre = (ancre || "").trim();
|
||||||
if (ancre.startsWith("#")) ancre = ancre.slice(1);
|
if (ancre.startsWith("#")) ancre = ancre.slice(1);
|
||||||
|
|
||||||
// fallback si ticket mal formé
|
|
||||||
if (!ancre) ancre = extractAnchorIdAnywhere(title) || extractAnchorIdAnywhere(body);
|
if (!ancre) ancre = extractAnchorIdAnywhere(title) || extractAnchorIdAnywhere(body);
|
||||||
|
|
||||||
chemin = normalizeChemin(chemin);
|
chemin = normalizeChemin(chemin);
|
||||||
@@ -604,7 +676,6 @@ async function main() {
|
|||||||
const distHtmlPath = path.join(DIST_ROOT, chemin.replace(/^\/+|\/+$/g, ""), "index.html");
|
const distHtmlPath = path.join(DIST_ROOT, chemin.replace(/^\/+|\/+$/g, ""), "index.html");
|
||||||
await ensureBuildIfNeeded(distHtmlPath);
|
await ensureBuildIfNeeded(distHtmlPath);
|
||||||
|
|
||||||
// Texte cible: préférence au texte complet (ticket), sinon dist si extrait probable
|
|
||||||
let targetText = texteActuel;
|
let targetText = texteActuel;
|
||||||
let distText = "";
|
let distText = "";
|
||||||
|
|
||||||
@@ -621,18 +692,24 @@ async function main() {
|
|||||||
throw new Error("Impossible de reconstruire le texte du paragraphe (ni texte actuel, ni dist html).");
|
throw new Error("Impossible de reconstruire le texte du paragraphe (ni texte actuel, ni dist html).");
|
||||||
}
|
}
|
||||||
|
|
||||||
const original = await fs.readFile(contentFile, "utf-8");
|
const originalRaw = await fs.readFile(contentFile, "utf-8");
|
||||||
const blocks = splitParagraphBlocks(original);
|
const { hasFrontmatter, frontmatter, body: originalBody } = splitMdxFrontmatter(originalRaw);
|
||||||
|
|
||||||
|
const split = splitParagraphBlocksPreserve(originalBody);
|
||||||
|
const blocks = split.blocks;
|
||||||
|
const separators = split.separators;
|
||||||
|
|
||||||
|
if (!blocks.length) {
|
||||||
|
throw new Error(`Aucun bloc éditorial exploitable dans ${path.relative(CWD, contentFile)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const ranked = rankedBlockMatches(blocks, targetText, 5);
|
const ranked = rankedBlockMatches(blocks, targetText, 5);
|
||||||
const best = ranked[0] || { i: -1, score: -1, excerpt: "" };
|
const best = ranked[0] || { i: -1, score: -1, excerpt: "" };
|
||||||
const runnerUp = ranked[1] || null;
|
const runnerUp = ranked[1] || null;
|
||||||
|
|
||||||
// seuil absolu
|
|
||||||
if (best.i < 0 || best.score < 40) {
|
if (best.i < 0 || best.score < 40) {
|
||||||
console.error("❌ Match trop faible: je refuse de remplacer automatiquement.");
|
console.error("❌ Match trop faible: je refuse de remplacer automatiquement.");
|
||||||
console.error(`➡️ Score=${best.score}. Recommandation: ticket avec 'Texte actuel (copie exacte du paragraphe)'.`);
|
console.error(`➡️ Score=${best.score}. Recommandation: ticket avec 'Texte actuel (copie exacte du paragraphe)'.`);
|
||||||
|
|
||||||
console.error("Top candidates:");
|
console.error("Top candidates:");
|
||||||
for (const r of ranked) {
|
for (const r of ranked) {
|
||||||
console.error(` #${r.i + 1} score=${r.score} ${r.excerpt}${r.excerpt.length >= 140 ? "…" : ""}`);
|
console.error(` #${r.i + 1} score=${r.score} ${r.excerpt}${r.excerpt.length >= 140 ? "…" : ""}`);
|
||||||
@@ -640,7 +717,6 @@ async function main() {
|
|||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// seuil relatif : si le 2e est trop proche du 1er, on refuse aussi
|
|
||||||
if (runnerUp) {
|
if (runnerUp) {
|
||||||
const ambiguityGap = best.score - runnerUp.score;
|
const ambiguityGap = best.score - runnerUp.score;
|
||||||
if (ambiguityGap < 15) {
|
if (ambiguityGap < 15) {
|
||||||
@@ -659,7 +735,16 @@ async function main() {
|
|||||||
|
|
||||||
const nextBlocks = blocks.slice();
|
const nextBlocks = blocks.slice();
|
||||||
nextBlocks[best.i] = afterBlock;
|
nextBlocks[best.i] = afterBlock;
|
||||||
const updated = nextBlocks.join("\n\n");
|
|
||||||
|
const updatedBody = joinParagraphBlocksPreserve(nextBlocks, separators);
|
||||||
|
const updatedRaw = joinMdxFrontmatter(frontmatter, updatedBody);
|
||||||
|
|
||||||
|
assertFrontmatterIntegrity({
|
||||||
|
hadFrontmatter: hasFrontmatter,
|
||||||
|
originalFrontmatter: frontmatter,
|
||||||
|
finalText: updatedRaw,
|
||||||
|
filePath: path.relative(CWD, contentFile),
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`🧩 Matched block #${best.i + 1}/${blocks.length} score=${best.score}`);
|
console.log(`🧩 Matched block #${best.i + 1}/${blocks.length} score=${best.score}`);
|
||||||
|
|
||||||
@@ -673,16 +758,15 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// backup uniquement si on écrit
|
|
||||||
const relContentFile = path.relative(CWD, contentFile);
|
const relContentFile = path.relative(CWD, contentFile);
|
||||||
const bakPath = path.join(BACKUP_ROOT, `${relContentFile}.bak.issue-${issueNum}`);
|
const bakPath = path.join(BACKUP_ROOT, `${relContentFile}.bak.issue-${issueNum}`);
|
||||||
await fs.mkdir(path.dirname(bakPath), { recursive: true });
|
await fs.mkdir(path.dirname(bakPath), { recursive: true });
|
||||||
|
|
||||||
if (!(await fileExists(bakPath))) {
|
if (!(await fileExists(bakPath))) {
|
||||||
await fs.writeFile(bakPath, original, "utf-8");
|
await fs.writeFile(bakPath, originalRaw, "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(contentFile, updated, "utf-8");
|
await fs.writeFile(contentFile, updatedRaw, "utf-8");
|
||||||
console.log("✅ Applied.");
|
console.log("✅ Applied.");
|
||||||
|
|
||||||
let aliasChanged = false;
|
let aliasChanged = false;
|
||||||
@@ -703,13 +787,11 @@ async function main() {
|
|||||||
|
|
||||||
if (aliasChanged) {
|
if (aliasChanged) {
|
||||||
console.log(`✅ Alias ajouté: ${chemin} ${ancre} -> ${newId}`);
|
console.log(`✅ Alias ajouté: ${chemin} ${ancre} -> ${newId}`);
|
||||||
// MàJ dist sans rebuild complet (inject seulement)
|
|
||||||
run("node", ["scripts/inject-anchor-aliases.mjs"], { cwd: CWD });
|
run("node", ["scripts/inject-anchor-aliases.mjs"], { cwd: CWD });
|
||||||
} else {
|
} else {
|
||||||
console.log(`ℹ️ Alias déjà présent ou inutile (${ancre} -> ${newId}).`);
|
console.log(`ℹ️ Alias déjà présent ou inutile (${ancre} -> ${newId}).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// garde-fous rapides
|
|
||||||
run("node", ["scripts/check-anchor-aliases.mjs"], { cwd: CWD });
|
run("node", ["scripts/check-anchor-aliases.mjs"], { cwd: CWD });
|
||||||
run("node", ["scripts/verify-anchor-aliases-in-dist.mjs"], { cwd: CWD });
|
run("node", ["scripts/verify-anchor-aliases-in-dist.mjs"], { cwd: CWD });
|
||||||
run("npm", ["run", "test:anchors"], { cwd: CWD });
|
run("npm", ["run", "test:anchors"], { cwd: CWD });
|
||||||
@@ -741,7 +823,6 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mode manuel
|
|
||||||
console.log("Next (manuel) :");
|
console.log("Next (manuel) :");
|
||||||
console.log(` git diff -- ${path.relative(CWD, contentFile)}`);
|
console.log(` git diff -- ${path.relative(CWD, contentFile)}`);
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
{}
|
{
|
||||||
|
"/archicrat-ia/prologue/": {
|
||||||
|
"p-0-d7974f88": "p-0-e729df02",
|
||||||
|
"p-4-8ed4f807": "p-4-90b2a1cc",
|
||||||
|
"p-5-85126fa5": "p-5-d788c546"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ source:
|
|||||||
kind: docx
|
kind: docx
|
||||||
path: "sources/docx/archicrat-ia/Prologue—Archicratie-fondation_et_finalite_sociopolitique_et_historique-version_officielle.docx"
|
path: "sources/docx/archicrat-ia/Prologue—Archicratie-fondation_et_finalite_sociopolitique_et_historique-version_officielle.docx"
|
||||||
---
|
---
|
||||||
Nous vivons dans une époque saturée de diagnostics sur les formes de domination, les mutations du pouvoir, les détournements de la souveraineté. Depuis une vingtaine d’années, les appellations s’accumulent : *démocratie illibérale*, *ploutocratie*, *happycratie*, *gouvernement algorithmique*, *démocrature*… À travers ces tentatives de nommer le désordre du présent, un fait se répète, de manière sourde : la scène politique semble désorientée. Les catégories héritées — *État*, *pouvoir*, *représentation*, *volonté générale*, *contrat social* — apparaissent de moins en moins capables de décrire ce qui nous gouverne effectivement.
|
Nous vivons une époque saturée de diagnostics sur les formes de domination, les mutations du pouvoir, les détournements de la souveraineté. Depuis une vingtaine d’années, les appellations s’accumulent : démocratie illibérale, ploutocratie, happycratie, gouvernement algorithmique, démocrature… À travers ces tentatives de nommer le désordre du présent, un fait se répète, de manière sourde : la scène politique semble désorientée. Les catégories héritées — État, pouvoir, représentation, volonté générale, contrat social — apparaissent de moins en moins capables de décrire ce qui nous gouverne effectivement.
|
||||||
|
|
||||||
C’est cette perte de prise sur le réel que ce livre souhaite prendre au sérieux. Non pour lui ajouter un terme de plus au lexique fatigué des contre-pouvoirs ou des impuissances, mais pour repartir d’un point plus fondamental, presque en-deçà de la question politique classique. Ce point, c’est celui de la *tenue d’un monde commun* — c’est-à-dire la possibilité, pour des êtres dissemblables, vulnérables, inégaux, traversés de contradictions et situés dans des temporalités hétérogènes, de coexister sans s’annihiler.
|
C’est cette perte de prise sur le réel que ce livre souhaite prendre au sérieux. Non pour lui ajouter un terme de plus au lexique fatigué des contre-pouvoirs ou des impuissances, mais pour repartir d’un point plus fondamental, presque en-deçà de la question politique classique. Ce point, c’est celui de la *tenue d’un monde commun* — c’est-à-dire la possibilité, pour des êtres dissemblables, vulnérables, inégaux, traversés de contradictions et situés dans des temporalités hétérogènes, de coexister sans s’annihiler.
|
||||||
|
|
||||||
@@ -20,10 +20,9 @@ Cette tenue du monde n’équivaut ni à la paix civile, ni à la stabilité des
|
|||||||
|
|
||||||
Le terme n’est pas trivial. Il ne s’agit pas simplement d’une viabilité partagée, ni d’une coexistence pacifique, ni même d’une durabilité écologique élargie. Il s’agit d’un état dynamique, instable, fragile, dans lequel un ensemble — une société, d’un système biologique, d’une formation historique, d’un milieu technique ou d’un monde institué — parvient à maintenir une *existence viable*, *malgré et grâce à ses tensions constitutives*.
|
Le terme n’est pas trivial. Il ne s’agit pas simplement d’une viabilité partagée, ni d’une coexistence pacifique, ni même d’une durabilité écologique élargie. Il s’agit d’un état dynamique, instable, fragile, dans lequel un ensemble — une société, d’un système biologique, d’une formation historique, d’un milieu technique ou d’un monde institué — parvient à maintenir une *existence viable*, *malgré et grâce à ses tensions constitutives*.
|
||||||
|
|
||||||
La *co-viabilité* ne désigne ni un état d’équilibre, ni une finalité normative. Elle nomme un état dynamique et instable, dans lequel un monde — société, milieu technique, formation historique — tient non pas par homogénéité ou harmonie, mais parce qu’il parvient à réguler ce qui le menace sans se détruire lui-même. Il compose entre des éléments hétérogènes — forces d’inertie et d’innovation, attachements profonds et ruptures nécessaires — sans chercher à les unifier. C’est cette disposition active, faite de compromis fragiles et d’ajustements toujours révisables, que nous tenons pour première, et non dérivée.
|
La co-viabilité ne désigne ni un état d’équilibre, ni une finalité normative. Elle nomme un état dynamique et instable, dans lequel un monde — société, milieu technique, formation historique — tient non pas par homogénéité ou harmonie, mais parce qu’il parvient à réguler ce qui le menace sans se détruire lui-même. Il compose entre des éléments hétérogènes — forces d’inertie et d’innovation, attachements profonds et ruptures nécessaires — sans chercher à les unifier. C’est cette disposition active, faite de compromis fragiles et d’ajustements toujours révisables, que nous tenons pour première.
|
||||||
|
|
||||||
Ce qui revient à dire que la question politique — au sens fort — n’a peut-être jamais été qui commande ? Mais bien plus : *Comment un ordre tient-il malgré ce qui le défait ?* *Quels sont les dispositifs qui permettent à une société de ne pas se désagréger sous l’effet de ses propres contradictions ?* *Comment sont régulées les tensions qui traversent le tissu du monde commun sans le déchirer ?*
|
Ce qui revient à dire que la question politique — au sens fort — n’a peut-être jamais été qui commande ? Mais bien plus : Comment un ordre tient-il malgré ce qui le défait ? Quels sont les dispositifs qui permettent à une société de ne pas se désagréger sous l’effet de ses propres contradictions ? Comment sont régulées les tensions qui traversent le tissu du monde commun sans le déchirer ? Cette bascule de perspective prolonge des intuitions anciennes. Max Weber (Économie et société, 1922) rappelait que ce qui fait tenir un ordre, ce n’est pas seulement la force ou la loi, mais les « chances de validité » socialement reconnues. Norbert Elias (La dynamique de l’Occident, 1939/1975) montrait, quant à lui, que les sociétés se maintiennent par des équilibres toujours précaires entre interdépendances, rivalités et pacifications. Notre démarche s’inscrit dans ce sillage : travailler cette interrogation sur les conditions de viabilité d’un monde commun soumis à ses propres tensions constitutives.
|
||||||
Cette bascule de perspective prolonge des intuitions anciennes. Max Weber (*Économie et société*, 1922) rappelait que ce qui fait tenir un ordre, ce n’est pas seulement la force ou la loi, mais les « chances de validité » socialement reconnues. Norbert Elias (*La dynamique de l’Occident*, 1939/1975) montrait, quant à lui, que les sociétés se maintiennent par des équilibres toujours précaires entre interdépendances, rivalités et pacifications. Notre démarche s’inscrit dans ce sillage : travailler cette interrogation sur les *conditions de viabilité d’un monde commun*.
|
|
||||||
|
|
||||||
Ce changement de perspective implique une rupture profonde dans la manière même de poser la question politique. Pendant des siècles, les sociétés ont pensé le politique à partir de principes transcendants — Dieu, Nature, Volonté générale, Pacte social. Ces principes, supposés extérieurs aux conflits du présent, garantissaient l’ordre en surplomb. Comme le rappelle Michel Foucault, il n’y a pas de principe extérieur au jeu des forces : seulement des rapports de pouvoir situés, modulés, réversibles. C’est précisément cette exigence — trouver dans les relations elles-mêmes les ressources nécessaires pour maintenir des mondes vivables — qui définit notre époque.
|
Ce changement de perspective implique une rupture profonde dans la manière même de poser la question politique. Pendant des siècles, les sociétés ont pensé le politique à partir de principes transcendants — Dieu, Nature, Volonté générale, Pacte social. Ces principes, supposés extérieurs aux conflits du présent, garantissaient l’ordre en surplomb. Comme le rappelle Michel Foucault, il n’y a pas de principe extérieur au jeu des forces : seulement des rapports de pouvoir situés, modulés, réversibles. C’est précisément cette exigence — trouver dans les relations elles-mêmes les ressources nécessaires pour maintenir des mondes vivables — qui définit notre époque.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user