Configuration LibreOffice / OpenOffice Calc — chaîne eval, mode colonne

Julien Béranger

+ Claude Opus 5

Variante « par lots » de la configuration cellule par cellule (RUKH_EVAL_OPEN_CALC_CONFIG.md). Même grille, même colonne de sortie, même bouton — mais un appel traite une vingtaine de critères au lieu d'un seul. La macro construit un CSV à partir de la feuille, le colle dans le message, reçoit une ligne par enregistrement et les réécrit dans la colonne de sortie.

Contexte dédié : eval-oc-full.

C'est la transposition du contexte eval — CSV brut envoyé en une fois — à un tableur qui sait le fabriquer lui-même. Ce que Calc apporte par rapport au copier-coller manuel : le CSV est propre par construction, et le calage est vérifié automatiquement avant écriture.

Quel mode choisir

eval-oc (cellule par cellule)eval-oc-full (mode colonne)
Appels pour un chapitre de 57 critères573
Quota 50/hatteint au bout d'un chapitre~16 chapitres possibles par heure
Coût d'entrée par chapitre~0,46 $~0,04 $
Cohérence entre cellulesverbes seulement, via Débuts déjà utilisésle modèle voit tout le lot
Acronymesredéveloppés dans chaque celluledéveloppés une fois par lot
Granularité de reprisela cellulele lot : un lot rejeté est à refaire en entier
Durée d'un appel~6 s~1 à 2 min (estimation, à mesurer)
Risque de dépassement de délainégligeableréel — c'est la contrainte du mode
Refaire une cellule isoléenaturelpossible, mais l'autre mode est plus adapté

En pratique : eval-oc-full pour traiter un chapitre, eval-oc pour reprendre les quelques cellules dont la réécriture ne convient pas. Les deux écrivent dans la même colonne Commentaires réécrits et se relaient sans précaution particulière.

Ce que la macro envoie

Un message par lot, construit à partir de la feuille — jamais un fichier joint, jamais un export manuel :

Enregistrements : 20
Débuts déjà utilisés : La personne indique qu'elle est ac | Les professionnels précisent

Chapitre;Critère;Cotations;Commentaires
"Chapitre 1";"1.1.1";"4;3;4";"AT2 : la personne explique qu'elle est accompagnée depuis 2021…"
"Chapitre 1";"1.1.2";"3";"les professionnels disent qu'il n'y a pas eu de formation"

Trois écarts délibérés par rapport au CSV que traite le contexte eval :

  • Seules les lignes de critère sont envoyées. Les lignes E.E. ne portent aucun commentaire à réécrire ; dans eval elles n'existent dans la réponse que pour produire un - de calage, parce qu'un humain colle la colonne entière dans la feuille. Ici la macro écrit cellule par cellule à des positions qu'elle connaît : ces lignes de remplissage n'ont plus de raison d'être. Sur le chapitre de référence, cela fait 57 lignes de sortie au lieu de 180, donc autant de risque de décalage et de jetons de sortie en moins.
  • Les cotations des lignes E.E. sont regroupées dans une colonne Cotations sur la ligne du critère. Elles restent nécessaires à la règle « cependant », elles ne coûtent plus une ligne chacune.
  • Un enregistrement = une ligne physique. La macro remplace les retours à la ligne des cellules par des espaces avant d'écrire le CSV. La « première cause de décalage » signalée dans les instructions du contexte eval — une cellule entre guillemets qui s'étale sur plusieurs lignes — disparaît à la source.

La ligne Enregistrements : N annonce le nombre de lignes attendues en retour. Débuts déjà utilisés : transmet les deux derniers débuts du lot précédent, pour que la variation des verbes franchisse les bornes de lot.

Le garde-fou de calage

C'est ce qui rend le mode colonne utilisable sans relecture ligne à ligne.

À la réception, la macro retire l'encadrement ```, ignore les lignes vides, et compte les lignes. Si le compte ne correspond pas exactement au nombre d'enregistrements envoyés, rien n'est écrit pour ce lot : une relance automatique est tentée (RETRIES), puis la macro s'arrête en indiquant le lot en cause. Les lots précédents restent écrits.

Autrement dit, un décalage ne peut pas s'installer silencieusement dans la colonne. C'est le contrôle que eval-verif effectuait a posteriori sur une colonne collée à la main, ramené à l'intérieur de la boucle et exécuté avant toute écriture.

Ce que le garde-fou ne vérifie pas : que la ligne n corresponde bien au critère n. Un modèle qui produirait le bon nombre de lignes dans le mauvais ordre passerait le contrôle. C'est peu probable — l'ordre du CSV est explicite et les identifiants de critère sont transmis — mais cela reste la raison de relire un chapitre avant de le rendre.

Prérequis

Identiques à ceux du mode cellule par cellule, à un point près : c'est le contexte eval-oc-full qui doit être déployé.

  • LibreOffice 7.x ou plus récent, avec le fournisseur de scripts Python. Inclus par défaut sous Windows et macOS ; sous Linux, installer au besoin libreoffice-script-provider-python (Debian/Ubuntu) ou libreoffice-pyuno (Fedora).
  • Accès réseau direct vers rukh.w3hc.org. urllib lit http_proxy / https_proxy, pas le proxy configuré dans LibreOffice.
  • Aucune authentification, aucun jeton.
  • Le contexte eval-oc-full déployé sur l'API (voir « Déploiement du contexte eval-oc-full », en fin de page).
  • Apache OpenOffice : voir « Variante Apache OpenOffice », plus bas.

Mise en place (une fois)

1. Le pont Python

Déposer rukh.py dans Scripts/python du profil utilisateur :

SystèmeChemin
macOS~/Library/Application Support/LibreOffice/4/user/Scripts/python/
Linux~/.config/libreoffice/4/user/Scripts/python/
Windows%APPDATA%\LibreOffice\4\user\Scripts\python\
import json
import urllib.request
import urllib.error

RUKH_URL = "https://rukh.w3hc.org/ask"
TIMEOUT = 300  # secondes ; ~6 s en mode cellule, 1 à 2 min en mode colonne

def ask(message, context):
    """Renvoie "OK:<texte>" ou "ERR:<code>". Les exceptions Python ne
    traversent pas proprement le pont UNO : le code d'erreur voyage donc dans
    la chaîne de retour, ce qui permet à Basic de distinguer un 429 d'une
    vraie panne."""
    body = json.dumps(
        {"message": message, "model": "anthropic", "context": context}
    ).encode("utf-8")
    req = urllib.request.Request(
        RUKH_URL, data=body, headers={"Content-Type": "application/json"}
    )
    try:
        with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
            return "OK:" + json.loads(r.read().decode("utf-8"))["output"]
    except urllib.error.HTTPError as e:
        return "ERR:%d" % e.code
    except Exception as e:
        return "ERR:0 %s" % e

g_exportedScripts = (ask,)

Le nom du contexte n'est pas figé dans ce fichier : il arrive en paramètre de ask. La valeur, elle, est bien une constante — mais côté Basic, une par macro : CONTEXT_FULL = "eval-oc-full" ici, CONTEXT_CELL = "eval-oc" dans la macro cellule par cellule. Un seul pont Python sert donc les deux chaînes. Si le rukh.py de la configuration cellule par cellule est déjà en place, le remplacer par celui-ci.

Redémarrer LibreOffice après le dépôt.

2. La macro Basic

Outils → Macros → Modifier les macros, puis Mes macros → Standard → Module1 — impérativement là, et non dans le classeur, pour éviter l'invite de confiance à chaque ouverture du .ods.

Les deux macros peuvent cohabiter dans le même module : elles partagent RukhAsk, Norm, StartsWith, JoinOpenings, StripFence et OneLine, strictement identiques dans les deux documents.

Si la macro cellule par cellule est déjà installée, ne coller ci-dessous que FormaliserColonne, Csv et SplitLines, plus les constantes CONTEXT_FULL, CHUNK et RETRIES. Coller le bloc entier provoquerait une erreur de compilation : Option Explicit, RUKH_SCRIPT, TARGET_HEADER, MAX_CALLS, OPENING_CHARS, oRukhScript et les six fonctions partagées seraient déclarés deux fois.

3. Le bouton

Affichage → Barres d'outils → Contrôles de formulaire, dessiner un bouton, propriétés → Événements → Exécuter l'actionFormaliserColonne.

La macro

Option Explicit

Const RUKH_SCRIPT = "vnd.sun.star.script:rukh.py$ask?language=Python&location=user"
Const CONTEXT_FULL = "eval-oc-full"
Const TARGET_HEADER = "Commentaires réécrits"
Const CHUNK = 20              ' enregistrements par appel — le seul réglage à connaître
Const RETRIES = 1             ' relances automatiques sur écart de compte
Const MAX_CALLS = 45          ' garde-fou par exécution, marge sous le quota de 50/heure
Const OPENING_CHARS = 32      ' longueur des débuts transmis au lot suivant

Dim oRukhScript As Object     ' résolu une fois, réutilisé pour toute l'exécution

Sub FormaliserColonne
    Dim oDoc As Object, oSheet As Object, oCur As Object, oSel As Object, oAddr As Object
    Dim oInd As Object
    Dim lastRow As Long, lastCol As Long, headerRow As Long
    Dim cChapitre As Long, cCritere As Long, cCotation As Long, cComm As Long
    Dim cQuestion As Long, cTarget As Long
    Dim selStart As Long, selEnd As Long, bSingle As Boolean
    Dim aTodo() As Long, nTodo As Long, nDone As Long, nCalls As Long
    Dim aOpen(1) As String
    Dim aLines() As String, nOut As Long
    Dim nPos As Long, nIn As Long, nTry As Long
    Dim r As Long, c As Long, j As Long, k As Long
    Dim h As String, s As String, sCot As String, sPayload As String, sRes As String

    oDoc = ThisComponent
    oSheet = oDoc.CurrentController.ActiveSheet

    oCur = oSheet.createCursor()
    oCur.gotoEndOfUsedArea(False)
    lastRow = oCur.RangeAddress.EndRow
    lastCol = oCur.RangeAddress.EndColumn

    ' --- ligne d'en-tête : colonne A = « Chapitre » ---
    headerRow = -1
    For r = 0 To lastRow
        If Norm(oSheet.getCellByPosition(0, r).getString()) = "chapitre" Then
            headerRow = r
            Exit For
        End If
    Next r
    If headerRow < 0 Then
        MsgBox "Ligne d'en-tête introuvable (colonne A = « Chapitre »).", 16, "Rukh"
        Exit Sub
    End If

    ' --- repérage des colonnes par intitulé, jamais par position ---
    cChapitre = -1 : cCritere = -1 : cCotation = -1 : cComm = -1 : cQuestion = -1 : cTarget = -1
    For c = 0 To lastCol
        h = Norm(oSheet.getCellByPosition(c, headerRow).getString())
        If h = "chapitre" And cChapitre < 0 Then cChapitre = c
        If h = "critère" And cCritere < 0 Then cCritere = c   ' « Critère ID » exclu : identifiant interne
        If StartsWith(h, "cotation") And cCotation < 0 Then cCotation = c
        If h = Norm(TARGET_HEADER) Then
            cTarget = c
        ElseIf StartsWith(h, "commentaires") And cComm < 0 Then
            cComm = c        ' la colonne de sortie commence aussi par « Commentaires » : à exclure
        End If
        If StartsWith(h, "is question") And cQuestion < 0 Then cQuestion = c
    Next c
    If cChapitre < 0 Or cComm < 0 Or cQuestion < 0 Then
        MsgBox "Colonnes « Chapitre », « Commentaires » ou « Is Question? » introuvables.", 16, "Rukh"
        Exit Sub
    End If

    If cTarget < 0 Then
        cTarget = lastCol + 1
        oSheet.getCellByPosition(cTarget, headerRow).setString(TARGET_HEADER)
    End If

    ' --- périmètre : c'est la sélection qui dit quoi traiter ---
    oSel = oDoc.CurrentSelection
    If IsNull(oSel) Then
        MsgBox "Sélectionner d'abord une cellule ou une plage.", 16, "Rukh"
        Exit Sub
    End If
    If oSel.supportsService("com.sun.star.sheet.SheetCellRanges") Then
        oAddr = oSel.getRangeAddresses()(0)   ' sélection multiple : première plage seulement
    ElseIf oSel.supportsService("com.sun.star.sheet.SheetCellRange") Then
        oAddr = oSel.RangeAddress
    Else
        MsgBox "Sélectionner d'abord une cellule ou une plage.", 16, "Rukh"
        Exit Sub
    End If
    selStart = oAddr.StartRow
    selEnd = oAddr.EndRow
    bSingle = (selStart = selEnd)

    ' --- enregistrements à traiter : lignes de critère non encore réécrites ---
    ReDim aTodo(lastRow)
    nTodo = 0
    For r = headerRow + 1 To lastRow
        If r >= selStart And r <= selEnd Then
            If StartsWith(oSheet.getCellByPosition(cChapitre, r).getString(), "Chapitre ") Then
                If Trim(oSheet.getCellByPosition(cQuestion, r).getString()) = "" Then
                    If Trim(oSheet.getCellByPosition(cComm, r).getString()) <> "" Then
                        If bSingle Or Trim(oSheet.getCellByPosition(cTarget, r).getString()) = "" Then
                            aTodo(nTodo) = r
                            nTodo = nTodo + 1
                        End If
                    End If
                End If
            End If
        End If
    Next r

    If nTodo = 0 Then
        MsgBox "Rien à faire : toutes les cellules de la sélection sont déjà réécrites.", 64, "Rukh"
        Exit Sub
    End If

    ' --- mémoire des débuts, réamorcée depuis la colonne de sortie ---
    aOpen(0) = "" : aOpen(1) = ""
    For r = aTodo(0) - 1 To headerRow + 1 Step -1
        s = Trim(oSheet.getCellByPosition(cTarget, r).getString())
        If s <> "" Then
            If aOpen(1) = "" Then
                aOpen(1) = Left(s, OPENING_CHARS)
            Else
                aOpen(0) = Left(s, OPENING_CHARS)
                Exit For
            End If
        End If
    Next r

    oInd = oDoc.CurrentController.Frame.createStatusIndicator()
    oInd.start("Rukh : réécriture par lots…", nTodo)
    nPos = 0 : nDone = 0 : nCalls = 0

    Do While nPos < nTodo
        If nCalls >= MAX_CALLS Then Exit Do

        nIn = nTodo - nPos
        If nIn > CHUNK Then nIn = CHUNK

        ' --- construction du CSV du lot ---
        sPayload = "Enregistrements : " & nIn & Chr(10)
        If aOpen(0) <> "" Or aOpen(1) <> "" Then
            sPayload = sPayload & "Débuts déjà utilisés : " & JoinOpenings(aOpen) & Chr(10)
        End If
        sPayload = sPayload & Chr(10) & "Chapitre;Critère;Cotations;Commentaires" & Chr(10)

        For k = 0 To nIn - 1
            r = aTodo(nPos + k)

            ' cotations des lignes E.E. rattachées à ce critère, pour la règle « cependant »
            sCot = ""
            For j = r + 1 To lastRow
                If Trim(oSheet.getCellByPosition(cQuestion, j).getString()) = "" Then Exit For
                If cCotation >= 0 Then
                    s = Trim(oSheet.getCellByPosition(cCotation, j).getString())
                    If s <> "" Then
                        If sCot <> "" Then sCot = sCot & ";"
                        sCot = sCot & s
                    End If
                End If
            Next j

            s = ""
            If cCritere >= 0 Then s = oSheet.getCellByPosition(cCritere, r).getString()

            ' OneLine() sur le commentaire : un enregistrement tient sur une ligne physique,
            ' ce qui supprime à la source la principale cause de décalage.
            sPayload = sPayload & _
                Csv(oSheet.getCellByPosition(cChapitre, r).getString()) & ";" & _
                Csv(s) & ";" & Csv(sCot) & ";" & _
                Csv(OneLine(oSheet.getCellByPosition(cComm, r).getString())) & Chr(10)
        Next k

        ' --- appel, avec relance sur écart de compte ---
        nOut = -1
        For nTry = 0 To RETRIES
            sRes = RukhAsk(sPayload, CONTEXT_FULL)
            nCalls = nCalls + 1

            If Left(sRes, 7) = "ERR:429" Then
                oInd.end()
                MsgBox "Quota atteint après " & nDone & " cellule(s). Reste " & (nTodo - nDone) & _
                       ". Relancer dans une heure.", 48, "Rukh"
                Exit Sub
            ElseIf Left(sRes, 4) = "ERR:" Then
                oInd.end()
                MsgBox "Erreur " & Mid(sRes, 5) & " sur le lot commençant ligne " & (aTodo(nPos) + 1) & _
                       ". " & nDone & " cellule(s) écrite(s), conservées.", 16, "Rukh"
                Exit Sub
            End If

            nOut = SplitLines(StripFence(Mid(sRes, 4)), aLines())
            If nOut = nIn Then Exit For
        Next nTry

        ' --- garde-fou de calage : sur écart, ce lot n'est pas écrit du tout ---
        If nOut <> nIn Then
            oInd.end()
            MsgBox nOut & " ligne(s) reçue(s) pour " & nIn & " enregistrement(s) envoyé(s) : " & _
                   "ce lot n'a pas été écrit, pour ne pas décaler la colonne." & Chr(10) & _
                   "Lot commençant ligne " & (aTodo(nPos) + 1) & ". " & nDone & _
                   " cellule(s) écrite(s) avant, conservées." & Chr(10) & _
                   "Réduire CHUNK puis relancer : la reprise repartira de ce lot.", 16, "Rukh"
            Exit Sub
        End If

        ' --- écriture du lot ---
        For k = 0 To nIn - 1
            oSheet.getCellByPosition(cTarget, aTodo(nPos + k)).setString(OneLine(aLines(k)))
            nDone = nDone + 1
        Next k
        oInd.setValue(nDone)

        ' --- débuts transmis au lot suivant ---
        If nIn >= 2 Then
            aOpen(0) = Left(OneLine(aLines(nIn - 2)), OPENING_CHARS)
        Else
            aOpen(0) = aOpen(1)
        End If
        aOpen(1) = Left(OneLine(aLines(nIn - 1)), OPENING_CHARS)

        nPos = nPos + nIn
    Loop

    oInd.end()

    If nTodo - nDone > 0 Then
        MsgBox nDone & " cellule(s) réécrite(s) en " & nCalls & " appel(s). Reste " & _
               (nTodo - nDone) & " : relancer.", 64, "Rukh"
    Else
        MsgBox nDone & " cellule(s) réécrite(s) en " & nCalls & " appel(s). Sélection terminée.", 64, "Rukh"
    End If
End Sub

' --- pont vers le script Python ---------------------------------------------

Function RukhAsk(sMessage As String, sContext As String) As String
    Dim oFactory As Object
    Dim aArgs(1) As Variant
    Dim aOutIdx(), aOutArgs()

    If IsNull(oRukhScript) Then
        oFactory = createUnoService("com.sun.star.script.provider.MasterScriptProviderFactory")
        oRukhScript = oFactory.createScriptProvider("").getScript(RUKH_SCRIPT)
    End If

    aArgs(0) = sMessage
    aArgs(1) = sContext
    RukhAsk = oRukhScript.invoke(aArgs(), aOutIdx(), aOutArgs())
End Function

' --- utilitaires propres au mode colonne -------------------------------------

' Champ CSV : toujours entre guillemets, guillemet interne doublé.
Function Csv(sText As String) As String
    Csv = """" & Join(Split(sText, """"), """""") & """"
End Function

' Découpe la réponse en lignes non vides. Les lignes vides sont ignorées plutôt
' que comptées : les instructions les interdisent, et les ignorer évite de
' rejeter un lot par ailleurs correct.
Function SplitLines(sText As String, aOut() As String) As Long
    Dim a() As String, k As Long, n As Long, s As String
    a = Split(Join(Split(sText, Chr(13) & Chr(10)), Chr(10)), Chr(10))
    ReDim aOut(UBound(a))
    n = 0
    For k = 0 To UBound(a)
        s = Trim(a(k))
        If s <> "" Then
            aOut(n) = s
            n = n + 1
        End If
    Next k
    SplitLines = n
End Function

' --- utilitaires partagés avec la macro cellule par cellule -------------------

Function Norm(v As String) As String
    Dim s As String
    s = LCase(Trim(v))
    s = Join(Split(s, Chr(10)), " ")
    s = Join(Split(s, Chr(13)), " ")
    s = Join(Split(s, Chr(9)), " ")
    Do While InStr(s, "  ") > 0
        s = Join(Split(s, "  "), " ")
    Loop
    Norm = Trim(s)
End Function

Function StartsWith(sText As String, sPrefix As String) As Boolean
    StartsWith = (Left(sText, Len(sPrefix)) = sPrefix)
End Function

Function JoinOpenings(aOpen() As String) As String
    If aOpen(0) <> "" And aOpen(1) <> "" Then
        JoinOpenings = aOpen(0) & " | " & aOpen(1)
    Else
        JoinOpenings = aOpen(0) & aOpen(1)
    End If
End Function

Function StripFence(sText As String) As String
    Dim a() As String, i0 As Long, i1 As Long, k As Long, sOut As String
    a = Split(Join(Split(sText, Chr(13) & Chr(10)), Chr(10)), Chr(10))
    i0 = LBound(a)
    i1 = UBound(a)
    Do While i0 <= i1 And Trim(a(i0)) = ""
        i0 = i0 + 1
    Loop
    Do While i1 >= i0 And Trim(a(i1)) = ""
        i1 = i1 - 1
    Loop
    If i0 <= i1 And Left(Trim(a(i0)), 3) = "```" Then i0 = i0 + 1
    If i0 <= i1 And Trim(a(i1)) = "```" Then i1 = i1 - 1
    For k = i0 To i1
        If sOut <> "" Then sOut = sOut & Chr(10)
        sOut = sOut & a(k)
    Next k
    StripFence = sOut
End Function

Function OneLine(sText As String) As String
    Dim s As String
    s = Join(Split(sText, Chr(13)), " ")
    s = Join(Split(s, Chr(10)), " ")
    Do While InStr(s, "  ") > 0
        s = Join(Split(s, "  "), " ")
    Loop
    OneLine = Trim(s)
End Function

Utilisation

Comme en mode cellule par cellule, c'est la sélection qui décide du périmètre — la différence est qu'elle est ensuite découpée en lots de CHUNK enregistrements.

  • Tout le chapitre : cliquer sur l'en-tête de la colonne, lancer. Trois appels environ, aucune intervention entre les lots.
  • Un lot précis : sélectionner les lignes voulues.
  • Reprendre : relancer sur la même sélection. Les cellules déjà écrites sont sautées, la reprise repart au premier lot non écrit.
  • Refaire une cellule : sélectionner une seule ligne. La cellule est réécrite même si elle l'était déjà — mais pour ce geste, la macro Formaliser du mode cellule par cellule est plus économique.

Le compte rendu final indique le nombre de cellules écrites et le nombre d'appels consommés, ce dernier étant ce qui se déduit du quota horaire.

Quota, coût et durée

Critères à réécrire, chapitre de référence57
Enregistrements par appelCHUNK, 20 par défaut
Appels pour un chapitre3
Quota API50 requêtes/heure, par adresse IP
Garde-fou par exécutionMAX_CALLS = 45 appels
Tokens d'entrée par chapitre~18 000 (estimation : ~3 900 d'instructions + ~2 000 de CSV par appel)
Coût d'entrée estimé~0,04 $ par chapitre, contre ~0,46 $ cellule par cellule
Durée d'un appel~1 à 2 min (estimation, à mesurer)
Durée d'un chapitre~4 à 6 min

Le coût de sortie, lui, ne change pas : c'est le même texte qui est produit dans les deux modes. L'économie porte entièrement sur les instructions, qui sont renvoyées 3 fois au lieu de 57.

Les trois propriétés de reprise sont conservées, mais à la maille du lot :

  1. Reprise automatique — seules les cellules dont Commentaires réécrits est vide sont traitées, et la mémoire des débuts est relue dans cette même colonne.
  2. Écriture par lot — chaque lot est écrit dès sa validation. Une coupure fait perdre au plus le lot en cours, soit ~20 cellules.
  3. Arrêt propre — sur 429, sur erreur HTTP et sur écart de compte, la macro s'arrête en indiquant ce qui a été écrit et où reprendre.

Limites et risques

  • Le dépassement de délai est le vrai risque du mode colonne. Une réponse de vingt commentaires demande une à deux minutes ; le proxy_read_timeout du nginx placé devant l'API reste à vérifier, et c'est lui qui décidera du CHUNK praticable. En cas de coupure systématique au même endroit, descendre CHUNK à 10 avant toute autre hypothèse. Le TIMEOUT de 300 s côté Python est délibérément large pour que ce soit l'API qui tranche.
  • L'interface est figée pendant chaque appel, une à deux minutes durant. Basic est synchrone : ni await, ni interruption par Échap. Enregistrer le fichier avant de lancer.
  • Un lot rejeté est à refaire en entier. C'est le prix du garde-fou : mieux vaut vingt cellules à refaire qu'une colonne décalée.
  • Le contrôle porte sur le nombre de lignes, pas sur leur ordre. Voir « Le garde-fou de calage », plus haut.
  • Sélection multiple ignorée au-delà de la première plage.
  • La cohérence s'arrête à la borne du lot. Un acronyme est développé une fois par lot, donc trois fois par chapitre au lieu de cinquante-sept — mieux qu'en mode cellule, pas parfait pour autant. Augmenter CHUNK améliore ce point et aggrave le risque de délai : c'est le seul arbitrage à faire.

Variante Apache OpenOffice

Toute la macro fonctionne telle quelle : UNO et StarBasic sont partagés. Seul le pont ne transfère pas — AOO 4.1.x embarque Python 2.7. Route pragmatique : remplacer RukhAsk par un appel curl synchrone (4ᵉ argument de Shell à True) écrivant le corps JSON dans un fichier temporaire, avec les utilitaires JsonEscape et JsonStringValue de docs/SPREADSHEETS.md transposés en remplaçant AscW/ChrW$ par Asc/Chr$.

Le mode colonne rend cette variante plus fragile que le mode cellule : le corps JSON à échapper contient tout un CSV, et la réponse à extraire fait plusieurs milliers de signes — soit exactement ce que l'extracteur artisanal gère le moins bien. Si le poste peut recevoir LibreOffice, l'y installer.

Déploiement du contexte eval-oc-full

data/ et docs/private/ sont dans le .gitignore : le contexte créé en local ne part pas avec un git push. Pour le rendre disponible sur rukh.w3hc.org, le déposer via l'API de contextes (POST /context puis POST /context/upload avec instruction-file.md), signature SIWE à l'appui — l'adresse doit correspondre au creatorAddress de index.json. Tant que ce n'est pas fait, la macro renverra des réponses hors contexte, et le garde-fou de calage rejettera probablement tous les lots.

Après toute modification de instruction-file.md, refaire le dépôt : c'est le fichier déployé qui fait foi, pas la copie locale.