@roomi-fields/grillhouse 0.0.0-stage → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +9 -0
- package/METACADRE.md +41 -0
- package/agents/developpeur.md +10 -0
- package/agents/explorateur.md +9 -0
- package/agents/questionneur.md +11 -0
- package/agents/referent-technique.md +9 -0
- package/agents/regulateur.md +11 -0
- package/agents/relecteur.md +9 -0
- package/agents/testwriter.md +10 -0
- package/bin/grillhouse.mjs +125 -0
- package/cadre/templates/projet/.claude/settings.json +30 -0
- package/cadre/templates/projet/.editorconfig +47 -0
- package/cadre/templates/projet/.env.example +57 -0
- package/cadre/templates/projet/.eslintrc.cjs +56 -0
- package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
- package/cadre/templates/projet/.mcp.json +13 -0
- package/cadre/templates/projet/.prettierrc +11 -0
- package/cadre/templates/projet/CHANGELOG.md +61 -0
- package/cadre/templates/projet/CLAUDE.md +46 -0
- package/cadre/templates/projet/CONTRIBUTING.md +243 -0
- package/cadre/templates/projet/CREDITS.md +41 -0
- package/cadre/templates/projet/README.md +152 -0
- package/cadre/templates/projet/_gitignore +91 -0
- package/cadre/templates/projet/_npmignore +46 -0
- package/cadre/templates/projet/deployment/README.md +98 -0
- package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
- package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
- package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
- package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
- package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
- package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
- package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
- package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
- package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
- package/cadre/templates/projet/ecosystem.config.cjs +73 -0
- package/cadre/templates/projet/package.json +90 -0
- package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
- package/cadre/templates/projet/src/config.ts +68 -0
- package/cadre/templates/projet/src/errors.ts +102 -0
- package/cadre/templates/projet/src/index.ts +46 -0
- package/cadre/templates/projet/src/services/.gitkeep +8 -0
- package/cadre/templates/projet/src/types/index.ts +24 -0
- package/cadre/templates/projet/src/utils/logger.ts +106 -0
- package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
- package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
- package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
- package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
- package/cadre/templates/projet/tsconfig.eslint.json +9 -0
- package/cadre/templates/projet/tsconfig.json +41 -0
- package/cadre/templates/projet/tsdoc.json +6 -0
- package/cadre/templates/projet/vitest.config.ts +44 -0
- package/cadre/templates/turbo/paquets.json +29 -0
- package/cadre/templates/turbo/sans-paquets.json +28 -0
- package/charte.md +107 -0
- package/docs/ARCHITECTURE.md +90 -0
- package/hooks/crochet.mjs +33 -0
- package/hooks/hooks.json +58 -0
- package/hooks/register.tsx +558 -0
- package/package.json +75 -4
- package/scripts/avancement.mjs +244 -0
- package/scripts/bilan.mjs +523 -0
- package/scripts/branche-de-nuit.mjs +59 -0
- package/scripts/cadre.mjs +109 -0
- package/scripts/circuit.mjs +4375 -0
- package/scripts/code-mort.mjs +371 -0
- package/scripts/composants-affectes.mjs +598 -0
- package/scripts/consommateurs-arbitres.mjs +192 -0
- package/scripts/consommateurs.mjs +310 -0
- package/scripts/crochet.mjs +24 -0
- package/scripts/crochets.mjs +135 -0
- package/scripts/dernier-vert.mjs +208 -0
- package/scripts/dette.mjs +181 -0
- package/scripts/droits.mjs +354 -0
- package/scripts/enveloppe/codes.mjs +17 -0
- package/scripts/enveloppe/composant.mjs +11 -0
- package/scripts/enveloppe/enveloppe.mjs +628 -0
- package/scripts/enveloppe/lancer.sh +311 -0
- package/scripts/etapes.mjs +323 -0
- package/scripts/etat-du-circuit.mjs +104 -0
- package/scripts/frontieres.mjs +456 -0
- package/scripts/gardes-du-cadre.mjs +72 -0
- package/scripts/greffons.mjs +338 -0
- package/scripts/grillhouse-maj.mjs +514 -0
- package/scripts/index-interfaces.mjs +293 -0
- package/scripts/init.mjs +117 -0
- package/scripts/integration/file-attente.mjs +61 -0
- package/scripts/integration/file.mjs +494 -0
- package/scripts/integration/integrer.mjs +381 -0
- package/scripts/integration/rejeu.mjs +139 -0
- package/scripts/integration/verdict.mjs +13 -0
- package/scripts/interfaces-contre-code.mjs +547 -0
- package/scripts/ligne-etat/defaut.sh +110 -0
- package/scripts/ligne-etat/ligne-etat.mjs +93 -0
- package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
- package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
- package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
- package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
- package/scripts/migrations.mjs +43 -0
- package/scripts/nuit.sh +269 -0
- package/scripts/paquets.mjs +79 -0
- package/scripts/poussee-cles.mjs +46 -0
- package/scripts/preparation.mjs +84 -0
- package/scripts/references-par-les-types.mjs +420 -0
- package/scripts/registre.mjs +336 -0
- package/scripts/resultats.mjs +580 -0
- package/scripts/resumes.mjs +57 -0
- package/scripts/retirer-copie.sh +127 -0
- package/scripts/session-start.sh +203 -0
- package/scripts/sortie.mjs +155 -0
- package/scripts/sous-modules.sh +316 -0
- package/scripts/structure-guard.sh +27 -0
- package/scripts/superviseur.mjs +102 -0
- package/scripts/tableau.mjs +1886 -0
- package/scripts/tests.mjs +236 -0
- package/scripts/ticket.sh +23 -0
- package/scripts/turbo-des-paquets.mjs +899 -0
- package/scripts/un-commit-par-ticket.mjs +212 -0
- package/scripts/verifier.mjs +469 -0
- package/scripts/verrou-de-fichier.mjs +130 -0
- package/scripts/verrou-de-machine.mjs +211 -0
- package/scripts/verrous/commande.mjs +180 -0
- package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
- package/scripts/verrous/verrou.mjs +165 -0
- package/scripts/vitest-config/avancement.mjs +39 -0
- package/scripts/vitest-config/avancement.reporter.mjs +56 -0
- package/scripts/vitest-config/base-de-passe.mjs +128 -0
- package/scripts/vitest-config/base.d.ts +6 -0
- package/scripts/vitest-config/base.mjs +87 -0
- package/scripts/vitest-config/changements.mjs +173 -0
- package/scripts/vitest-config/cibles.mjs +207 -0
- package/scripts/vitest-config/duree.reporter.mjs +98 -0
- package/scripts/vitest-config/machine.setup.mjs +14 -0
- package/scripts/vitest-config/mesure.d.ts +6 -0
- package/scripts/vitest-config/mesure.mjs +13 -0
- package/scripts/vitest-config/mesure.setup.mjs +12 -0
- package/scripts/vitest-config/pression.mjs +90 -0
- package/scripts/vitest-config/resultats.mjs +23 -0
- package/scripts/vitest-config/resultats.setup.mjs +19 -0
- package/scripts/vitest.nuit.config.mjs +26 -0
- package/skills/developpeur/SKILL.md +175 -0
- package/skills/explorateur/SKILL.md +84 -0
- package/skills/grill/SKILL.md +238 -0
- package/skills/grill/references/releve-structurel.md +38 -0
- package/skills/mesure/SKILL.md +168 -0
- package/skills/pitmaster/SKILL.md +382 -0
- package/skills/pitmaster/references/cadre.md +119 -0
- package/skills/pitmaster/references/consigne-agent.md +308 -0
- package/skills/pitmaster/references/erreurs.md +25 -0
- package/skills/pitmaster/references/tour.md +124 -0
- package/skills/questionneur/SKILL.md +64 -0
- package/skills/redacteur/SKILL.md +84 -0
- package/skills/redacteur/references/documents-d-un-composant.md +105 -0
- package/skills/referent-technique/SKILL.md +145 -0
- package/skills/regulateur/SKILL.md +220 -0
- package/skills/release/SKILL.md +134 -0
- package/skills/release/scripts/update-version.cjs +92 -0
- package/skills/relecteur/SKILL.md +152 -0
- package/skills/relecteur/references/LICENSE-MIT +30 -0
- package/skills/relecteur/references/SOURCE.md +8 -0
- package/skills/relecteur/references/claims-check.md +14 -0
- package/skills/relecteur/references/deletion-check.md +14 -0
- package/skills/relecteur/references/edge-case-hunter.md +80 -0
- package/skills/relecteur/references/verification-gap.md +113 -0
- package/skills/testwriter/SKILL.md +187 -0
- package/types/index.d.ts +56 -0
- package/README.md +0 -3
|
@@ -0,0 +1,4375 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The circuit of one ticket: for each component its title names, in order, the provider first, the
|
|
3
|
+
// testwriter, the proof of the red, the developpeur and the checks; then the reviewer, and on its
|
|
4
|
+
// ACCEPTÉ the integration, which commits the lot on the ticket's branch and puts that branch in the
|
|
5
|
+
// queue; then the queue, which brings it onto main, pushes and reads the CI, and the circuit ends
|
|
6
|
+
// on the state of the ticket the queue leaves. The supervisor runs it once, in the background, and
|
|
7
|
+
// its end wakes him: at 0 the ticket is closed; at 75 he runs `--reprendre` again once the machine
|
|
8
|
+
// is at rest; at « question » (18) the regulator's question goes to him: he answers it
|
|
9
|
+
// himself, or carries it to the owner when it is the owner's, then runs `--reponse "<texte>"` with
|
|
10
|
+
// the answer, which enters the plan before the regulator resumes; at « composants-manquants »
|
|
11
|
+
// (21) he adds the components to the title or answers why they stay out; at « questionnement »
|
|
12
|
+
// (22) he changes the plan or keeps it, and answers; at « references » (23), a reference the
|
|
13
|
+
// regeneration of 3.1 changed and the plan does not name, which goes to the regulator first; a
|
|
14
|
+
// decision of the owner given out of any
|
|
15
|
+
// such stop, the circuit running or not, he gives by `--decision "<texte>"`; at
|
|
16
|
+
// any other code, the stop is one the regulator could not settle, or one the
|
|
17
|
+
// table of the stops hands to him. On a stop other than 0 and 21, the circuit reads first the
|
|
18
|
+
// table of the stops (scripts/etapes.mjs, TABLE_DES_ARRETS), its written policy, as Zuul and
|
|
19
|
+
// GitHub's merge queue write their bounded retries: a gesture the table names, it applies itself;
|
|
20
|
+
// for what the table does not settle, the circuit launches a session of the regulator itself, as
|
|
21
|
+
// the scheduler of an integration (Zuul, bors) calls the judgement at a stop and applies the
|
|
22
|
+
// decision it gets: the regulator settles, and its sheet is its decision, which the circuit checks
|
|
23
|
+
// on the state, writes at the ticket and applies in the same process. So the supervisor is woken
|
|
24
|
+
// only by the end of the lot, a question of the regulator, or a stop neither the table nor the
|
|
25
|
+
// regulator settled.
|
|
26
|
+
//
|
|
27
|
+
// npx grillhouse circuit <ticket> [--reprendre] [--contexte <texte>]
|
|
28
|
+
// npx grillhouse circuit <ticket> --depuis <testwriter|developpeur> <composant> --contexte <texte>
|
|
29
|
+
// npx grillhouse circuit <ticket> --reponse <texte>
|
|
30
|
+
// npx grillhouse circuit <ticket> --decision <texte>
|
|
31
|
+
//
|
|
32
|
+
// The circuit decides on structured results only (scripts/resultats.mjs, the one module of their
|
|
33
|
+
// formats), as Bazel's Build Event Protocol and the JUnit reports carry the outcome of a build:
|
|
34
|
+
// the sheet that ends each session, the summaries of Turborepo, the JSON reports of Vitest and the
|
|
35
|
+
// SARIF of the guards. It reads no comment, no answer and no log to decide (a guard holds it:
|
|
36
|
+
// tests/unit/circuit-sans-texte-libre.spec.ts); the handovers at the ticket are for the human and
|
|
37
|
+
// for the next session of their author, the log for the human and the regulator.
|
|
38
|
+
//
|
|
39
|
+
// - The components are read in the ticket's title (`bd show <ticket> --json`), after its last
|
|
40
|
+
// « — », separated by commas: each one a path (`packages/<nom>`, or `.`), or the name of a
|
|
41
|
+
// package, which becomes its directory (scripts/paquets.mjs), read in the ticket's copy when it
|
|
42
|
+
// exists, else in the main tree. A component that names neither stops the call, exit 2, named.
|
|
43
|
+
// - Before the testwriter of the first component (a run that starts there), the circuit surveys the
|
|
44
|
+
// components (step 1.1 of scripts/etapes.mjs) the ticket's Architecture and criteria reach (scripts/composants-affectes.mjs), as
|
|
45
|
+
// the « affected components » of a design document: a component the survey finds, the title
|
|
46
|
+
// does not name and no admission covers (`composants-affectes.mjs <ticket> --admettre`) stops it
|
|
47
|
+
// at once, before any session and without a regulator, at
|
|
48
|
+
// « composants-manquants », the missing components at the head of its report. The supervisor
|
|
49
|
+
// adds it to the title, the provider first, and runs the circuit again; or he answers why it
|
|
50
|
+
// stays out, `--reponse <texte>`, which the circuit writes at the ticket and keeps beside the
|
|
51
|
+
// copy (`.claude/worktrees/<ticket>.composants.json`): the components answered for pass from then
|
|
52
|
+
// on, and the run starts from the first component.
|
|
53
|
+
// - Then, before any session of the lot, the questioning of the plan (step 1.2 of
|
|
54
|
+
// scripts/etapes.mjs), as Amazon's bar raiser, a cockpit's challenge and response and the
|
|
55
|
+
// devil's advocate of a design review: a session of the questionneur out of the envelope, as the
|
|
56
|
+
// regulator's, read-only (no tool of writing, every write of bd refused, scripts/droits.mjs), its
|
|
57
|
+
// instruction from the eighth template (the proposal, the métacadre, the rubrics « Comment on
|
|
58
|
+
// arbitre » and « Ce qui monte au superviseur » of the project's CLAUDE.md, the ticket's
|
|
59
|
+
// description and the survey of its components; no list of questions), its sheet `issue`
|
|
60
|
+
// QUESTIONS (each question with what it aims at), AUCUNE or DÉSACCORD. On QUESTIONS the technical
|
|
61
|
+
// referent answers for the plan (`--agent referent-technique`, a fresh session, the eleventh
|
|
62
|
+
// template, then resumed with the tenth), its sheet `reponses` (CHANGE and what the plan becomes,
|
|
63
|
+
// or GARDE and why) and the `titre` or `description` the plan becomes, which the circuit writes
|
|
64
|
+
// to the ticket (`bd update --title`, `-d`; a title whose components the repository does not
|
|
65
|
+
// hold stays); the questionneur's session is resumed with the answers (the ninth template); four
|
|
66
|
+
// rounds at most (the loop `questionnement` of 1.2, `questionnement` in the state while it runs). Every question
|
|
67
|
+
// and answer is written at the ticket (`Circuit · <composant> · questionnement : …`) and the end
|
|
68
|
+
// kept with the circuit's results (`questionnement`: objet, tours, change, issue, the plan's
|
|
69
|
+
// fingerprint). AUCUNE goes on, the survey played again on a plan changed; a plan whose title
|
|
70
|
+
// and description passed a questioning, or the supervisor's answer, passes unquestioned. A
|
|
71
|
+
// DÉSACCORD, or a fourth round still questioned, stops at « questionnement », what remains at the
|
|
72
|
+
// head of its report; `--reponse <texte>` writes the answer at the ticket and lets the plan pass,
|
|
73
|
+
// from the first component.
|
|
74
|
+
// - Each session is a launch of scripts/enveloppe/lancer.sh, a child process the circuit waits
|
|
75
|
+
// for, in its own process group, its errors written to a file: a session outlives the death of
|
|
76
|
+
// its circuit (its caller dead, the circuit with it), and the state names its group while it
|
|
77
|
+
// runs. The instruction is written from the first template of
|
|
78
|
+
// mods/grillhouse/skills/pitmaster/references/consigne-agent.md, each `{{champ}}` filled: the ticket and
|
|
79
|
+
// its subject, the component, the ticket's description (its criteria) and its notes (a section
|
|
80
|
+
// absent without notes), both read with `bd show` at the session's launch, the report of the
|
|
81
|
+
// step before (the proof, the reds of a check), the last handover of the session's author
|
|
82
|
+
// (`<rôle> <composant>`) at the ticket, for a developpeur without one yet the last of the
|
|
83
|
+
// testwriter of his component, else « aucune » (no other component's handover, nor a verdict), and
|
|
84
|
+
// the line `CONTEXTE :`: the text of `--contexte`, for each agent of a run without `--depuis`,
|
|
85
|
+
// for each agent of the component it plays again under `--depuis`. The session's own exit code
|
|
86
|
+
// is the one lancer.sh writes to `.claude/worktrees/<ticket>.fin`; a launch that leaves none stopped before its session.
|
|
87
|
+
// - Each session of the testwriter, the developpeur, the reviewer, the regulator, the questionneur and
|
|
88
|
+
// the technical referent ends on its sheet
|
|
89
|
+
// (« la fiche »): claude runs with `--output-format json --json-schema <the schema of its role>`
|
|
90
|
+
// (scripts/resultats.mjs), and its JSON result carries the sheet in `structured_output`, which
|
|
91
|
+
// the circuit reads (`lireFiche`): lancer.sh writes that result to
|
|
92
|
+
// `.claude/worktrees/<ticket>.resultat.json`, and a session out of the envelope prints it. Each
|
|
93
|
+
// sheet read is kept with the ticket's sheets, `.claude/worktrees/<ticket>.fiches.jsonl` (`{
|
|
94
|
+
// date, role, composant, fiche }`, the lot a review read joined to its sheet, `lot`), beside the
|
|
95
|
+
// circuit's own results: the report of each proof and check, which the reviewer receives, with
|
|
96
|
+
// its outcome (`resultat`), each stop (`arret`), each developpeur resumed who changed nothing
|
|
97
|
+
// (`inchange`), each session launched (`lancement`, its role and its session id), each end of a
|
|
98
|
+
// run on a stop (`montee`), which the bilan reads; and each of its discoveries (`decouvertes`) filed closed
|
|
99
|
+
// under the ticket (scripts/ticket.sh, label `decouverte`). A session that ends without a sheet
|
|
100
|
+
// that conforms (none, one out of its schema, a code other than 0) is resumed once, the same
|
|
101
|
+
// session, with the fifth template, which names what lacks; a second one stops the circuit at
|
|
102
|
+
// « sans-fiche ». The handover at the ticket (`bd comments add`) stays, for the human.
|
|
103
|
+
// - A fresh session of the testwriter or the developpeur opens under a session id the circuit sets
|
|
104
|
+
// (`lancer.sh --seance <uuid>`, claude's `--session-id`), kept in the state (`reprise`, its
|
|
105
|
+
// author and id). A relaunch of that agent on its step and component (the testwriter after a failed
|
|
106
|
+
// proof, the developpeur after red checks) resumes that session (`lancer.sh --reprise <id>`,
|
|
107
|
+
// claude's `--resume`, which finds its transcript in the copy's folder of transcripts, which
|
|
108
|
+
// the envelope mounts in writing over the real one) with the sixth template of consigne-agent.md
|
|
109
|
+
// (the ticket, the component, what changed, the new report), save the last attempt (the
|
|
110
|
+
// testwriter's third session, the developpeur's on the strong model), a fresh and whole session. A
|
|
111
|
+
// resumption that ends with a code other than 0, or without a sheet that conforms, writes so at
|
|
112
|
+
// the ticket and falls back on a fresh and whole session. A resumed session keeps its id, and so
|
|
113
|
+
// its one line in the register, its latest end.
|
|
114
|
+
// - A session whose transcripts (`~/.claude/projects/<copy in dashes>/`, as scripts/registre.mjs
|
|
115
|
+
// reads them) and log (its preparation writes there before the session opens) stand still for
|
|
116
|
+
// 20 minutes (IMMOBILE_MS; GRILLHOUSE_CIRCUIT_IMMOBILE_MS replaces
|
|
117
|
+
// it, in milliseconds) is stopped: SIGTERM, then SIGKILL, to the launcher's group.
|
|
118
|
+
// - Each comment at the ticket carries its author, which bd reads in BEADS_ACTOR (ACTEUR): each
|
|
119
|
+
// session runs with the author of its role (`testwriter <composant>`, `developpeur <composant>`,
|
|
120
|
+
// `relecteur`, `regulateur`), which the envelope passes on; the circuit writes its own
|
|
121
|
+
// lines under `circuit` and the reviewer's verdict under `relecteur`, and the queue writes under
|
|
122
|
+
// `file` (scripts/integration/file.mjs), whose last word the circuit names. The rights of every
|
|
123
|
+
// session refuse bd's `--actor`, `-a`/`--author` and the variable itself (AUTEUR_FIXE of
|
|
124
|
+
// scripts/droits.mjs, in the settings of every role's session), so that no agent changes its
|
|
125
|
+
// author.
|
|
126
|
+
// - The sheet of the testwriter and of the developpeur carries its status (`statut`): FAIT goes on as
|
|
127
|
+
// below; BESOIN (its `question`) stops the circuit at once at « besoin », the question and the
|
|
128
|
+
// handover as its report, with no session more; so does BLOQUÉ (its `raison`), at
|
|
129
|
+
// « hors-portee », save for a developpeur whose sheet sends the red to a testwriter (`renvoi`,
|
|
130
|
+
// below), which is that sending. The regulator settles either stop, as any other.
|
|
131
|
+
// - A component that carries no test (sansTests: in a repository of packages, no package with a
|
|
132
|
+
// script `test:vitest` lies in it or holds it, as `npm test -- <chemin>` refuses a path out of
|
|
133
|
+
// every package or in a package without that script) skips the testwriter and the proof: the
|
|
134
|
+
// circuit writes `Circuit · <composant> · testwriter : aucun test …` at the ticket and launches its
|
|
135
|
+
// developpeur, the reason as his report, then his checks.
|
|
136
|
+
// - Each command of the proof and of the checks runs with its own results directory
|
|
137
|
+
// (`GRILLHOUSE_RESULTATS`), where Turborepo writes its summary (scripts/tests.mjs,
|
|
138
|
+
// scripts/verifier.mjs), Vitest its JSON report (scripts/vitest-config/resultats.mjs) and each
|
|
139
|
+
// guard its SARIF; its output goes to the log, for the human. Each `npm test` of them carries
|
|
140
|
+
// the mark `GRILLHOUSE_DEPUIS_LE_VERT` (scripts/dernier-vert.mjs): in a package named whole,
|
|
141
|
+
// the tests that play are those Vitest relates to the files changed since the last green of the
|
|
142
|
+
// copy's branch (main's while the copy has none), the files named among them, as `npm run verifier` measures itself; a lot that
|
|
143
|
+
// changes one test plays that test, and those whose graph it reaches.
|
|
144
|
+
// - Each automatic pass (each command of the proof and of the checks, the final pass, the
|
|
145
|
+
// integration and the queue) plays under a progress directory of its own
|
|
146
|
+
// (`GRILLHOUSE_AVANCEMENT`, scripts/avancement.mjs), which its state names while it plays
|
|
147
|
+
// (`avancement`: `{ dossier }`, null between two), as Bazel counts its actions: Vitest writes
|
|
148
|
+
// there the files done and planned of each task, the queue the steps of its CI; the board shows
|
|
149
|
+
// them on the ticket's line (scripts/tableau.mjs). Its total is kept at its end under its kind
|
|
150
|
+
// (the step, the component and the command), which the next pass of that kind takes while all
|
|
151
|
+
// its tasks have not started. The survey of the components names its start in the state while it
|
|
152
|
+
// runs (`releve`).
|
|
153
|
+
// - The proof of the red plays the new tests alone, as test-driven development runs the test it
|
|
154
|
+
// has just written to see it red, not the whole suite: one run per package, `npm test -- --force
|
|
155
|
+
// <files>` in the copy, the test files the testwriter's lot adds or changes in the component (git
|
|
156
|
+
// status of the copy, the testwriter's files as the integration shares them, `isTesterFile`),
|
|
157
|
+
// played for real, since a run replayed from Turborepo's cache writes no report; the other
|
|
158
|
+
// tests of the component play at its checks and at the final pass. With no file of the lot in
|
|
159
|
+
// the component (a reader whose provider changed a type, no new test to write), each package of
|
|
160
|
+
// the component that has a script `test:vitest` plays whole, `npm test -- <package>`: main is
|
|
161
|
+
// green, so a test HEAD holds that turns red in the copy is a regression of the lot, a red,
|
|
162
|
+
// written `rouge existant : <file> › <name> — <message>`, and so is a file that does not load
|
|
163
|
+
// while no build of the component fails (it loaded at HEAD); its greens do not count. A measure
|
|
164
|
+
// on the same tree is not played again, as Turborepo replays a task whose inputs did not move: a
|
|
165
|
+
// testwriter relaunched who changes no file of the copy (an expected green added to his sheet
|
|
166
|
+
// alone) is judged on the results of the proof already played, his sheet read again. A file of
|
|
167
|
+
// the lot that does not load proves nothing; a failed build of a package of the component (a
|
|
168
|
+
// task `<package>#build` of the summary) is a red, as not compiling is failing, named with its
|
|
169
|
+
// log, and the files it keeps from loading play at the checks, after the developpeur; a failed
|
|
170
|
+
// build of a package out of the component leaves them broken. A green test passes only when a
|
|
171
|
+
// sheet of the testwriter of the component names it in `verts_attendus`: its file from the root
|
|
172
|
+
// and its full name (or its title), with its reason; one without `test`, for a test file HEAD
|
|
173
|
+
// holds, justifies each green test of that file and excuses no red; on a new file it justifies
|
|
174
|
+
// nothing, and the report says so for that file. A green test HEAD already holds has nothing to
|
|
175
|
+
// prove: the version of HEAD of each file plays in the same run, from a sibling file
|
|
176
|
+
// (`<nom>.circuit-avant.test.<ext>`, removed after it), and a green test whose full name HEAD's
|
|
177
|
+
// version holds, green there too, is set aside, counted; a body changed under the same name is
|
|
178
|
+
// not told apart. The proof passes with a red (of the lot, existing or of a build), even without
|
|
179
|
+
// a file of the lot in the component: a testwriter with no new test to write says so in his
|
|
180
|
+
// handover, the existing reds are enough. It fails with
|
|
181
|
+
// neither a file of the lot nor a red, with a broken file of the lot, or with a green of the lot
|
|
182
|
+
// no expected green justifies. A failed proof relaunches the testwriter with its report, twice at
|
|
183
|
+
// most. The report (reds, justified greens) is written at the ticket. A proof with a red
|
|
184
|
+
// launches the developpeur, and so does a proof without a red when a finding of the reviewer
|
|
185
|
+
// names him; else it skips him: the lot is a lot of tests alone.
|
|
186
|
+
// - A developpeur is launched on a red, of a proof or of a check, on a finding of the reviewer, or
|
|
187
|
+
// in a component without tests: a session that changes no file of its component (`git status
|
|
188
|
+
// --porcelain` of its directory and the content of each file it lists, taken before and after
|
|
189
|
+
// the session; the sheet and the handover do not count) stops the circuit at « sans-livraison »,
|
|
190
|
+
// its report naming the component and sending to the developpeur's handover.
|
|
191
|
+
// - A sheet of the developpeur whose `renvoi` names `testwriter <composant>` (a component of the title
|
|
192
|
+
// up to his own) says the red that remains lies in the tests of that component: the circuit
|
|
193
|
+
// plays it as a `RELANCE --depuis testwriter <composant>` of the regulator, the sending and the
|
|
194
|
+
// handover as its context, whether or not the session changed a file: that component's testwriter,
|
|
195
|
+
// the sending on his line CONTEXTE, then its steps, the components after it as `--depuis` plays
|
|
196
|
+
// them, the final pass and the reviewer; in a round of `renvois` (a RENDU, a `--depuis`), the
|
|
197
|
+
// renvois of his component and of the components after it stay pending, each played after it with
|
|
198
|
+
// its findings, as a queue of jobs grows and is never replaced. A component out of the title or
|
|
199
|
+
// after his own is no such sending. The same sending twice in a row, with no green checks between them, stops at
|
|
200
|
+
// « sans-livraison ».
|
|
201
|
+
// - The checks after the developpeur of a component judge that component, as Bazel plays the
|
|
202
|
+
// targets of a change: `npm test -- <test files of the lot>` that lie in it (and those of the
|
|
203
|
+
// shared test material whose package depends on none of the components after it), `npm test --
|
|
204
|
+
// <package>` for each of its packages, touched by the lot or not (a package without a script
|
|
205
|
+
// `test:vitest` plays only when the lot touches it), then the guards in the envelope of the
|
|
206
|
+
// component (`GRILLHOUSE_ENVELOPPE=<composant> npm run grillhouse:gardes`, and `npm run gardes`
|
|
207
|
+
// when the project has it). Each red is read in the results of its command: a
|
|
208
|
+
// failed test of the reports of Vitest, a finding of the guards' SARIF, a failed build or task of
|
|
209
|
+
// the summary; a red check none of them situates is one red of the command. Each red belongs to
|
|
210
|
+
// the component that holds its file (or its task's package), the deepest; one without a file
|
|
211
|
+
// (the root's, a project's guard without SARIF), or in a package no component of the title
|
|
212
|
+
// holds, belongs to none. Out of the final pass, the reds of the component are its developpeur's;
|
|
213
|
+
// the others wait for the final pass: another component's, a finding of a guard on a file of
|
|
214
|
+
// another package (the SARIF names its file and its package), and a red without a file, the
|
|
215
|
+
// global guards': they play at the final pass alone.
|
|
216
|
+
// - After the last component of the run, the final pass: `npm run verifier`, what the
|
|
217
|
+
// integration plays, as bors plays on a lot the very check of the merge. Turborepo's cache
|
|
218
|
+
// and the last green hold its cost: a task nothing moved replays from it, and in a package
|
|
219
|
+
// that plays again, the tests Vitest relates to the files changed since the last green of the
|
|
220
|
+
// copy (scripts/dernier-vert.mjs, main's while the copy has none); what the lot reaches plays again,
|
|
221
|
+
// by the dependencies, the inputs of the tasks and `grillhouse.lit` (a reader out of the title
|
|
222
|
+
// whose tests read a directory the lot changes included). After a review, save on the lot it
|
|
223
|
+
// accepted, the final pass is restricted to what changed since the lot it read and to its
|
|
224
|
+
// dependents, as Turborepo selects them (`laPasseFinale`): the circuit keeps the copy as it
|
|
225
|
+
// stands as a commit on that lot (`garderLeLot`), and plays `npm run verifier --
|
|
226
|
+
// --filter=...[<lot relu>...<copie>] --filter=//`, the root's tasks (the guards) with them;
|
|
227
|
+
// Turborepo reads a range from its merge base, the lot read, where a single `[<commit>]` would
|
|
228
|
+
// be read against HEAD. The whole final pass green keeps the tree of its lot (`verifie`). A red
|
|
229
|
+
// goes to the developpeur of the first component, the provider first, that owns one. A red in a
|
|
230
|
+
// package no component of the title holds, or that no component owns, stops at « controles »:
|
|
231
|
+
// the regulator decides, who refers the component's entry in the title upward, or has the lot
|
|
232
|
+
// corrected. A developpeur
|
|
233
|
+
// whose reds are all findings to settle (`a-arbitrer`) is not launched: the circuit stops at
|
|
234
|
+
// « controles ». From then on every check is the final pass.
|
|
235
|
+
// - The regeneration of the references (step 3.1 of scripts/etapes.mjs), as `rustc --bless`,
|
|
236
|
+
// `jest -u` and `cargo insta accept`: a project declares in its package.json the references it
|
|
237
|
+
// derives from the whole copy, which no envelope shows (a fingerprint of its output, a ceiling
|
|
238
|
+
// of a count), `grillhouse.references`: each `{ nom, commande, fichiers, tests, gardes }`, its
|
|
239
|
+
// name, the shell command that writes it again from the root of the copy, the files it writes
|
|
240
|
+
// (paths, directories or glob patterns, `*` and `**`, from the root, as the inputs of
|
|
241
|
+
// Turborepo), the test files that judge it, as `rustc --bless` ties a test to its expected
|
|
242
|
+
// outputs, and the guards of the project that judge it, by the name their SARIF finding carries
|
|
243
|
+
// (its guard or its rule). A final pass whose every red is one of the references declared (a
|
|
244
|
+
// failed test of one of their test files, a finding of one of their guards, whatever file it
|
|
245
|
+
// aims at, or a failed test or a finding on a file they cover) sends no developpeur: the circuit
|
|
246
|
+
// keeps the copy as a lot, runs each command on
|
|
247
|
+
// the whole copy, out of the envelope, under the lock of the machine, its output added to the
|
|
248
|
+
// log, keeps the copy again on that lot, then plays the final pass restricted to what the
|
|
249
|
+
// regeneration changed and its dependents (`--filter=...[<avant>...<après>] --filter=//`); that
|
|
250
|
+
// pass green after a whole one keeps its tree as verified. The files regenerated stay in the
|
|
251
|
+
// copy: the lot carries them, and the reviewer reads them with the rest of the lot, the step's
|
|
252
|
+
// report among the circuit's. A reference changed that the ticket's description names neither
|
|
253
|
+
// by its name nor by one of its files, a file changed that the files of no reference cover, a command at
|
|
254
|
+
// a code other than 0, a regeneration that changes nothing, or a pass after it red again on the
|
|
255
|
+
// references, stops at « references » (23), the copy restored as it was before the
|
|
256
|
+
// regeneration, each reference and file named: the regulator decides. Without the declaration,
|
|
257
|
+
// the step never runs.
|
|
258
|
+
// - The proof, the checks and the final pass are heavy passes: each holds the lock of the machine
|
|
259
|
+
// (scripts/verrou-de-machine.mjs), shared by every repository of the machine, and waits, the
|
|
260
|
+
// wait written to the log, while another measure holds it; the wait bounded, the pass is at 75,
|
|
261
|
+
// « à repasser ».
|
|
262
|
+
// - The report a developpeur receives names his own reds first, each on its line (test or
|
|
263
|
+
// finding, file and line, message, the beginning of a failed build's own log), then the count
|
|
264
|
+
// of the others and the path of the log; the report of a stop names every red with its
|
|
265
|
+
// component.
|
|
266
|
+
// - A check at 75 (« à repasser ») out of the final pass plays once more; verifier plays its
|
|
267
|
+
// tasks at 75 again itself, at rest, and its 75 is the final pass's. A red relaunches the
|
|
268
|
+
// developpeur with its report, twice at most, the third session on the strong model
|
|
269
|
+
// (`lancer.sh --modele opus --effort high`): three sessions for a component's checks, and three
|
|
270
|
+
// for the whole final pass whatever the components its reds go to (the last component's
|
|
271
|
+
// sessions counted in it), so that two components sending a red to each other stop at
|
|
272
|
+
// « controles ».
|
|
273
|
+
// - After the final pass, green, the reviewer: a session out of the envelope, which sees the whole
|
|
274
|
+
// repository with its index, claude itself (`claude -p --agent relecteur --session-id <uuid>`,
|
|
275
|
+
// CLAUDE_BIN replaces claude) in the ticket's copy, on Opus, under the rights and the
|
|
276
|
+
// context of its role (scripts/droits.mjs: the commands its skill names, its index), its instruction from the second template of
|
|
277
|
+
// consigne-agent.md: the ticket, its components, the path of the claims file (the ticket's
|
|
278
|
+
// description as `bd show` gives it at the session's launch, written by the circuit in its
|
|
279
|
+
// working folder, which the session may read; the instruction carries no criteria, which the
|
|
280
|
+
// reviewer's skill reads only after its first passes), the files of the lot (the testwriter's by
|
|
281
|
+
// `isTesterFile`, then the others) and the circuit's reports since its last sheet (its proofs and
|
|
282
|
+
// checks). Its errors go to the log under its header line, its line to the register
|
|
283
|
+
// (scripts/registre.mjs --copie), and it is watched as a session in the envelope is. Its sheet
|
|
284
|
+
// is its verdict (`verdict` ACCEPTÉ or RENDU, its `constats`, each `bloquant` with the criterion
|
|
285
|
+
// or rule it holds, or `remarque`, its `question`, `ecarts`): the circuit writes it at the ticket
|
|
286
|
+
// for the human, under the author `relecteur`, its first word the verdict, each blocking finding
|
|
287
|
+
// under its heading with its recipient, then the remarks (`texteDuVerdict`).
|
|
288
|
+
// - A sheet of the reviewer with a `question` (what goes up to the owner, a real word of the
|
|
289
|
+
// language written in the code) stops the circuit at « besoin », whatever its verdict, before
|
|
290
|
+
// it commits or relaunches anyone, the question and the verdict as its report.
|
|
291
|
+
// - RENDU: the blocking findings alone send the lot back, a remark reaching only the ticket (a
|
|
292
|
+
// verdict without a blocking finding is ACCEPTÉ, the sheet's schema refusing any other). Each
|
|
293
|
+
// blocking finding names its recipient, `role` testwriter or developpeur and `composant` one of
|
|
294
|
+
// the title (a path or a package name). The circuit plays again the components the findings
|
|
295
|
+
// name, the provider first, and those alone (`suivant`: the final pass before the review
|
|
296
|
+
// carried every other one through): its testwriter → preuve → developpeur → controles when a
|
|
297
|
+
// finding names its testwriter, else its developpeur → controles, the findings for each role as
|
|
298
|
+
// the report of its session (those for the developpeur after the proof's report); then the
|
|
299
|
+
// final pass, restricted to what the fix changes and its dependents, and the reviewer again, on
|
|
300
|
+
// the difference: the lot it read before is kept (`lot` of its sheet's entry), as Gerrit keeps
|
|
301
|
+
// each patch set, and the next review receives the command of the difference between the two
|
|
302
|
+
// lots, the files of the lot that changed since, and the findings before it, which it judges
|
|
303
|
+
// lifted or not (`suivi`); the rest of the lot it does not read again (its skill, « La
|
|
304
|
+
// relecture bornée »).
|
|
305
|
+
// A blocking finding without a valid recipient, or one that puts its criterion in question
|
|
306
|
+
// (`critere_en_cause`), stops at « relecture-arret »; so does the third RENDU since
|
|
307
|
+
// the ticket's last ACCEPTÉ. The regulator settles.
|
|
308
|
+
// - ACCEPTÉ: the integration, once the whole final pass has played on the lot accepted (its tree
|
|
309
|
+
// is `verifie`); else that pass first, once, its red going to its owner's developpeur and the
|
|
310
|
+
// lot then to the reviewer again. The circuit builds the lot from the copy, as the regulator
|
|
311
|
+
// built it: the files the copy changes against its HEAD, the testwriter's (`isTesterFile`: test
|
|
312
|
+
// files and the shared test material) in one patch, the others in another, each file against
|
|
313
|
+
// HEAD and a new one against nothing (`git diff --no-index /dev/null`), the copy's index
|
|
314
|
+
// untouched; the message `<type>(<composants>): <sujet> (<ticket>)`, the type read from the
|
|
315
|
+
// ticket's type (bug fix, chore, docs, else feat), its body written once from the final state:
|
|
316
|
+
// the criteria of the plan, then each deviation the sheets of the developpeurs and of the
|
|
317
|
+
// reviewer name (`ecarts`), once by its subject, from the last sheet that carries it, with the
|
|
318
|
+
// trailer Co-Authored-By. It runs
|
|
319
|
+
// scripts/integration/integrer.mjs from the main tree, its output added to the log: 0 is the
|
|
320
|
+
// commit on agent/<ticket> (which the circuit names from git), the copy removed, the branch in
|
|
321
|
+
// the queue; 75 (« à repasser ») plays it once more, then stops at « a-repasser »; any other
|
|
322
|
+
// code is its refusal, which stops at « integration ».
|
|
323
|
+
// - Then the queue: scripts/integration/file.mjs from the main tree, waited for, its output added
|
|
324
|
+
// to the log (it brings the branch onto main, pushes it and reads the CI of the commit pushed).
|
|
325
|
+
// A queue already running exits 0 at once: the circuit waits while the branch is in the queue
|
|
326
|
+
// and the queue's lock names a live process. Then the state of the ticket: closed, the circuit
|
|
327
|
+
// ends at « integre », exit 0 (the queue's last word at the ticket named with it, a CI it could
|
|
328
|
+
// not read); reopened (out of the queue, or a red CI), it stops at « integration », the queue's
|
|
329
|
+
// last word as its reason; still in the queue, it stops at « a-repasser », the queue's reason.
|
|
330
|
+
// - The working directory is the ticket's, `.claude/worktrees/<ticket>.travail`: the instructions,
|
|
331
|
+
// results and errors of the sessions, the patches and message of the integration. It lives as
|
|
332
|
+
// long as the ticket, as the workspace of a pipeline: a circuit relaunched takes it up again, no
|
|
333
|
+
// sweep touches it, the queue removes it at the ticket's closure, and scripts/retirer-copie.sh
|
|
334
|
+
// with the copy of a ticket closed without integration. The results and progress of the passes
|
|
335
|
+
// live in the base of the run (scripts/vitest-config/base-de-passe.mjs), removed at its end.
|
|
336
|
+
// - The state is `.claude/worktrees/<ticket>.circuit.json`, written again at each step and at the
|
|
337
|
+
// start and end of each session: `{ etape, composant, tentative, code, raison, rapport, dernier,
|
|
338
|
+
// finale, boucles, seance, regulateur, questionnement, processus, composants, renvois, atteints,
|
|
339
|
+
// verifie, mene, reprise, journal }`; `processus` the process of the circuit that writes it; `questionnement` the questioning under way (`{ objet, tour, seance
|
|
340
|
+
// }`: the plan or a decision, its round, the questionneur's session), null otherwise;
|
|
341
|
+
// `etape`
|
|
342
|
+
// is the state's name of a numbered step of the process (scripts/etapes.mjs: testwriter 2.1,
|
|
343
|
+
// preuve 2.2, developpeur 2.3, controles 2.4, or 3 the final pass once `finale`, references
|
|
344
|
+
// 3.1, relecture 4,
|
|
345
|
+
// integration 5.1, file 5.2), or integre at the end; `raison` names the stop; `composants` the
|
|
346
|
+
// components of the title, in their order, which the board reads for the rank of `composant`;
|
|
347
|
+
// `boucles` the counts of the bounded loops of the step under way, by their key (`tests`, the
|
|
348
|
+
// testwriter's sessions with their proofs; `code`, the developpeur's with their checks;
|
|
349
|
+
// `relecture`, the reviews; `arret`, the pass of the same stop at the arbitrage;
|
|
350
|
+
// `questionnement`, the round of a questioning), null at a step without one;
|
|
351
|
+
// `dernier` is the last component of the run; `finale` whether the final pass has begun; `seance` the process group of
|
|
352
|
+
// the session that runs, null between two; `regulateur` the regulator's session on the stop
|
|
353
|
+
// (`{ seance, arret, code, rapport, faits, ticket, signature }`: its id, the stop it settles,
|
|
354
|
+
// its code and its reason, the stop's facts, the fingerprints of the description and of the
|
|
355
|
+
// stop), while it runs and after its question, null otherwise; `renvois` the components a
|
|
356
|
+
// RENDU, `--depuis` or a gesture of the table plays again, each with its first step and its
|
|
357
|
+
// findings, and under `--depuis` its context (`contexte`), null out of such a round;
|
|
358
|
+
// `atteints` the components the circuit has carried through, each on its own (its proof without a
|
|
359
|
+
// red for its developpeur, its developpeur's delivery, its green checks, every one once the final
|
|
360
|
+
// pass is green), whatever the title gains before it, kept from one run to the next, empty when a
|
|
361
|
+
// run starts over from the first component's testwriter; `verifie` the tree of the lot the last
|
|
362
|
+
// whole final pass green played, null before; `mene` the start (an ISO date) of the step the
|
|
363
|
+
// circuit leads itself while it runs, null otherwise; `reprise` the session of the testwriter or
|
|
364
|
+
// developpeur its relaunches resume, `{ agent, seance }`, null before any. `--reprendre` starts again at the step and component of the state, its report given to the
|
|
365
|
+
// next session, and goes on to its `dernier`; after any stop, the death of the circuit with its
|
|
366
|
+
// caller included. A session the state names that still runs is waited for first, and stopped
|
|
367
|
+
// when it stands still as long as a motionless session.
|
|
368
|
+
// - `--depuis <testwriter|developpeur> <composant>` (a path or a package name the title names) has
|
|
369
|
+
// the lot corrected by one component: it plays it from that agent, the text of `--contexte` on
|
|
370
|
+
// the line CONTEXTE of each agent of that component, its testwriter then its developpeur, as a
|
|
371
|
+
// retry of GitLab CI plays the jobs after it with the same variables: that component's steps,
|
|
372
|
+
// its developpeur launched by a red of its proof as without `--depuis`; then each component after
|
|
373
|
+
// it that the circuit has not carried through yet (`atteints`), its whole circuit, testwriter,
|
|
374
|
+
// proof, developpeur and checks; one it has carried through plays nothing, the final pass judges
|
|
375
|
+
// it; then the final pass, the reviewer and the integration. It goes with no `--reprendre`.
|
|
376
|
+
// - On a stop other than 0 and 21, the table of the stops, tried line by line in its order (`genre`, its
|
|
377
|
+
// gesture, the case of BPScript 320.3 it settles): the same stop (its name, step and component)
|
|
378
|
+
// come back after a RELANCE on the same fingerprint (the files the copy changes against its HEAD
|
|
379
|
+
// and the ticket's description, taken at each stop) ends the circuit on it, for the supervisor,
|
|
380
|
+
// its decisions in its reason; « à repasser » ends at 75; reds to settle go to the regulator;
|
|
381
|
+
// a red of another component waits for its checks, or at the final pass goes to its developpeur
|
|
382
|
+
// (above); a developpeur's sending plays its testwriter (above);
|
|
383
|
+
// on `--reponse`, a component the title gained since the question plays from its testwriter, the
|
|
384
|
+
// stop as its context (the answer is in the plan), with no regulator resumed. Each gesture the
|
|
385
|
+
// circuit applies itself is written at the ticket, `Circuit · <composant> · arbitrage : <genre>
|
|
386
|
+
// → <geste>`, and counts as a RELANCE on its stop.
|
|
387
|
+
// - On a stop the table does not settle, the regulator: a session out of the envelope, as the
|
|
388
|
+
// reviewer's, claude itself (`claude -p --agent regulateur --session-id <uuid>`) in the
|
|
389
|
+
// ticket's copy (the main tree once the integration removed it), on Sonnet, under the rights
|
|
390
|
+
// and the context of its role (scripts/droits.mjs: the commands its skill names, the summary of
|
|
391
|
+
// the state by scripts/etat-du-circuit.mjs, the reads of a precedent by bd and of the copy by
|
|
392
|
+
// git, the reads of the log and the proofs, the log searched in pieces; no write at the ticket),
|
|
393
|
+
// its instruction from the third template of consigne-agent.md: the ticket, the stop's name and
|
|
394
|
+
// code, its step, component and reason, the paths of the state and the log, and the plan it
|
|
395
|
+
// judges on, the ticket's description, where a decision of the owner is poured. The state
|
|
396
|
+
// names its session (`regulateur`) while it runs. Its sheet is its decision (`decision`:
|
|
397
|
+
// « RELANCE --reprendre » with its `contexte`, « RELANCE --depuis » with in `depuis` one or more
|
|
398
|
+
// components of the title, each with its agent and its `contexte`, or « MONTE » with its
|
|
399
|
+
// `question`; its `faits`, the facts of the state it rests on; for a RELANCE,
|
|
400
|
+
// its `arbitrage`, what it settled alone with its source, and its `critere`, the criterion it
|
|
401
|
+
// adds). A
|
|
402
|
+
// fresh session's decision is questioned first, as the plan is, the regulator answering in its
|
|
403
|
+
// own session, resumed with the tenth template (its `reponses`, and the decision it ends on,
|
|
404
|
+
// changed or kept, the one the circuit checks and applies); a DÉSACCORD, or a fourth round still
|
|
405
|
+
// questioned, makes of the exchange the regulator's question, the stop « question », its session
|
|
406
|
+
// kept; a decision on the supervisor's answer is not questioned. The
|
|
407
|
+
// circuit checks it on the state before it applies it, an admission control (refusDe), each
|
|
408
|
+
// component of a --depuis judged: a component out of the title or named twice, a step the state
|
|
409
|
+
// does not hold, a developpeur whose testwriter has not run, a decision other than a RELANCE from a testwriter after the ticket's description changed (a
|
|
410
|
+
// criterion added: the table's `critere-neuf`), a fact the state proves false (au-titre,
|
|
411
|
+
// hors-titre, rouge-de, sans-changement, sous-charge), or the same RELANCE as the one before on
|
|
412
|
+
// the same stop with no fact the one before did not have, an `arbitrage` or a `critere` on a
|
|
413
|
+
// MONTE, or a `critere` that does not go to a testwriter first, is refused: written at the ticket
|
|
414
|
+
// (`Circuit · <composant> · regulateur : décision refusée — <raison>`), the same session resumed
|
|
415
|
+
// once with the reason (the seventh template); a second refusal ends the circuit on the stop,
|
|
416
|
+
// both reasons joined to its own. The circuit writes a decision admitted at the ticket
|
|
417
|
+
// (`Circuit · <composant> · regulateur : …`), then its arbitrage (`Circuit · <composant> ·
|
|
418
|
+
// regulateur : arbitrage — <texte>`, which scripts/consommateurs-arbitres.mjs and the
|
|
419
|
+
// integration read as settled) and its criterion, numbered after the last one at the end of the
|
|
420
|
+
// description (`bd update -d`). RELANCE runs the circuit again in the same process, as the
|
|
421
|
+
// option it names, the context as `--contexte`, a --depuis of several components as one of each
|
|
422
|
+
// in a single run: each played again in the order of the title, from its agent with its context
|
|
423
|
+
// (`renvois`), then the components after them the circuit has not carried through, then one final
|
|
424
|
+
// pass, as GitLab's « Retry failed jobs » retries every failed job in one go; MONTE ends the
|
|
425
|
+
// circuit at « question », the question as its reason, the regulator's session kept in the
|
|
426
|
+
// state. `--reponse <texte>` first
|
|
427
|
+
// pours the answer into the plan, as an RFC amended or an ADR, the plan being what every agent
|
|
428
|
+
// reads and the exchange its history: a fresh session of the technical referent, out of the
|
|
429
|
+
// envelope (the twelfth template: the question, the answer, the plan), changes the plan (a
|
|
430
|
+
// criterion added, removed or made precise, its title) or keeps it; the circuit writes the plan
|
|
431
|
+
// (`bd update`) and, the plan changed, surveys it again and has it questioned, as at 1.2, the
|
|
432
|
+
// referent answering in its own session, before anything else; a plan the answer leaves as it
|
|
433
|
+
// was is said at the ticket, with no questioning. Then it resumes the regulator's session
|
|
434
|
+
// (`claude -p --resume <id>`, the fourth template: the supervisor's answer, which may carry the
|
|
435
|
+
// owner's, and the plan as it leaves it) and applies the decision it gives the same way; without
|
|
436
|
+
// a session kept
|
|
437
|
+
// after a question, nor a stop « composants-manquants » or « questionnement » it answers, the
|
|
438
|
+
// call is refused. A session without a sheet that conforms is resumed
|
|
439
|
+
// once, as any session; a second one, or
|
|
440
|
+
// a session that cannot end (a launch that fails, a motionless session), ends the circuit on the
|
|
441
|
+
// stop it settled, the reason joined to its own.
|
|
442
|
+
// - `--decision <texte>`, a decision of the owner given in session, out of any stop « question »
|
|
443
|
+
// (at « question », « questionnement » or « composants-manquants » the call is refused: the
|
|
444
|
+
// stop waits for `--reponse`; so is it once the lot has reached the integration), takes the same
|
|
445
|
+
// path into the plan, as GitLab CI cancels a pipeline a new one makes redundant and Zuul the
|
|
446
|
+
// jobs of a change superseded: a circuit running on the ticket (the state's `processus`, alive)
|
|
447
|
+
// stops first, SIGTERM then SIGKILL, and so does the session it led (`seance`), said at the
|
|
448
|
+
// ticket (`Circuit · <composant> · decision : …`); then the technical referent pours the
|
|
449
|
+
// decision into the plan (the twelfth template, its question « aucune »), the circuit writes it,
|
|
450
|
+
// and a plan changed is surveyed again and questioned. The run resumes where the state left it,
|
|
451
|
+
// as `--reprendre`: each component the change reaches that it had passed (carried through,
|
|
452
|
+
// before its component, or all of them at the final pass and the review), and its own component
|
|
453
|
+
// past its testwriter when the change reaches it, plays again from its testwriter (`renvois`,
|
|
454
|
+
// the plan changed as its report, never the decision), the run starting at the first of them,
|
|
455
|
+
// its own component then resuming at its step; the components the change reaches are those the
|
|
456
|
+
// survey of the components (scripts/composants-affectes.mjs) finds in the lines it adds or
|
|
457
|
+
// removes, with each one the title gained, all of the title when the survey finds none. A
|
|
458
|
+
// decision that leaves the plan as it was is said at the ticket, and the run resumes as it was.
|
|
459
|
+
// Without a state, the run starts on the plan it leaves. The third RELANCE in a row on one stop (its
|
|
460
|
+
// name, step and component) is the last: when that stop comes again, the circuit ends on it, the
|
|
461
|
+
// three decisions in its reason, without a regulator.
|
|
462
|
+
// - At each step, a short comment at the ticket: `Circuit · <composant> · <étape> : <résumé>`.
|
|
463
|
+
// A proof writes there its reds and what refuses it; its whole report, the justified greens with
|
|
464
|
+
// their reasons, goes to `.claude/worktrees/<ticket>.preuve-<n>.md`, which the comment names.
|
|
465
|
+
// - The commands of the circuit (the proof, the checks, the integration) are added to the log of the ticket,
|
|
466
|
+
// `.claude/worktrees/<ticket>.log`, next to the sessions, each under its header line.
|
|
467
|
+
// - Each step the circuit leads itself, with no agent (the proof, the checks and the final pass,
|
|
468
|
+
// the integration, the queue), writes at its end, whatever its outcome, a line to the register
|
|
469
|
+
// of finished agents (scripts/registre.mjs, `ligneDEtape`): the role `circuit`, the ticket, the
|
|
470
|
+
// step and its component, its start and end, its working time, no tokens. The board counts it,
|
|
471
|
+
// and the step under way (`mene` in the state), in the ticket's working time.
|
|
472
|
+
// - At each of its ends (« integre », or a stop it hands to the supervisor), the circuit writes the
|
|
473
|
+
// ticket's bilan (scripts/bilan.mjs): its figures, measured on the register, its results and its
|
|
474
|
+
// state, kept in `.claude/worktrees/<ticket>.bilan.json` and written at the ticket
|
|
475
|
+
// in one short comment, `Circuit · <composant> · bilan : …`, compared with the bilan before it in
|
|
476
|
+
// the project.
|
|
477
|
+
// - Exit codes: 0 at « integre »; 2 a malformed call; a stop, by its name (ARRETS):
|
|
478
|
+
// 10 lancement (lancer.sh stopped before the session), 11 bloqué (a motionless transcript),
|
|
479
|
+
// 12 sans-fiche (twice no sheet that conforms), 13 preuve (the third proof fails), 14 controles
|
|
480
|
+
// (the third developpeur of a component's checks or of the final pass leaves a red check, a red
|
|
481
|
+
// of the final pass no component of the title owns, or reds to settle),
|
|
482
|
+
// 15 sans-livraison (a developpeur session that changes no file of its component; the first
|
|
483
|
+
// session of a `--reprendre` whose component already holds changes goes on to the checks; or
|
|
484
|
+
// the developpeur's same sending to a testwriter twice in a row, no green checks between them),
|
|
485
|
+
// 16 relecture-arret (a finding without a valid recipient, one on a criterion, the third RENDU),
|
|
486
|
+
// 17 integration (the integration script refuses the lot, or the queue reopens the ticket),
|
|
487
|
+
// 18 question (the regulator's question for the supervisor, `--reponse` resumes it),
|
|
488
|
+
// 19 hors-portee (a testwriter or developpeur whose sheet says BLOQUÉ, without a sending to a
|
|
489
|
+
// testwriter), 20 besoin (a testwriter or developpeur whose sheet says BESOIN, a reviewer whose sheet
|
|
490
|
+
// carries a `question`),
|
|
491
|
+
// 21 composants-manquants (a component the survey of the ticket finds and its title misses),
|
|
492
|
+
// 22 questionnement (the questioning of the plan ends on a disagreement, or its fourth round is
|
|
493
|
+
// still questioned),
|
|
494
|
+
// 75 a-repasser (a check or the integration still at 75 once played again, or the branch still
|
|
495
|
+
// in the queue).
|
|
496
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
497
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
498
|
+
import {
|
|
499
|
+
closeSync,
|
|
500
|
+
copyFileSync,
|
|
501
|
+
existsSync,
|
|
502
|
+
openSync,
|
|
503
|
+
mkdirSync,
|
|
504
|
+
mkdtempSync,
|
|
505
|
+
readdirSync,
|
|
506
|
+
readFileSync,
|
|
507
|
+
renameSync,
|
|
508
|
+
rmSync,
|
|
509
|
+
statSync,
|
|
510
|
+
appendFileSync,
|
|
511
|
+
writeFileSync,
|
|
512
|
+
} from 'node:fs';
|
|
513
|
+
import os from 'node:os';
|
|
514
|
+
import path from 'node:path';
|
|
515
|
+
import { pathToFileURL } from 'node:url';
|
|
516
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
517
|
+
import { ecrireLeBilan } from './bilan.mjs';
|
|
518
|
+
import { DEPUIS_LE_VERT } from './dernier-vert.mjs';
|
|
519
|
+
import { releve as releveDuTexte, repondre, verifierLeTitre } from './composants-affectes.mjs';
|
|
520
|
+
import { optionsDuRole } from './droits.mjs';
|
|
521
|
+
import { matieres, NOM_DE_TEST } from './consommateurs.mjs';
|
|
522
|
+
import { VARIABLE as VARIABLE_ENVELOPPE } from './enveloppe/composant.mjs';
|
|
523
|
+
import {
|
|
524
|
+
ARRETS,
|
|
525
|
+
AUTO,
|
|
526
|
+
boucle,
|
|
527
|
+
bouclesDe,
|
|
528
|
+
etapeDuCircuit,
|
|
529
|
+
gesteDe,
|
|
530
|
+
libelle,
|
|
531
|
+
nomDuRole as enMots,
|
|
532
|
+
QUESTIONNEUR,
|
|
533
|
+
REGULATEUR,
|
|
534
|
+
SUITE,
|
|
535
|
+
TABLE_DES_ARRETS,
|
|
536
|
+
} from './etapes.mjs';
|
|
537
|
+
import { paquets } from './paquets.mjs';
|
|
538
|
+
import { ecrire as ecrireAuRegistre, ligneDEtape, transcriptionsDeLaCopie } from './registre.mjs';
|
|
539
|
+
import {
|
|
540
|
+
lireFiche,
|
|
541
|
+
lireRapportsVitest,
|
|
542
|
+
lireResumes,
|
|
543
|
+
lireSarif,
|
|
544
|
+
RESULTATS,
|
|
545
|
+
schemaDeFiche,
|
|
546
|
+
} from './resultats.mjs';
|
|
547
|
+
import { AUTEUR_DE_LA_FILE } from './integration/file.mjs';
|
|
548
|
+
import { dossierCommun, lireEntrees } from './integration/file-attente.mjs';
|
|
549
|
+
import { AUTEUR_DU_VERDICT } from './integration/verdict.mjs';
|
|
550
|
+
import { attendreUnPeu } from './verrou-de-fichier.mjs';
|
|
551
|
+
import { AttenteDeLaMachine, avecLaMachine } from './verrou-de-machine.mjs';
|
|
552
|
+
import { garderLeTotal, ouvrirAvancement } from './avancement.mjs';
|
|
553
|
+
import { AVANCEMENT } from './vitest-config/avancement.mjs';
|
|
554
|
+
import { NOMS_DES_GARDES } from './gardes-du-cadre.mjs';
|
|
555
|
+
import { isTesterFile } from './verrous/verrou.mjs';
|
|
556
|
+
import { A_REPASSER } from './vitest-config/pression.mjs';
|
|
557
|
+
import { ouvrirBase, vivant } from './vitest-config/base-de-passe.mjs';
|
|
558
|
+
import { GREFFON } from './cadre.mjs';
|
|
559
|
+
|
|
560
|
+
// The time a session's transcripts may stand still before the circuit stops it.
|
|
561
|
+
export const IMMOBILE_MS = 20 * 60 * 1000;
|
|
562
|
+
// The exit code of each stop, from the list of the steps and stops.
|
|
563
|
+
export { ARRETS };
|
|
564
|
+
const CODE_USAGE = 2;
|
|
565
|
+
// The bounded loops, each declared with its step (scripts/etapes.mjs, `boucles`): the testwriter is
|
|
566
|
+
// relaunched twice at most after a failed proof (three sessions); the developpeur has three
|
|
567
|
+
// sessions, the last on the strong model; the third RENDU of one ticket stops the circuit; the
|
|
568
|
+
// third RELANCE in a row on one stop is the last; the fourth round of questioning still questioned
|
|
569
|
+
// goes up to the supervisor.
|
|
570
|
+
const RELANCES_TESTWRITER = boucle('tests').max - 1;
|
|
571
|
+
const SEANCES_DEVELOPPEUR = boucle('code').max;
|
|
572
|
+
const RELECTURES = boucle('relecture').max;
|
|
573
|
+
const RELANCES_DU_REGULATEUR = boucle('arret').max;
|
|
574
|
+
const TOURS = boucle('questionnement').max;
|
|
575
|
+
// The author who answers for a ticket's plan to the questionneur.
|
|
576
|
+
const REFERENT = 'referent-technique';
|
|
577
|
+
// The roles a finding of the reviewer may name: the agents of the steps run for each component.
|
|
578
|
+
const DESTINATAIRES = SUITE.filter(e => e.parComposant)
|
|
579
|
+
.flatMap(e => e.qui)
|
|
580
|
+
.filter(q => q !== AUTO);
|
|
581
|
+
// The steps of the circuit's state, read from the list of the steps.
|
|
582
|
+
const etatDe = n => SUITE.find(e => e.numero === n).etat;
|
|
583
|
+
const [TESTS, PREUVE, CODE, CONTROLES, REFERENCES, RELECTURE, INTEGRATION, EN_FILE] = [
|
|
584
|
+
'2.1',
|
|
585
|
+
'2.2',
|
|
586
|
+
'2.3',
|
|
587
|
+
'2.4',
|
|
588
|
+
'3.1',
|
|
589
|
+
'4',
|
|
590
|
+
'5.1',
|
|
591
|
+
'5.2',
|
|
592
|
+
].map(etatDe);
|
|
593
|
+
// The variable in which bd reads the author of a comment (`bd --help`: « default: $BEADS_ACTOR,
|
|
594
|
+
// git user.name, $USER »); the author of the circuit's own lines; the author of a session of
|
|
595
|
+
// <role> on <composant>: `testwriter <composant>`, `developpeur <composant>`, else the role alone.
|
|
596
|
+
const ACTEUR = 'BEADS_ACTOR';
|
|
597
|
+
const AUTEUR_DU_CIRCUIT = 'circuit';
|
|
598
|
+
const auteur = (role, composant) => (DESTINATAIRES.includes(role) ? `${role} ${composant}` : role);
|
|
599
|
+
// The sessions out of the envelope (the reviewer on Opus, the regulator on Sonnet), each in the
|
|
600
|
+
// ticket's copy, the main tree once the integration removed it, under the rights and the context
|
|
601
|
+
// of its role (scripts/droits.mjs: the commands its skill names, the regulator's reads of the
|
|
602
|
+
// ticket's log and proofs, the refusal of a change of its author at the ticket), without a
|
|
603
|
+
// prompt: any call its rights do not permit is refused.
|
|
604
|
+
const horsDeLEnveloppe = (ctx, role, modele) => [
|
|
605
|
+
'--model',
|
|
606
|
+
modele,
|
|
607
|
+
'--permission-mode',
|
|
608
|
+
'dontAsk',
|
|
609
|
+
...optionsDuRole(role, {
|
|
610
|
+
racine: ctx.racine,
|
|
611
|
+
copie: existsSync(ctx.copie) ? ctx.copie : ctx.racine,
|
|
612
|
+
ticket: ctx.ticket,
|
|
613
|
+
journal: ctx.journal,
|
|
614
|
+
preuves: `${ctx.copie}.preuve-*.md`,
|
|
615
|
+
}),
|
|
616
|
+
];
|
|
617
|
+
const FORT = ['--modele', 'opus', '--effort', 'high'];
|
|
618
|
+
const PREFIXE = 'Circuit · ';
|
|
619
|
+
const TICKET_SH = path.join(import.meta.dirname, 'ticket.sh');
|
|
620
|
+
// The steps the circuit leads itself, with no agent: each one counts in the ticket's working time.
|
|
621
|
+
const MENEES = new Set(SUITE.filter(e => e.qui.includes(AUTO) && e.etat).map(e => e.etat));
|
|
622
|
+
|
|
623
|
+
const ici = import.meta.dirname;
|
|
624
|
+
const LANCEUR = path.join(ici, 'enveloppe', 'lancer.sh');
|
|
625
|
+
const INTEGRER = path.join(ici, 'integration', 'integrer.mjs');
|
|
626
|
+
const FILE = path.join(ici, 'integration', 'file.mjs');
|
|
627
|
+
const REGISTRE = path.join(ici, 'registre.mjs');
|
|
628
|
+
// The templates of the instructions, in the skill of the supervisor, which the frame's plugin
|
|
629
|
+
// carries, read at the plugin's root (scripts/cadre.mjs).
|
|
630
|
+
const GABARIT = path.join(GREFFON, 'skills', 'pitmaster', 'references', 'consigne-agent.md');
|
|
631
|
+
|
|
632
|
+
// A stop of the circuit: its name (a key of ARRETS) and its report; for a stop of the checks, its
|
|
633
|
+
// reds read in the structured results (`{ composant, texte, detail }`) and its motive, the report
|
|
634
|
+
// without their list, which the regulator receives apart; the outcome of the step it stops, when
|
|
635
|
+
// it has one (a proof's), which the bilan reads; and the facts the table of the stops and the
|
|
636
|
+
// admission of the regulator's decision read (`faits`: `{ genre, composant, rouges,
|
|
637
|
+
// aRepasser }`, its kind in the table, the component its gesture plays, the components or
|
|
638
|
+
// packages that own its reds, whether a command of it was at 75).
|
|
639
|
+
class Arret extends Error {
|
|
640
|
+
constructor(nom, rapport, { rouges = null, motif = null, resultat = null, faits = {} } = {}) {
|
|
641
|
+
super(nom);
|
|
642
|
+
this.nom = nom;
|
|
643
|
+
this.rapport = rapport;
|
|
644
|
+
this.rouges = rouges;
|
|
645
|
+
this.motif = motif;
|
|
646
|
+
this.resultat = resultat;
|
|
647
|
+
this.faits = faits;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// A malformed call: its message.
|
|
652
|
+
class Usage extends Error {}
|
|
653
|
+
|
|
654
|
+
const sansCouleurs = t => stripVTControlCharacters(String(t ?? ''));
|
|
655
|
+
const premiereLigne = t => sansCouleurs(t).trim().split('\n')[0].slice(0, 300);
|
|
656
|
+
const fin = (t, n) => sansCouleurs(t).split('\n').slice(-n).join('\n');
|
|
657
|
+
const enUneLigne = t =>
|
|
658
|
+
sansCouleurs(t)
|
|
659
|
+
.split('\n')
|
|
660
|
+
.map(l => l.trim())
|
|
661
|
+
.filter(Boolean)
|
|
662
|
+
.join(' ')
|
|
663
|
+
.slice(0, 300);
|
|
664
|
+
|
|
665
|
+
// The options of the command line, or a Usage.
|
|
666
|
+
function lireArguments(argv) {
|
|
667
|
+
const o = {
|
|
668
|
+
ticket: null,
|
|
669
|
+
reprendre: false,
|
|
670
|
+
contexte: null,
|
|
671
|
+
depuis: null,
|
|
672
|
+
reponse: null,
|
|
673
|
+
decision: null,
|
|
674
|
+
};
|
|
675
|
+
for (let i = 0; i < argv.length; i++) {
|
|
676
|
+
const a = argv[i];
|
|
677
|
+
if (a === '--reprendre') o.reprendre = true;
|
|
678
|
+
else if (a === '--reponse') {
|
|
679
|
+
if (i + 1 >= argv.length) throw new Usage('--reponse attend la réponse du superviseur.');
|
|
680
|
+
o.reponse = argv[++i];
|
|
681
|
+
} else if (a === '--decision') {
|
|
682
|
+
if (i + 1 >= argv.length) throw new Usage('--decision attend la décision du responsable.');
|
|
683
|
+
o.decision = argv[++i];
|
|
684
|
+
} else if (a === '--depuis') {
|
|
685
|
+
const [etape, composant] = argv.slice(i + 1, i + 3);
|
|
686
|
+
if (!['testwriter', 'developpeur'].includes(etape))
|
|
687
|
+
throw new Usage('--depuis attend testwriter ou developpeur, puis un composant.');
|
|
688
|
+
if (!composant || composant.startsWith('-'))
|
|
689
|
+
throw new Usage('--depuis attend son composant.');
|
|
690
|
+
o.depuis = { etape, composant };
|
|
691
|
+
i += 2;
|
|
692
|
+
} else if (a === '--contexte') {
|
|
693
|
+
if (i + 1 >= argv.length) throw new Usage('--contexte attend son texte.');
|
|
694
|
+
o.contexte = argv[++i];
|
|
695
|
+
} else if (a.startsWith('-')) throw new Usage(`option inconnue : ${a}`);
|
|
696
|
+
else if (o.ticket) throw new Usage(`un seul ticket : ${o.ticket}, puis ${a}.`);
|
|
697
|
+
else o.ticket = a;
|
|
698
|
+
}
|
|
699
|
+
if (!o.ticket) throw new Usage('le ticket manque.');
|
|
700
|
+
if (o.depuis && o.reprendre) throw new Usage('--depuis et --reprendre ne vont pas ensemble.');
|
|
701
|
+
if (o.reponse !== null && (o.reprendre || o.depuis || o.contexte !== null))
|
|
702
|
+
throw new Usage('--reponse va seule : le régulateur rend la décision.');
|
|
703
|
+
if (o.decision !== null && (o.reprendre || o.depuis || o.contexte !== null || o.reponse !== null))
|
|
704
|
+
throw new Usage('--decision va seule : le circuit reprend là où il en était.');
|
|
705
|
+
return o;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// The main tree, from git's common directory: run from a copy, the circuit still finds the others.
|
|
709
|
+
function arbrePrincipal() {
|
|
710
|
+
const r = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
711
|
+
encoding: 'utf8',
|
|
712
|
+
});
|
|
713
|
+
if (r.status !== 0) throw new Usage("le dossier courant n'est pas dans un dépôt git.");
|
|
714
|
+
return path.dirname(r.stdout.trim());
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// The context of one run: the paths of the ticket, its components, how the circuit speaks to bd.
|
|
718
|
+
function contexte(o) {
|
|
719
|
+
const racine = arbrePrincipal();
|
|
720
|
+
const copie = path.join(racine, '.claude', 'worktrees', o.ticket);
|
|
721
|
+
// The comments of a long ticket pass the 1 MiB that a child process returns by default. bd runs
|
|
722
|
+
// under the author <qui>; `bd` under the circuit's.
|
|
723
|
+
const bdSous = (qui, ...a) =>
|
|
724
|
+
spawnSync('bd', a, {
|
|
725
|
+
cwd: racine,
|
|
726
|
+
encoding: 'utf8',
|
|
727
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
728
|
+
env: { ...process.env, [ACTEUR]: qui },
|
|
729
|
+
});
|
|
730
|
+
const bd = (...a) => bdSous(AUTEUR_DU_CIRCUIT, ...a);
|
|
731
|
+
const show = bd('show', o.ticket, '--json');
|
|
732
|
+
if (show.status !== 0) throw new Usage(`ticket ${o.ticket} illisible : ${show.stderr}`);
|
|
733
|
+
const t = JSON.parse(show.stdout)[0];
|
|
734
|
+
const base = existsSync(copie) ? copie : racine;
|
|
735
|
+
const liste = paquets(base);
|
|
736
|
+
const resoudre = nom => {
|
|
737
|
+
if (nom === '.') return '.';
|
|
738
|
+
const norme = path.normalize(nom).endsWith('/')
|
|
739
|
+
? path.normalize(nom).slice(0, -1)
|
|
740
|
+
: path.normalize(nom);
|
|
741
|
+
if (liste.some(p => p.dossier === norme)) return norme;
|
|
742
|
+
const parNom = liste.find(p => p.pkg.name === nom);
|
|
743
|
+
if (parNom) return parNom.dossier;
|
|
744
|
+
if (!norme.startsWith('..') && existsSync(path.join(base, norme))) return norme;
|
|
745
|
+
throw new Usage(
|
|
746
|
+
`le composant « ${nom} » du titre ne nomme ni un paquet ni un chemin du dépôt.`
|
|
747
|
+
);
|
|
748
|
+
};
|
|
749
|
+
// The subject and the components of the title <titre>, « numéro — sujet — composants », or a
|
|
750
|
+
// Usage.
|
|
751
|
+
const duTitre = titre => {
|
|
752
|
+
const parts = String(titre ?? '').split(' — ');
|
|
753
|
+
if (parts.length < 2) throw new Usage(`le titre de ${o.ticket} ne nomme pas ses composants.`);
|
|
754
|
+
return {
|
|
755
|
+
sujet: parts.length > 2 ? parts.slice(1, -1).join(' — ') : parts[0],
|
|
756
|
+
composants: parts
|
|
757
|
+
.at(-1)
|
|
758
|
+
.split(',')
|
|
759
|
+
.map(c => c.trim())
|
|
760
|
+
.filter(Boolean)
|
|
761
|
+
.map(resoudre),
|
|
762
|
+
};
|
|
763
|
+
};
|
|
764
|
+
const { sujet, composants } = duTitre(t?.title);
|
|
765
|
+
if (o.depuis) {
|
|
766
|
+
const c = composants.includes(o.depuis.composant)
|
|
767
|
+
? o.depuis.composant
|
|
768
|
+
: composants.find(x => {
|
|
769
|
+
try {
|
|
770
|
+
return resoudre(o.depuis.composant) === x;
|
|
771
|
+
} catch {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
if (!c) throw new Usage(`--depuis : ${o.depuis.composant} n'est pas un composant du titre.`);
|
|
776
|
+
o.depuis.composant = c;
|
|
777
|
+
}
|
|
778
|
+
// The working directory of the circuit is the ticket's, `<copie>.travail` beside its copy: what
|
|
779
|
+
// the circuit hands its sessions (their instructions, results and errors, the reviewer's
|
|
780
|
+
// statements, the integration's patches and message), which a session out of the envelope reads
|
|
781
|
+
// even when its circuit is killed. As the workspace of a pipeline (Jenkins, GitLab), it lives as
|
|
782
|
+
// long as the ticket: no sweep touches it, a circuit relaunched takes it up again, numbering its
|
|
783
|
+
// files after those it finds, and the queue removes it at the ticket's closure, or the removal of
|
|
784
|
+
// the copy of a ticket closed without integration (scripts/retirer-copie.sh). The results and
|
|
785
|
+
// the progress of the circuit's passes live in the base of its run
|
|
786
|
+
// (scripts/vitest-config/base-de-passe.mjs), removed at its end, swept when the run is killed.
|
|
787
|
+
const tmp = `${copie}.travail`;
|
|
788
|
+
mkdirSync(tmp, { recursive: true });
|
|
789
|
+
const numero = Math.max(
|
|
790
|
+
0,
|
|
791
|
+
...readdirSync(tmp).map(n => Number(/-(\d+)(?:\.[a-z]+)?$/.exec(n)?.[1] ?? 0))
|
|
792
|
+
);
|
|
793
|
+
const baseDuRun = ouvrirBase('circuit');
|
|
794
|
+
return {
|
|
795
|
+
...o,
|
|
796
|
+
// The components a relaunch --depuis plays again, each with its step and its context.
|
|
797
|
+
depuis: o.depuis ? [{ ...o.depuis, contexte: o.contexte }] : null,
|
|
798
|
+
racine,
|
|
799
|
+
copie,
|
|
800
|
+
bd,
|
|
801
|
+
bdSous,
|
|
802
|
+
resoudre,
|
|
803
|
+
duTitre,
|
|
804
|
+
type: String(t.issue_type ?? ''),
|
|
805
|
+
sujet,
|
|
806
|
+
composants,
|
|
807
|
+
etat: `${copie}.circuit.json`,
|
|
808
|
+
journal: `${copie}.log`,
|
|
809
|
+
fin: `${copie}.fin`,
|
|
810
|
+
resultat: `${copie}.resultat.json`,
|
|
811
|
+
fiches: `${copie}.fiches.jsonl`,
|
|
812
|
+
transcriptions: transcriptionsDeLaCopie(copie, os.homedir()),
|
|
813
|
+
immobileMs: Number(process.env.GRILLHOUSE_CIRCUIT_IMMOBILE_MS) || IMMOBILE_MS,
|
|
814
|
+
base: baseDuRun,
|
|
815
|
+
tmp,
|
|
816
|
+
numero,
|
|
817
|
+
// The regulator's RELANCE decisions in a row on one stop.
|
|
818
|
+
relances: [],
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Every comment at the ticket, in their order, `{ author, text }` as Beads gives them; a failed
|
|
823
|
+
// read throws.
|
|
824
|
+
function lesCommentaires(ctx) {
|
|
825
|
+
const r = ctx.bd('comments', ctx.ticket, '--json');
|
|
826
|
+
if (r.status !== 0) {
|
|
827
|
+
throw new Error(
|
|
828
|
+
`commentaires de ${ctx.ticket} illisibles : ${r.error?.message ?? r.stderr ?? ''}`.trim()
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
return (JSON.parse(r.stdout || '[]') ?? []).map(c => ({
|
|
832
|
+
author: String(c.author ?? ''),
|
|
833
|
+
text: String(c.text ?? ''),
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// The ticket's description (its criteria), notes and title as Beads holds them at this moment, read
|
|
838
|
+
// at the launch of each session: `{ description, notes, titre }`; a failed read throws.
|
|
839
|
+
function leTicket(ctx) {
|
|
840
|
+
const r = ctx.bd('show', ctx.ticket, '--json');
|
|
841
|
+
if (r.status !== 0) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`ticket ${ctx.ticket} illisible : ${r.error?.message ?? r.stderr ?? ''}`.trim()
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const t = JSON.parse(r.stdout || '[]')[0] ?? {};
|
|
847
|
+
return {
|
|
848
|
+
description: String(t.description ?? ''),
|
|
849
|
+
notes: String(t.notes ?? ''),
|
|
850
|
+
titre: String(t.title ?? ''),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// The comments of the agents at the ticket, `{ author, text }`: every comment but the circuit's
|
|
855
|
+
// own. The circuit reads none of them: it hands an agent's last one to the next session of its
|
|
856
|
+
// author, and names it in a stop.
|
|
857
|
+
const passations = ctx => lesCommentaires(ctx).filter(c => c.author !== AUTEUR_DU_CIRCUIT);
|
|
858
|
+
|
|
859
|
+
// The last handover at the ticket of the author <qui>, or null.
|
|
860
|
+
const derniereDe = (ctx, qui) =>
|
|
861
|
+
passations(ctx)
|
|
862
|
+
.filter(c => c.author === qui)
|
|
863
|
+
.at(-1) ?? null;
|
|
864
|
+
|
|
865
|
+
// The sheets of the ticket's sessions and the circuit's reports, in their order
|
|
866
|
+
// (`<ticket>.fiches.jsonl`, beside the copy): `{ date, role, composant, fiche }` for the sheet of
|
|
867
|
+
// a session, `{ date, role: 'circuit', etape, composant, rapport }` for the report of a proof or of
|
|
868
|
+
// checks, which the reviewer receives.
|
|
869
|
+
function lesFiches(ctx) {
|
|
870
|
+
return lire(ctx.fiches)
|
|
871
|
+
.split('\n')
|
|
872
|
+
.filter(Boolean)
|
|
873
|
+
.flatMap(l => {
|
|
874
|
+
try {
|
|
875
|
+
return [JSON.parse(l)];
|
|
876
|
+
} catch {
|
|
877
|
+
return [];
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
const inscrire = (ctx, entree) =>
|
|
882
|
+
appendFileSync(ctx.fiches, `${JSON.stringify({ date: new Date().toISOString(), ...entree })}\n`);
|
|
883
|
+
|
|
884
|
+
// The sheets of <role> on <composant> (every component when null), in their order.
|
|
885
|
+
const fichesDe = (ctx, role, composant = null) =>
|
|
886
|
+
lesFiches(ctx)
|
|
887
|
+
.filter(e => e.role === role && (composant === null || e.composant === composant))
|
|
888
|
+
.map(e => e.fiche);
|
|
889
|
+
|
|
890
|
+
// The step of the circuit's state <etat> in words, « 2.3 Code » (scripts/etapes.mjs), or its
|
|
891
|
+
// `etape` when the list holds none.
|
|
892
|
+
const ouDansLeCircuit = etat => {
|
|
893
|
+
const e = etapeDuCircuit(etat);
|
|
894
|
+
return e ? libelle(e) : etat.etape;
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// One line of the circuit's history at the ticket.
|
|
898
|
+
function commenter(ctx, composant, etape, texte) {
|
|
899
|
+
const r = ctx.bd('comments', 'add', ctx.ticket, `${PREFIXE}${composant} · ${etape} : ${texte}`);
|
|
900
|
+
if (r.status !== 0) process.stderr.write(`⚠️ commentaire non écrit au ticket : ${r.stderr}\n`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// The report of a proof or of checks: its line at the ticket, and its report kept for the
|
|
904
|
+
// reviewer, <rapport> when the comment carries only its summary, beside its outcome <resultat>,
|
|
905
|
+
// which the bilan reads (scripts/bilan.mjs): for a proof `{ ok, rouges, justifies, sans, casses,
|
|
906
|
+
// anciens, vide }`, for checks `{ issue, finale }`.
|
|
907
|
+
function ecrireRapport(ctx, composant, etape, texte, resultat, rapport = texte) {
|
|
908
|
+
commenter(ctx, composant, etape, texte);
|
|
909
|
+
inscrire(ctx, {
|
|
910
|
+
role: AUTEUR_DU_CIRCUIT,
|
|
911
|
+
etape,
|
|
912
|
+
composant,
|
|
913
|
+
resultat,
|
|
914
|
+
rapport: `${PREFIXE}${composant} · ${etape} : ${rapport}`,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// The state of the run, written whole (`seance` and `regulateur` null and `finale` false unless <etat>
|
|
919
|
+
// sets them, `processus`, `renvois`, `atteints`, `verifie` and `reprise` the run's), kept as the
|
|
920
|
+
// run's current state.
|
|
921
|
+
function ecrireEtat(ctx, etat) {
|
|
922
|
+
ctx.courant = {
|
|
923
|
+
finale: false,
|
|
924
|
+
seance: null,
|
|
925
|
+
regulateur: null,
|
|
926
|
+
questionnement: null,
|
|
927
|
+
mene: null,
|
|
928
|
+
avancement: null,
|
|
929
|
+
releve: null,
|
|
930
|
+
...etat,
|
|
931
|
+
processus: process.pid,
|
|
932
|
+
composants: ctx.composants,
|
|
933
|
+
renvois: ctx.renvois ?? null,
|
|
934
|
+
atteints: ctx.atteints ?? [],
|
|
935
|
+
verifie: ctx.verifie ?? null,
|
|
936
|
+
reprise: ctx.reprise ?? null,
|
|
937
|
+
};
|
|
938
|
+
const provisoire = `${ctx.etat}.${process.pid}`;
|
|
939
|
+
writeFileSync(
|
|
940
|
+
provisoire,
|
|
941
|
+
`${JSON.stringify({ ...ctx.courant, journal: ctx.journal }, null, 2)}\n`
|
|
942
|
+
);
|
|
943
|
+
renameSync(provisoire, ctx.etat);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// The register's line of the step <etape> of <composant> the circuit led itself from <debut> (an
|
|
947
|
+
// ISO date) to now; a register it cannot write leaves the circuit going.
|
|
948
|
+
function inscrireEtape(ctx, { etape, composant, debut }) {
|
|
949
|
+
try {
|
|
950
|
+
ecrireAuRegistre(
|
|
951
|
+
ligneDEtape({
|
|
952
|
+
projet: ctx.racine,
|
|
953
|
+
ticket: ctx.ticket,
|
|
954
|
+
etape,
|
|
955
|
+
composant,
|
|
956
|
+
debut,
|
|
957
|
+
fin: new Date().toISOString(),
|
|
958
|
+
})
|
|
959
|
+
);
|
|
960
|
+
} catch (e) {
|
|
961
|
+
process.stderr.write(`⚠️ étape ${etape} non écrite au registre : ${e.message}\n`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const entete = (ctx, etape, composant) =>
|
|
966
|
+
appendFileSync(
|
|
967
|
+
ctx.journal,
|
|
968
|
+
`=== ${new Date().toISOString()} · circuit · ${etape} · ${composant} ===\n`
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// The latest moment a transcript of the copy moved, or 0.
|
|
972
|
+
function derniereTrace(dossier) {
|
|
973
|
+
if (!existsSync(dossier)) return 0;
|
|
974
|
+
let m = 0;
|
|
975
|
+
for (const f of readdirSync(dossier, { recursive: true })) {
|
|
976
|
+
if (!String(f).endsWith('.jsonl')) continue;
|
|
977
|
+
try {
|
|
978
|
+
m = Math.max(m, statSync(path.join(dossier, String(f))).mtimeMs);
|
|
979
|
+
} catch {
|
|
980
|
+
// A transcript removed meanwhile does not move.
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return m;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const mtime = f => {
|
|
987
|
+
try {
|
|
988
|
+
return statSync(f).mtimeMs;
|
|
989
|
+
} catch {
|
|
990
|
+
return 0;
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// One launch of a session: `{ code, seance, bloque, erreur, sortie }`, `seance` the session's
|
|
995
|
+
// exit code (from lancer.sh's end file for a session in the envelope), null when the launch
|
|
996
|
+
// stopped before it, `sortie` claude's JSON result, its sheet in `structured_output`. Every
|
|
997
|
+
// session runs with the author of <role> on <composant> (BEADS_ACTOR). A session in the envelope
|
|
998
|
+
// opens under the id <ouvre> (lancer.sh --seance), or resumes the session <reprend> (lancer.sh
|
|
999
|
+
// --reprise); its result is the file lancer.sh writes (`<ticket>.resultat.json`).
|
|
1000
|
+
// A session <horsEnveloppe> (the reviewer's, the regulator's) is claude itself, in the ticket's
|
|
1001
|
+
// copy (the main tree once the copy is gone), under <droits> (`horsDeLEnveloppe`), the
|
|
1002
|
+
// schema of its role's sheet and the options <session> names (its id, or the session it
|
|
1003
|
+
// resumes), its instruction on its standard input and its result on its standard output, added
|
|
1004
|
+
// to the log under its header line, then its line written to the register, as lancer.sh does.
|
|
1005
|
+
// Its launch is kept with the circuit's results (`lancement`: its role and its session id), which
|
|
1006
|
+
// the bilan reads to count the sessions the circuit launched (scripts/bilan.mjs).
|
|
1007
|
+
function lancer(
|
|
1008
|
+
ctx,
|
|
1009
|
+
role,
|
|
1010
|
+
composant,
|
|
1011
|
+
consigne,
|
|
1012
|
+
{ fort, horsEnveloppe, droits = [], session = [], ouvre, reprend } = {}
|
|
1013
|
+
) {
|
|
1014
|
+
rmSync(ctx.fin, { force: true });
|
|
1015
|
+
rmSync(ctx.resultat, { force: true });
|
|
1016
|
+
const id = ouvre ?? reprend ?? session[1] ?? null;
|
|
1017
|
+
if (id) {
|
|
1018
|
+
inscrire(ctx, { role: AUTEUR_DU_CIRCUIT, composant, lancement: { role, seance: id } });
|
|
1019
|
+
}
|
|
1020
|
+
const fichier = path.join(ctx.tmp, `consigne-${++ctx.numero}.md`);
|
|
1021
|
+
writeFileSync(fichier, consigne);
|
|
1022
|
+
// A file, not a pipe: a session whose circuit is dead writes its errors without breaking.
|
|
1023
|
+
const erreurs = path.join(ctx.tmp, `erreurs-${ctx.numero}.txt`);
|
|
1024
|
+
let commande;
|
|
1025
|
+
let fds;
|
|
1026
|
+
// The result of a session out of the envelope.
|
|
1027
|
+
const reponse = path.join(ctx.tmp, `reponse-${ctx.numero}.json`);
|
|
1028
|
+
if (horsEnveloppe) {
|
|
1029
|
+
entete(ctx, role, composant);
|
|
1030
|
+
fds = [openSync(fichier, 'r'), openSync(reponse, 'w'), openSync(ctx.journal, 'a')];
|
|
1031
|
+
const fiche = ['--output-format', 'json', '--json-schema', JSON.stringify(schemaDeFiche(role))];
|
|
1032
|
+
commande = {
|
|
1033
|
+
cmd: process.env.CLAUDE_BIN || 'claude',
|
|
1034
|
+
args: ['-p', '--agent', role, ...fiche, ...droits, '--add-dir', ctx.tmp, ...session],
|
|
1035
|
+
cwd: existsSync(ctx.copie) ? ctx.copie : ctx.racine,
|
|
1036
|
+
};
|
|
1037
|
+
} else {
|
|
1038
|
+
fds = ['ignore', 'ignore', openSync(erreurs, 'w')];
|
|
1039
|
+
commande = {
|
|
1040
|
+
cmd: 'bash',
|
|
1041
|
+
args: [
|
|
1042
|
+
LANCEUR,
|
|
1043
|
+
...(fort ? FORT : []),
|
|
1044
|
+
...(ouvre ? ['--seance', ouvre] : []),
|
|
1045
|
+
...(reprend ? ['--reprise', reprend] : []),
|
|
1046
|
+
ctx.ticket,
|
|
1047
|
+
role,
|
|
1048
|
+
composant,
|
|
1049
|
+
fichier,
|
|
1050
|
+
],
|
|
1051
|
+
cwd: ctx.racine,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
return new Promise(resolve => {
|
|
1055
|
+
// The session writes at the ticket under the author of its role.
|
|
1056
|
+
const enfant = spawn(commande.cmd, commande.args, {
|
|
1057
|
+
cwd: commande.cwd,
|
|
1058
|
+
detached: true,
|
|
1059
|
+
stdio: fds,
|
|
1060
|
+
env: { ...process.env, [ACTEUR]: auteur(role, composant) },
|
|
1061
|
+
});
|
|
1062
|
+
for (const fd of new Set(fds.filter(f => typeof f === 'number'))) closeSync(fd);
|
|
1063
|
+
let erreurDeLancement = null;
|
|
1064
|
+
ecrireEtat(ctx, { ...ctx.courant, seance: enfant.pid ?? null });
|
|
1065
|
+
let bloque = false;
|
|
1066
|
+
let fini = false;
|
|
1067
|
+
const veille = surveiller(ctx, enfant.pid, () => (bloque = true));
|
|
1068
|
+
// A command that does not start (claude absent) ends with its error, whether or not its
|
|
1069
|
+
// `close` follows.
|
|
1070
|
+
enfant.on('error', e => {
|
|
1071
|
+
erreurDeLancement = e.message;
|
|
1072
|
+
if (!enfant.pid) enfant.emit('close', null);
|
|
1073
|
+
});
|
|
1074
|
+
enfant.on('close', code => {
|
|
1075
|
+
if (fini) return;
|
|
1076
|
+
fini = true;
|
|
1077
|
+
clearInterval(veille);
|
|
1078
|
+
// What remains of the group goes with its launcher.
|
|
1079
|
+
if (bloque) signalerGroupe(enfant.pid, 'SIGKILL');
|
|
1080
|
+
ecrireEtat(ctx, { ...ctx.courant, seance: null });
|
|
1081
|
+
let seance;
|
|
1082
|
+
let sortie;
|
|
1083
|
+
if (horsEnveloppe) {
|
|
1084
|
+
seance = erreurDeLancement === null ? code : null;
|
|
1085
|
+
sortie = lire(reponse).trim();
|
|
1086
|
+
appendFileSync(ctx.journal, `${sortie}\n`);
|
|
1087
|
+
spawnSync(process.execPath, [REGISTRE, '--copie', ctx.copie], {
|
|
1088
|
+
cwd: ctx.racine,
|
|
1089
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1090
|
+
});
|
|
1091
|
+
} else {
|
|
1092
|
+
seance = existsSync(ctx.fin) ? Number(readFileSync(ctx.fin, 'utf8').trim()) : null;
|
|
1093
|
+
sortie = lire(ctx.resultat).trim();
|
|
1094
|
+
}
|
|
1095
|
+
resolve({ code, seance, bloque, erreur: erreurDeLancement ?? lire(erreurs), sortie });
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// <signal> to the process group <groupe>; false when the group is gone (or not the circuit's).
|
|
1101
|
+
function signalerGroupe(groupe, signal) {
|
|
1102
|
+
try {
|
|
1103
|
+
process.kill(-groupe, signal);
|
|
1104
|
+
return true;
|
|
1105
|
+
} catch {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// The watch of the session whose launcher leads the group <groupe>: its transcripts and the log
|
|
1111
|
+
// standing still for the delay, SIGTERM, then SIGKILL, to the group, and <bloque> called. Its
|
|
1112
|
+
// interval, which the caller clears.
|
|
1113
|
+
function surveiller(ctx, groupe, bloque) {
|
|
1114
|
+
let bouge = Date.now();
|
|
1115
|
+
let arrete = false;
|
|
1116
|
+
const pas = Math.max(100, Math.min(30_000, Math.floor(ctx.immobileMs / 4)));
|
|
1117
|
+
return setInterval(() => {
|
|
1118
|
+
bouge = Math.max(bouge, derniereTrace(ctx.transcriptions), mtime(ctx.journal));
|
|
1119
|
+
if (!arrete && Date.now() - bouge >= ctx.immobileMs) {
|
|
1120
|
+
arrete = true;
|
|
1121
|
+
bloque();
|
|
1122
|
+
signalerGroupe(groupe, 'SIGTERM');
|
|
1123
|
+
setTimeout(() => signalerGroupe(groupe, 'SIGKILL'), 5_000).unref();
|
|
1124
|
+
}
|
|
1125
|
+
}, pas);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// The wait for the session of group <groupe> that a dead circuit left running, under the same
|
|
1129
|
+
// watch as a session: a resumption starts once it has ended, so that two sessions never write
|
|
1130
|
+
// together in the copy.
|
|
1131
|
+
async function attendreLaSeance(ctx, groupe) {
|
|
1132
|
+
if (!groupe || !signalerGroupe(groupe, 0)) return;
|
|
1133
|
+
process.stderr.write(
|
|
1134
|
+
`… la séance du groupe ${groupe}, laissée par l'arrêt d'avant, tourne encore : le circuit attend sa fin.\n`
|
|
1135
|
+
);
|
|
1136
|
+
const veille = surveiller(ctx, groupe, () => {});
|
|
1137
|
+
while (signalerGroupe(groupe, 0)) await new Promise(r => setTimeout(r, 500));
|
|
1138
|
+
clearInterval(veille);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// The template of the instruction at <rang> (0 the testwriter's and the developpeur's, 1 the
|
|
1142
|
+
// reviewer's, 2 the regulator's on a stop, 3 the regulator's on the supervisor's answer, 4 any
|
|
1143
|
+
// session's on a sheet absent or out of its schema, 5 the testwriter's and the developpeur's on a
|
|
1144
|
+
// resumption, 6 the regulator's on a decision the circuit refuses, 7 to 10 the questioning's, 11
|
|
1145
|
+
// the technical referent's on the supervisor's answer or the owner's decision), filled with
|
|
1146
|
+
// <valeurs>.
|
|
1147
|
+
function consigne(valeurs, rang = 0) {
|
|
1148
|
+
const texte = readFileSync(GABARIT, 'utf8');
|
|
1149
|
+
const bloc = [...texte.matchAll(/```text\n([\s\S]*?)\n```/g)][rang];
|
|
1150
|
+
if (!bloc) throw new Error(`gabarit sans bloc de consigne n° ${rang + 1} : ${GABARIT}`);
|
|
1151
|
+
return `${bloc[1].replace(/\{\{(\w+)\}\}/g, (m, k) => (k in valeurs ? valeurs[k] : m))}\n`;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const nomDuRole = role => (role === 'developpeur' ? 'développeur' : role);
|
|
1155
|
+
|
|
1156
|
+
// One session of <role> on <composant>: its sheet. A relaunch on its step (<tentative> past 1)
|
|
1157
|
+
// that is not the last (<derniere>) resumes the agent's session the circuit keeps (`ctx.reprise`,
|
|
1158
|
+
// its author and id) with the short instruction (the sixth template); a resumption that fails (a
|
|
1159
|
+
// code other than 0, or no sheet that conforms) falls back on a fresh and whole session, under a
|
|
1160
|
+
// new id, which the circuit keeps. A fresh session's instruction (the first template) carries the
|
|
1161
|
+
// ticket's description and notes as they are at its launch, and the last handover of its author
|
|
1162
|
+
// at the ticket. Throws an Arret for a launch that stops before its session, a motionless
|
|
1163
|
+
// session, or a session that ends twice without a sheet that conforms.
|
|
1164
|
+
async function seance(ctx, role, composant, { tentative, rapport, fort, derniere }) {
|
|
1165
|
+
const agent = auteur(role, composant);
|
|
1166
|
+
const nom = nomDuRole(role);
|
|
1167
|
+
const reprise = tentative > 1 && !derniere && ctx.reprise?.agent === agent ? ctx.reprise : null;
|
|
1168
|
+
if (reprise) {
|
|
1169
|
+
const courte = {
|
|
1170
|
+
role: nom,
|
|
1171
|
+
ticket: ctx.ticket,
|
|
1172
|
+
sujet: ctx.sujet,
|
|
1173
|
+
composant,
|
|
1174
|
+
tentative,
|
|
1175
|
+
change:
|
|
1176
|
+
role === 'testwriter'
|
|
1177
|
+
? 'la preuve du rouge, jouée après ta séance, refuse ton lot'
|
|
1178
|
+
: 'les contrôles, joués après ta séance, sont rouges',
|
|
1179
|
+
rapport: rapport ?? 'aucun',
|
|
1180
|
+
};
|
|
1181
|
+
const r = await lancer(ctx, role, composant, consigne(courte, 5), {
|
|
1182
|
+
fort,
|
|
1183
|
+
reprend: reprise.seance,
|
|
1184
|
+
});
|
|
1185
|
+
seanceLancee(ctx, role, r);
|
|
1186
|
+
const lu = r.seance === 0 ? lireFiche(role, r.sortie) : { erreur: `code ${r.seance}` };
|
|
1187
|
+
if (lu.fiche) return garder(ctx, role, composant, lu.fiche);
|
|
1188
|
+
commenter(
|
|
1189
|
+
ctx,
|
|
1190
|
+
composant,
|
|
1191
|
+
role,
|
|
1192
|
+
`reprise de la séance ${reprise.seance} sans fiche conforme (${lu.erreur}) : séance neuve et entière.`
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
const lots = role === 'developpeur' ? testsDuLot(ctx) : testsDuComposant(ctx, composant);
|
|
1196
|
+
const { description, notes } = leTicket(ctx);
|
|
1197
|
+
// The last handover of this agent on its component; for a developpeur without one yet, the last
|
|
1198
|
+
// of the testwriter of his component.
|
|
1199
|
+
const passation =
|
|
1200
|
+
derniereDe(ctx, agent) ??
|
|
1201
|
+
(role === 'developpeur' ? derniereDe(ctx, auteur('testwriter', composant)) : null);
|
|
1202
|
+
const valeurs = {
|
|
1203
|
+
role: nom,
|
|
1204
|
+
depot: ctx.racine,
|
|
1205
|
+
ticket: ctx.ticket,
|
|
1206
|
+
sujet: ctx.sujet,
|
|
1207
|
+
composant,
|
|
1208
|
+
reprise:
|
|
1209
|
+
tentative > 1 || ctx.reprendre || ctx.depuis || ctx.renvois
|
|
1210
|
+
? "oui : une séance d'avant a travaillé ce composant ; ta copie garde son travail, le rapport ci-dessous dit ce qui reste"
|
|
1211
|
+
: 'non',
|
|
1212
|
+
lots: lots.length
|
|
1213
|
+
? `${role === 'developpeur' ? 'le lot de tests' : 'tes tests'}, déjà dans la copie : ${lots.join(', ')}`
|
|
1214
|
+
: 'aucun',
|
|
1215
|
+
contexte: contexteDe(ctx, composant),
|
|
1216
|
+
description: description.trim() || 'aucune',
|
|
1217
|
+
// The section of the ticket's notes, absent without notes.
|
|
1218
|
+
notes: notes.trim()
|
|
1219
|
+
? `\nLES NOTES DU TICKET, l'état qu'un agent d'avant y a écrit :\n\n${notes.trim()}\n`
|
|
1220
|
+
: '',
|
|
1221
|
+
rapport: rapport ?? 'aucun',
|
|
1222
|
+
passation: passation?.text ?? 'aucune',
|
|
1223
|
+
};
|
|
1224
|
+
return await jusquALaFiche(ctx, role, composant, consigne(valeurs), { fort, neuve: agent });
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// The line CONTEXTE of a session on <composant>: the context of the relaunch --depuis that plays
|
|
1228
|
+
// it again (`renvois`, kept in the state), with the context of a run without --depuis, which every
|
|
1229
|
+
// agent receives; « aucun » without either.
|
|
1230
|
+
function contexteDe(ctx, composant) {
|
|
1231
|
+
const textes = [ctx.renvois?.[composant]?.contexte, !ctx.depuis && ctx.contexte];
|
|
1232
|
+
return [...new Set(textes.filter(Boolean))].join('\n\n') || 'aucun';
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// The launch <r> of a session of <role>, or the Arret it is: a motionless session (« bloqué »),
|
|
1236
|
+
// a launch that stops before its session (« lancement »).
|
|
1237
|
+
function seanceLancee(ctx, role, r) {
|
|
1238
|
+
if (r.bloque) {
|
|
1239
|
+
const minutes = Math.round(ctx.immobileMs / 60_000);
|
|
1240
|
+
throw new Arret(
|
|
1241
|
+
'bloqué',
|
|
1242
|
+
`La transcription de la séance du ${role} n'a pas bougé en ${minutes ? `${minutes} min` : `${ctx.immobileMs} ms`} : la séance est arrêtée. Les dernières lignes du journal :\n\n${fin(lire(ctx.journal), 40)}`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
if (r.seance === null) {
|
|
1246
|
+
throw new Arret(
|
|
1247
|
+
'lancement',
|
|
1248
|
+
`Le lanceur s'arrête avant la séance du ${role} (code ${r.code}).\n${r.erreur.trim()}\n\n${fin(lire(ctx.journal), 20)}`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// The session of <role> on <composant> with the instruction <texte>, its sheet read in its result
|
|
1254
|
+
// (scripts/resultats.mjs): the sheet, kept with the ticket's sheets, its discoveries filed. A
|
|
1255
|
+
// session that ends without a sheet that conforms (none, one out of its schema, a code other than
|
|
1256
|
+
// 0) is resumed once, the same session, with the short instruction that names what lacks (the
|
|
1257
|
+
// fifth template). The session opens under the id <id> (a new one by default), or resumes it with
|
|
1258
|
+
// <premiere> « reprend » (the regulator on the supervisor's answer); with <neuve> (the author of a
|
|
1259
|
+
// testwriter or a developpeur), the circuit keeps that id for that author (`ctx.reprise`). Throws an
|
|
1260
|
+
// Arret for a launch that stops before its session, a motionless session, or twice no sheet
|
|
1261
|
+
// that conforms (« sans-fiche »). <avec> joins its fields to the sheet's entry (the lot a review
|
|
1262
|
+
// read).
|
|
1263
|
+
async function jusquALaFiche(ctx, role, composant, texte, options = {}) {
|
|
1264
|
+
const { neuve, premiere = 'ouvre', id = randomUUID(), avec = {}, ...reste } = options;
|
|
1265
|
+
if (neuve) ctx.reprise = { agent: neuve, seance: id };
|
|
1266
|
+
const ouvrir = mode =>
|
|
1267
|
+
reste.horsEnveloppe
|
|
1268
|
+
? { session: [mode === 'ouvre' ? '--session-id' : '--resume', id] }
|
|
1269
|
+
: { [mode]: id };
|
|
1270
|
+
let lu = null;
|
|
1271
|
+
for (let essai = 1; essai <= 2; essai++) {
|
|
1272
|
+
const courte = () => consigne({ ticket: ctx.ticket, sujet: ctx.sujet, erreur: lu.erreur }, 4);
|
|
1273
|
+
const r = await lancer(ctx, role, composant, essai === 1 ? texte : courte(), {
|
|
1274
|
+
...reste,
|
|
1275
|
+
...ouvrir(essai === 1 ? premiere : 'reprend'),
|
|
1276
|
+
});
|
|
1277
|
+
seanceLancee(ctx, role, r);
|
|
1278
|
+
lu =
|
|
1279
|
+
r.seance === 0
|
|
1280
|
+
? lireFiche(role, r.sortie)
|
|
1281
|
+
: { erreur: `la séance finit en erreur (code ${r.seance})` };
|
|
1282
|
+
if (lu.fiche) return garder(ctx, role, composant, lu.fiche, avec);
|
|
1283
|
+
if (essai === 1) {
|
|
1284
|
+
commenter(
|
|
1285
|
+
ctx,
|
|
1286
|
+
composant,
|
|
1287
|
+
role,
|
|
1288
|
+
`séance finie sans fiche conforme : ${lu.erreur} ; reprise une fois.`
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
throw new Arret(
|
|
1293
|
+
'sans-fiche',
|
|
1294
|
+
`Deux fois, la séance du ${nomDuRole(role)} sur ${composant} finit sans fiche conforme : ${lu.erreur}. Le journal du ticket : ${ctx.journal}`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// The name of <composant> as a title writes it: its package's name, or its path.
|
|
1299
|
+
function nomDe(ctx, composant) {
|
|
1300
|
+
const liste = paquets(existsSync(ctx.copie) ? ctx.copie : ctx.racine);
|
|
1301
|
+
return liste.find(p => p.dossier === composant)?.pkg.name ?? composant;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// The sheet <fiche> of <role> on <composant>, kept with the ticket's sheets, <avec> joined to its
|
|
1305
|
+
// entry, each of its discoveries filed closed under the ticket (scripts/ticket.sh, label
|
|
1306
|
+
// `decouverte`), its component the session's, or the ticket's for the reviewer and the regulator;
|
|
1307
|
+
// returns it.
|
|
1308
|
+
function garder(ctx, role, composant, fiche, avec = {}) {
|
|
1309
|
+
inscrire(ctx, { role, composant, fiche, ...avec });
|
|
1310
|
+
const composants = DESTINATAIRES.includes(role)
|
|
1311
|
+
? nomDe(ctx, composant)
|
|
1312
|
+
: ctx.composants.map(c => nomDe(ctx, c)).join(', ');
|
|
1313
|
+
for (const d of fiche.decouvertes ?? []) {
|
|
1314
|
+
const r = spawnSync(
|
|
1315
|
+
'bash',
|
|
1316
|
+
[TICKET_SH, ctx.ticket, d.sujet, composants, d.corps, 'decouverte'],
|
|
1317
|
+
{
|
|
1318
|
+
cwd: ctx.racine,
|
|
1319
|
+
encoding: 'utf8',
|
|
1320
|
+
env: { ...process.env, [ACTEUR]: AUTEUR_DU_CIRCUIT },
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
if (r.status !== 0) {
|
|
1324
|
+
process.stderr.write(`⚠️ découverte « ${d.sujet} » non consignée : ${r.stderr}\n`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return fiche;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const lire = f => (existsSync(f) ? readFileSync(f, 'utf8') : '');
|
|
1331
|
+
|
|
1332
|
+
// The circuit's reports since the reviewer's last sheet: those of its proofs and checks (the final
|
|
1333
|
+
// pass among them), as it wrote them at the ticket.
|
|
1334
|
+
function rapportsDuCircuit(ctx) {
|
|
1335
|
+
const tous = lesFiches(ctx);
|
|
1336
|
+
const depuis = tous.findLastIndex(e => e.role === 'relecteur') + 1;
|
|
1337
|
+
return tous
|
|
1338
|
+
.slice(depuis)
|
|
1339
|
+
.filter(e => e.role === AUTEUR_DU_CIRCUIT && e.rapport)
|
|
1340
|
+
.map(e => e.rapport);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// The files of the lot, as the copy holds them against its HEAD: `{ tests, code }`, the
|
|
1344
|
+
// testwriter's files (`isTesterFile`: test files and the shared test material) and the others.
|
|
1345
|
+
function fichiersDuLot(ctx) {
|
|
1346
|
+
const tests = [];
|
|
1347
|
+
const code = [];
|
|
1348
|
+
for (const { fichier } of changements(ctx)) {
|
|
1349
|
+
(isTesterFile(fichier, ctx.copie) ? tests : code).push(fichier);
|
|
1350
|
+
}
|
|
1351
|
+
return { tests: tests.sort(), code: code.sort() };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// The blocking findings of a reviewer's sheet <fiche>, the ones that send the lot back.
|
|
1355
|
+
const bloquants = fiche => (fiche.constats ?? []).filter(c => c.genre === 'bloquant');
|
|
1356
|
+
|
|
1357
|
+
// The blocking findings of a reviewer's sheet <fiche>, each under its heading, the criterion or
|
|
1358
|
+
// rule it holds, its recipient (or the criterion in question) and its body: lines.
|
|
1359
|
+
const lignesDesConstats = fiche =>
|
|
1360
|
+
bloquants(fiche).flatMap((c, i) => [
|
|
1361
|
+
`### Constat ${i + 1} — ${c.titre}`,
|
|
1362
|
+
`Bloquant : ${c.regle}`,
|
|
1363
|
+
c.critere_en_cause
|
|
1364
|
+
? `Critère en cause : ${c.critere_en_cause}`
|
|
1365
|
+
: `Agent : ${c.role ?? '?'} ${c.composant ?? '?'}`,
|
|
1366
|
+
c.corps,
|
|
1367
|
+
'',
|
|
1368
|
+
]);
|
|
1369
|
+
|
|
1370
|
+
// The verdict the reviewer's sheet <fiche> carries, as the circuit writes it at the ticket for the
|
|
1371
|
+
// human, its first word ACCEPTÉ or RENDU: after a bounded review, each finding before it lifted or
|
|
1372
|
+
// not, with its proof; each blocking finding under its heading, what it holds, its recipient (or
|
|
1373
|
+
// the criterion in question) and its body; then the remarks (its findings of `genre` remarque),
|
|
1374
|
+
// the question for the owner and the deviations to name at the commit.
|
|
1375
|
+
export function texteDuVerdict(fiche) {
|
|
1376
|
+
const section = (titre, l) => (l?.length ? [`## ${titre}`, '', ...l.map(x => `- ${x}`), ''] : []);
|
|
1377
|
+
const suivi = (fiche.suivi ?? []).map(
|
|
1378
|
+
x => `${x.titre} : ${x.etat}${x.preuve ? ` — ${x.preuve}` : ''}`
|
|
1379
|
+
);
|
|
1380
|
+
return [
|
|
1381
|
+
fiche.verdict,
|
|
1382
|
+
'',
|
|
1383
|
+
...section("Constats d'avant", suivi),
|
|
1384
|
+
...lignesDesConstats(fiche),
|
|
1385
|
+
...section(
|
|
1386
|
+
'Remarques',
|
|
1387
|
+
(fiche.constats ?? []).filter(c => c.genre === 'remarque').map(c => `${c.titre} — ${c.corps}`)
|
|
1388
|
+
),
|
|
1389
|
+
...section('Question au responsable', fiche.question ? [fiche.question] : []),
|
|
1390
|
+
...section(
|
|
1391
|
+
'Écarts à nommer au commit',
|
|
1392
|
+
fiche.ecarts?.map(e => `${e.sujet} : ${e.texte}`)
|
|
1393
|
+
),
|
|
1394
|
+
]
|
|
1395
|
+
.join('\n')
|
|
1396
|
+
.trim();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// The review of the lot, the reviewer's session out of the envelope, from the second template:
|
|
1400
|
+
// `{ verdict, fiche, texte, arbre }`, its sheet, the verdict the circuit writes at the ticket from
|
|
1401
|
+
// it, under the reviewer's author, and the tree of the lot it read. The lot it reads is kept as a
|
|
1402
|
+
// patch set (`garderLeLot`), on the lot the review before read, joined to its sheet's entry (`lot:
|
|
1403
|
+
// { commit, arbre, fichiers }`); after a review sent back, the review reads the difference
|
|
1404
|
+
// between the two and the findings before it (the bounded review), as Gerrit has the next review
|
|
1405
|
+
// read the difference between two patch sets: the files of the lot that changed since, the
|
|
1406
|
+
// command of the difference, the findings with their recipients.
|
|
1407
|
+
async function relire(ctx, composant) {
|
|
1408
|
+
const { tests, code } = fichiersDuLot(ctx);
|
|
1409
|
+
const rapports = rapportsDuCircuit(ctx);
|
|
1410
|
+
const precedents = fichesDe(ctx, 'relecteur').length;
|
|
1411
|
+
const avant = relectureDAvant(ctx);
|
|
1412
|
+
const lot = garderLeLot(ctx, avant?.lot.commit);
|
|
1413
|
+
let lus = { tests, code };
|
|
1414
|
+
let precedente = 'aucune : tu relis le lot entier';
|
|
1415
|
+
if (avant && lot) {
|
|
1416
|
+
const dansLeLot = new Set([...tests, ...code, ...(avant.lot.fichiers ?? [])]);
|
|
1417
|
+
const changes = differenceDesLots(ctx, avant.lot.commit, lot.commit).filter(f =>
|
|
1418
|
+
dansLeLot.has(f)
|
|
1419
|
+
);
|
|
1420
|
+
lus = {
|
|
1421
|
+
tests: changes.filter(f => isTesterFile(f, ctx.copie)),
|
|
1422
|
+
code: changes.filter(f => !isTesterFile(f, ctx.copie)),
|
|
1423
|
+
};
|
|
1424
|
+
precedente = [
|
|
1425
|
+
`la relecture ${precedents}, ${avant.fiche.verdict} : tu relis la différence du lot depuis elle et ses constats, selon ta compétence (« La relecture bornée »).`,
|
|
1426
|
+
'',
|
|
1427
|
+
`- La différence : \`git diff ${avant.lot.commit} ${lot.commit}\`, dans la copie du ticket ; ses fichiers : ${changes.join(', ') || 'aucun'}.`,
|
|
1428
|
+
'- Ses constats bloquants, chacun avec son destinataire :',
|
|
1429
|
+
'',
|
|
1430
|
+
...lignesDesConstats(avant.fiche),
|
|
1431
|
+
]
|
|
1432
|
+
.join('\n')
|
|
1433
|
+
.trim();
|
|
1434
|
+
}
|
|
1435
|
+
const affirmations = path.join(ctx.tmp, 'affirmations.md');
|
|
1436
|
+
writeFileSync(affirmations, leTicket(ctx).description);
|
|
1437
|
+
const valeurs = {
|
|
1438
|
+
depot: ctx.racine,
|
|
1439
|
+
ticket: ctx.ticket,
|
|
1440
|
+
sujet: ctx.sujet,
|
|
1441
|
+
composants: ctx.composants.join(', '),
|
|
1442
|
+
reprise: precedents
|
|
1443
|
+
? `oui : ${precedents} relecture(s) d'avant ont rendu le lot, et le lot a repassé le circuit depuis`
|
|
1444
|
+
: 'non',
|
|
1445
|
+
lots: [
|
|
1446
|
+
...(avant && lot ? ["ce que le lot change depuis la relecture d'avant"] : []),
|
|
1447
|
+
`le lot de tests : ${lus.tests.join(', ') || 'aucun fichier'}`,
|
|
1448
|
+
`le lot de code : ${lus.code.join(', ') || 'aucun fichier'}`,
|
|
1449
|
+
].join(' ; '),
|
|
1450
|
+
precedente,
|
|
1451
|
+
affirmations,
|
|
1452
|
+
rapports: rapports.join('\n\n') || 'aucun',
|
|
1453
|
+
// The regulator's context of a relaunch, the answer to a question of the reviewer among them.
|
|
1454
|
+
contexte: (!ctx.depuis && ctx.contexte) || 'aucun',
|
|
1455
|
+
};
|
|
1456
|
+
const fiche = await jusquALaFiche(ctx, 'relecteur', composant, consigne(valeurs, 1), {
|
|
1457
|
+
horsEnveloppe: true,
|
|
1458
|
+
droits: horsDeLEnveloppe(ctx, 'relecteur', 'opus'),
|
|
1459
|
+
avec: lot ? { lot: { ...lot, fichiers: [...tests, ...code] } } : {},
|
|
1460
|
+
});
|
|
1461
|
+
const texte = texteDuVerdict(fiche);
|
|
1462
|
+
const r = ctx.bdSous(AUTEUR_DU_VERDICT, 'comments', 'add', ctx.ticket, texte);
|
|
1463
|
+
if (r.status !== 0) process.stderr.write(`⚠️ verdict non écrit au ticket : ${r.stderr}\n`);
|
|
1464
|
+
return { verdict: fiche.verdict, fiche, texte, arbre: lot?.arbre ?? null };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// The last review of the ticket and the lot it read: its sheet's entry (`{ fiche, lot: { commit,
|
|
1468
|
+
// arbre, fichiers } }`), or null before any, without a copy, or when the repository no longer
|
|
1469
|
+
// holds that lot.
|
|
1470
|
+
function relectureDAvant(ctx) {
|
|
1471
|
+
const e = lesFiches(ctx).findLast(x => x.role === 'relecteur');
|
|
1472
|
+
if (!e?.lot?.commit || !existsSync(ctx.copie)) return null;
|
|
1473
|
+
const r = spawnSync('git', ['-C', ctx.copie, 'cat-file', '-e', `${e.lot.commit}^{commit}`]);
|
|
1474
|
+
return r.status === 0 ? e : null;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// The tree of the copy as it stands, the copy's HEAD and index untouched: its index copied to a
|
|
1478
|
+
// file of the circuit's, every file of the copy added to that one (`git add -A`, the ignored files
|
|
1479
|
+
// left out), written as a tree. Its id, or null when git fails.
|
|
1480
|
+
function arbreDeLaCopie(ctx) {
|
|
1481
|
+
const index = path.join(ctx.tmp, `index-${++ctx.numero}`);
|
|
1482
|
+
const reel = spawnSync(
|
|
1483
|
+
'git',
|
|
1484
|
+
['-C', ctx.copie, 'rev-parse', '--path-format=absolute', '--git-path', 'index'],
|
|
1485
|
+
{ encoding: 'utf8' }
|
|
1486
|
+
).stdout?.trim();
|
|
1487
|
+
const env = { ...process.env, GIT_INDEX_FILE: index };
|
|
1488
|
+
const git = (...a) => spawnSync('git', ['-C', ctx.copie, ...a], { encoding: 'utf8', env });
|
|
1489
|
+
try {
|
|
1490
|
+
if (reel && existsSync(reel)) copyFileSync(reel, index);
|
|
1491
|
+
else if (git('read-tree', 'HEAD').status !== 0) return null;
|
|
1492
|
+
if (git('add', '-A').status !== 0) return null;
|
|
1493
|
+
const r = git('write-tree');
|
|
1494
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
1495
|
+
} finally {
|
|
1496
|
+
rmSync(index, { force: true });
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// The lot of the copy kept as a commit on <parent> (the lot kept before, or HEAD), as Gerrit keeps
|
|
1501
|
+
// each patch set of a change: `{ commit, arbre }`, or null when git fails. Turborepo compares two
|
|
1502
|
+
// of them (`--filter=...[<avant>...<après>]`, the merge base of the range being the first), and
|
|
1503
|
+
// the reviewer reads their difference.
|
|
1504
|
+
function garderLeLot(ctx, parent) {
|
|
1505
|
+
const arbre = arbreDeLaCopie(ctx);
|
|
1506
|
+
if (!arbre) return null;
|
|
1507
|
+
const qui = { name: AUTEUR_DU_CIRCUIT, email: 'circuit@grillhouse.invalid' };
|
|
1508
|
+
const r = spawnSync(
|
|
1509
|
+
'git',
|
|
1510
|
+
['-C', ctx.copie, 'commit-tree', arbre, '-p', parent ?? 'HEAD', '-m', `lot de ${ctx.ticket}`],
|
|
1511
|
+
{
|
|
1512
|
+
encoding: 'utf8',
|
|
1513
|
+
env: {
|
|
1514
|
+
...process.env,
|
|
1515
|
+
GIT_AUTHOR_NAME: qui.name,
|
|
1516
|
+
GIT_AUTHOR_EMAIL: qui.email,
|
|
1517
|
+
GIT_COMMITTER_NAME: qui.name,
|
|
1518
|
+
GIT_COMMITTER_EMAIL: qui.email,
|
|
1519
|
+
},
|
|
1520
|
+
}
|
|
1521
|
+
);
|
|
1522
|
+
return r.status === 0 ? { commit: r.stdout.trim(), arbre } : null;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// The files that differ between the lots kept <avant> and <apres>, relative to the copy, sorted.
|
|
1526
|
+
function differenceDesLots(ctx, avant, apres) {
|
|
1527
|
+
const r = spawnSync('git', ['-C', ctx.copie, 'diff', '--name-only', '-z', avant, apres], {
|
|
1528
|
+
encoding: 'utf8',
|
|
1529
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1530
|
+
});
|
|
1531
|
+
return r.status === 0 ? r.stdout.split('\0').filter(Boolean).sort() : [];
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// The final pass the lot calls for: `{ arbre, restreinte }`, the tree of the copy, and, after a
|
|
1535
|
+
// review whose lot it no longer is the accepted one, the range Turborepo plays (`{ avant, apres }`:
|
|
1536
|
+
// the lot that review read and the copy kept on it), null for the whole pass: before any review,
|
|
1537
|
+
// or on the lot the last review accepted.
|
|
1538
|
+
function laPasseFinale(ctx) {
|
|
1539
|
+
const avant = relectureDAvant(ctx);
|
|
1540
|
+
const arbre = arbreDeLaCopie(ctx);
|
|
1541
|
+
if (!avant || !arbre) return { arbre, restreinte: null };
|
|
1542
|
+
if (avant.fiche.verdict === 'ACCEPTÉ' && avant.lot.arbre === arbre) {
|
|
1543
|
+
return { arbre, restreinte: null };
|
|
1544
|
+
}
|
|
1545
|
+
const apres = garderLeLot(ctx, avant.lot.commit);
|
|
1546
|
+
return {
|
|
1547
|
+
arbre,
|
|
1548
|
+
restreinte: apres ? { avant: avant.lot.commit, apres: apres.commit } : null,
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// The references the project declares in the package.json of the copy (`grillhouse.references`):
|
|
1553
|
+
// each `{ nom, commande, fichiers, tests, gardes }` with a name, a command and files, `tests` and
|
|
1554
|
+
// `gardes` arrays when given, empty when not; none without them.
|
|
1555
|
+
function lesReferences(ctx) {
|
|
1556
|
+
const l = lireJson(path.join(ctx.copie, 'package.json'))?.grillhouse?.references;
|
|
1557
|
+
return (Array.isArray(l) ? l : [])
|
|
1558
|
+
.filter(
|
|
1559
|
+
r =>
|
|
1560
|
+
typeof r?.nom === 'string' &&
|
|
1561
|
+
typeof r.commande === 'string' &&
|
|
1562
|
+
Array.isArray(r.fichiers) &&
|
|
1563
|
+
r.fichiers.length > 0 &&
|
|
1564
|
+
(r.tests === undefined || Array.isArray(r.tests)) &&
|
|
1565
|
+
(r.gardes === undefined || Array.isArray(r.gardes))
|
|
1566
|
+
)
|
|
1567
|
+
.map(r => ({ ...r, tests: r.tests ?? [], gardes: r.gardes ?? [] }));
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Whether one of <motifs> covers the file <fichier>, both from the root of the copy, as the
|
|
1571
|
+
// inputs of Turborepo read them: a path names that file, a directory every file under it, a glob
|
|
1572
|
+
// pattern (`*`, `**`) the files it matches (`path.matchesGlob` of Node).
|
|
1573
|
+
export function couvertPar(motifs, fichier) {
|
|
1574
|
+
return motifs.some(m => {
|
|
1575
|
+
const dossier = m.replace(/\/+$/, '');
|
|
1576
|
+
return fichier === dossier || fichier.startsWith(`${dossier}/`) || path.matchesGlob(fichier, m);
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// The references the project declares, when every red of a final pass, by its place of <lieux>
|
|
1581
|
+
// (`{ fichier, gardes }`, the names its finding carries), is one of theirs: a finding of one of
|
|
1582
|
+
// their `gardes`, a file of their `tests`, or a file their `fichiers` cover; none otherwise.
|
|
1583
|
+
function referencesDesRouges(ctx, lieux) {
|
|
1584
|
+
const refs = lesReferences(ctx);
|
|
1585
|
+
const leur = ({ fichier: f, gardes }) =>
|
|
1586
|
+
refs.some(
|
|
1587
|
+
r =>
|
|
1588
|
+
gardes.some(g => r.gardes.includes(g)) ||
|
|
1589
|
+
(f && (r.tests.includes(f) || couvertPar(r.fichiers, f)))
|
|
1590
|
+
);
|
|
1591
|
+
return refs.length && lieux.length && lieux.every(leur) ? refs : [];
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// The regeneration of the references the project declares (3.1): the copy kept as a lot on the
|
|
1595
|
+
// one the last review read (or HEAD), each command run on the whole copy, the copy kept again on
|
|
1596
|
+
// that lot. `{ texte, regenerees, fichiers, restreinte }`: the report, the names of the
|
|
1597
|
+
// references changed, the files changed, and the range of the final pass that follows. A
|
|
1598
|
+
// reference changed that the ticket's description does not name (its name or one of its files),
|
|
1599
|
+
// a file changed that no reference declares, a command at another code than 0 or a regeneration
|
|
1600
|
+
// that changes nothing restores the copy as it was and stops at « references ».
|
|
1601
|
+
function regenerer(ctx, composant) {
|
|
1602
|
+
const refs = lesReferences(ctx);
|
|
1603
|
+
const avant = garderLeLot(ctx, relectureDAvant(ctx)?.lot.commit);
|
|
1604
|
+
if (!avant) {
|
|
1605
|
+
throw new Arret(
|
|
1606
|
+
'references',
|
|
1607
|
+
'Le circuit ne garde pas la copie avant la régénération des références (git échoue) : la décision revient au régulateur.'
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const echecs = refs
|
|
1611
|
+
.map(r => ({ r, code: commandeDeReference(ctx, composant, r.commande) }))
|
|
1612
|
+
.filter(x => x.code !== 0)
|
|
1613
|
+
.map(x => `- ${x.r.nom} : \`${x.r.commande}\` rend le code ${x.code}.`);
|
|
1614
|
+
const apres = garderLeLot(ctx, avant.commit);
|
|
1615
|
+
const fichiers = apres ? differenceDesLots(ctx, avant.commit, apres.commit) : [];
|
|
1616
|
+
const description = leTicket(ctx).description;
|
|
1617
|
+
const siens = r => fichiers.filter(f => couvertPar(r.fichiers, f));
|
|
1618
|
+
const regenerees = refs.filter(r => siens(r).length);
|
|
1619
|
+
const nommee = r => [r.nom, ...r.fichiers].some(x => description.includes(x));
|
|
1620
|
+
const horsDeclaration = fichiers.filter(f => !refs.some(r => couvertPar(r.fichiers, f)));
|
|
1621
|
+
const ecarts = [
|
|
1622
|
+
...echecs,
|
|
1623
|
+
...(!apres ? ['- Le circuit ne garde pas la copie après la régénération (git échoue).'] : []),
|
|
1624
|
+
...(apres && !fichiers.length
|
|
1625
|
+
? [
|
|
1626
|
+
`- La régénération (${refs.map(r => `\`${r.commande}\``).join(', ')}) ne change aucun fichier : les rouges de la passe finale restent.`,
|
|
1627
|
+
]
|
|
1628
|
+
: []),
|
|
1629
|
+
...regenerees
|
|
1630
|
+
.filter(r => !nommee(r))
|
|
1631
|
+
.map(
|
|
1632
|
+
r =>
|
|
1633
|
+
`- ${r.nom} change (${siens(r).join(', ')}), et le plan du ticket ne nomme pas ce changement (ni « ${r.nom} » ni ses fichiers).`
|
|
1634
|
+
),
|
|
1635
|
+
...horsDeclaration.map(f => `- ${f} change, et aucune référence déclarée ne l'écrit.`),
|
|
1636
|
+
];
|
|
1637
|
+
if (ecarts.length) {
|
|
1638
|
+
restaurer(ctx, avant.commit, fichiers);
|
|
1639
|
+
throw new Arret(
|
|
1640
|
+
'references',
|
|
1641
|
+
[
|
|
1642
|
+
`La régénération des références, sur la copie entière, s'arrête ; la copie est rendue telle qu'avant elle. La décision revient au régulateur : faire nommer le changement au plan du ticket par le superviseur (\`MONTE\`), puis \`RELANCE --reprendre\` ; ou faire corriger le lot qui le cause (\`RELANCE --depuis developpeur <composant>\`).`,
|
|
1643
|
+
'',
|
|
1644
|
+
...ecarts,
|
|
1645
|
+
'',
|
|
1646
|
+
...(apres && fichiers.length
|
|
1647
|
+
? [
|
|
1648
|
+
`La différence, avant d'être rendue : \`git diff ${avant.commit} ${apres.commit}\`, dans la copie du ticket.`,
|
|
1649
|
+
]
|
|
1650
|
+
: []),
|
|
1651
|
+
`Le journal du ticket : ${ctx.journal}`,
|
|
1652
|
+
].join('\n')
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
return {
|
|
1656
|
+
texte: [
|
|
1657
|
+
`Les références régénérées sur la copie entière : ${regenerees.map(r => r.nom).join(', ')} ; le lot porte leurs fichiers, puis la passe finale restreinte à ce qu'elles changent.`,
|
|
1658
|
+
'',
|
|
1659
|
+
...regenerees.map(r => `- ${r.nom} (\`${r.commande}\`) : ${siens(r).join(', ')}`),
|
|
1660
|
+
'',
|
|
1661
|
+
`La différence : \`git diff ${avant.commit} ${apres.commit}\`, dans la copie du ticket.`,
|
|
1662
|
+
].join('\n'),
|
|
1663
|
+
regenerees: regenerees.map(r => r.nom),
|
|
1664
|
+
fichiers,
|
|
1665
|
+
restreinte: { avant: avant.commit, apres: apres.commit },
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// One command of a reference, run by the shell at the root of the copy, out of the envelope,
|
|
1670
|
+
// under the lock of the machine, its output added to the log: its exit code.
|
|
1671
|
+
function commandeDeReference(ctx, composant, commande) {
|
|
1672
|
+
entete(ctx, 'references', composant);
|
|
1673
|
+
appendFileSync(ctx.journal, `$ ${commande}\n`);
|
|
1674
|
+
const fd = openSync(ctx.journal, 'a');
|
|
1675
|
+
try {
|
|
1676
|
+
const r = avecLaMachine(
|
|
1677
|
+
env => spawnSync('sh', ['-c', commande], { cwd: ctx.copie, stdio: ['ignore', fd, fd], env }),
|
|
1678
|
+
{ dire: l => appendFileSync(ctx.journal, `${l}\n`) }
|
|
1679
|
+
);
|
|
1680
|
+
return r.status ?? 1;
|
|
1681
|
+
} catch (e) {
|
|
1682
|
+
if (!(e instanceof AttenteDeLaMachine)) throw e;
|
|
1683
|
+
appendFileSync(ctx.journal, `${e.message}\n`);
|
|
1684
|
+
return e.code;
|
|
1685
|
+
} finally {
|
|
1686
|
+
closeSync(fd);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// The files <fichiers> of the copy as the lot kept <commit> holds them, a file it does not hold
|
|
1691
|
+
// removed; the copy's index untouched.
|
|
1692
|
+
function restaurer(ctx, commit, fichiers) {
|
|
1693
|
+
for (const f of fichiers) {
|
|
1694
|
+
const tient = spawnSync('git', ['-C', ctx.copie, 'cat-file', '-e', `${commit}:${f}`]);
|
|
1695
|
+
if (tient.status === 0) {
|
|
1696
|
+
spawnSync('git', ['-C', ctx.copie, 'restore', `--source=${commit}`, '--worktree', '--', f]);
|
|
1697
|
+
} else {
|
|
1698
|
+
rmSync(path.join(ctx.copie, f), { force: true });
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// The stop for the sheet <fiche> of the session <tentative> of <role> on <composant>, BLOQUÉ
|
|
1704
|
+
// (« hors-portee ») or BESOIN (« besoin »): its reason or question first, then its handover.
|
|
1705
|
+
function arretDuStatut(ctx, role, composant, tentative, fiche) {
|
|
1706
|
+
const passation = derniereDe(ctx, auteur(role, composant))?.text ?? 'aucune';
|
|
1707
|
+
const qui = `Le ${nomDuRole(role)} de ${composant}`;
|
|
1708
|
+
return fiche.statut === 'BESOIN'
|
|
1709
|
+
? new Arret(
|
|
1710
|
+
'besoin',
|
|
1711
|
+
`${qui} attend une réponse (séance ${tentative}) : ${fiche.question}\n\nLa décision revient au régulateur. Sa passation :\n\n${passation}`
|
|
1712
|
+
)
|
|
1713
|
+
: new Arret(
|
|
1714
|
+
'hors-portee',
|
|
1715
|
+
`${qui} se déclare bloqué (séance ${tentative}) : ${fiche.raison}\n\nLe circuit ne le relance pas sur ce rouge : la décision revient au régulateur. Sa passation :\n\n${passation}`
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// The index in the title of the component <nom> (a path or a package name), or -1.
|
|
1720
|
+
function indexDe(ctx, nom) {
|
|
1721
|
+
try {
|
|
1722
|
+
return ctx.composants.indexOf(ctx.resoudre(nom));
|
|
1723
|
+
} catch {
|
|
1724
|
+
return -1;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// The sending to a testwriter in the sheet <fiche> of the developpeur of the component at <index>:
|
|
1729
|
+
// `{ index, texte }`, the component its `renvoi` names, one of the title up to <index>, and what
|
|
1730
|
+
// that testwriter receives (the sending, then the developpeur's handover); null without a sending,
|
|
1731
|
+
// or for a component out of the title or after his own.
|
|
1732
|
+
function renvoiAuTestwriter(ctx, fiche, index) {
|
|
1733
|
+
if (!fiche.renvoi) return null;
|
|
1734
|
+
const i = indexDe(ctx, fiche.renvoi.composant);
|
|
1735
|
+
if (i < 0 || i > index) return null;
|
|
1736
|
+
const composant = ctx.composants[index];
|
|
1737
|
+
const passation = derniereDe(ctx, auteur('developpeur', composant))?.text ?? 'aucune';
|
|
1738
|
+
const raison = fiche.raison ? ` : ${fiche.raison}` : '';
|
|
1739
|
+
return {
|
|
1740
|
+
index: i,
|
|
1741
|
+
texte: `Le développeur de ${composant} renvoie le rouge qui reste aux tests de ${ctx.composants[i]}${raison}.\n\nSa passation :\n\n${passation}`,
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// The agents the reviewer's sheet RENDU <fiche> sends the lot back to, by its blocking findings
|
|
1746
|
+
// alone (a remark reaches the ticket, no agent; the sheet's schema gives a RENDU at least one):
|
|
1747
|
+
// Map index of component → `{ etape, testwriter, developpeur }`, `etape` the first step that
|
|
1748
|
+
// component plays again (testwriter when a finding names its testwriter), `testwriter` and
|
|
1749
|
+
// `developpeur` the findings for each, or null. Throws an Arret `relecture-arret` for a finding
|
|
1750
|
+
// without a valid recipient (a component the title does not name) or one that puts a criterion in
|
|
1751
|
+
// question.
|
|
1752
|
+
function renvoisDuVerdict(ctx, fiche, texte) {
|
|
1753
|
+
const liste = bloquants(fiche);
|
|
1754
|
+
const arret = raison =>
|
|
1755
|
+
new Arret(
|
|
1756
|
+
'relecture-arret',
|
|
1757
|
+
`${raison} La décision revient au régulateur.\n\nLe verdict :\n\n${texte}`
|
|
1758
|
+
);
|
|
1759
|
+
const renvois = new Map();
|
|
1760
|
+
for (const [n, c] of liste.entries()) {
|
|
1761
|
+
if (c.critere_en_cause)
|
|
1762
|
+
throw arret(`Le constat « ${c.titre} » met en cause un critère du ticket.`);
|
|
1763
|
+
const index = c.composant ? indexDe(ctx, c.composant) : -1;
|
|
1764
|
+
if (!DESTINATAIRES.includes(c.role) || index < 0) {
|
|
1765
|
+
throw arret(
|
|
1766
|
+
`Le constat « ${c.titre} » n'a pas de destinataire valide : il nomme ${c.role ? `« ${c.role} ${c.composant ?? ''} »` : 'personne'}, quand le rôle est testwriter ou developpeur et le composant l'un du titre (${ctx.composants.join(', ')}).`
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
const r = renvois.get(index) ?? { etape: 'developpeur', testwriter: [], developpeur: [] };
|
|
1770
|
+
r[c.role].push(`### Constat ${n + 1} — ${c.titre}\n${c.corps}`);
|
|
1771
|
+
if (c.role === 'testwriter') r.etape = 'testwriter';
|
|
1772
|
+
renvois.set(index, r);
|
|
1773
|
+
}
|
|
1774
|
+
const joindre = l => (l.length ? l.join('\n\n') : null);
|
|
1775
|
+
return new Map(
|
|
1776
|
+
[...renvois].map(([i, r]) => [
|
|
1777
|
+
i,
|
|
1778
|
+
{ etape: r.etape, testwriter: joindre(r.testwriter), developpeur: joindre(r.developpeur) },
|
|
1779
|
+
])
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// The integration of the lot: its two patches and its message, written from the copy, then
|
|
1784
|
+
// scripts/integration/integrer.mjs from the main tree, its output added to the log. A run « à
|
|
1785
|
+
// repasser » (75) plays once more. `{ code, sortie }`.
|
|
1786
|
+
function integrer(ctx) {
|
|
1787
|
+
const { tests, code } = fichiersDuLot(ctx);
|
|
1788
|
+
const dansCopie = (...a) =>
|
|
1789
|
+
spawnSync('git', ['-C', ctx.copie, ...a], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
1790
|
+
// Each file against HEAD, a new one against nothing: the index of the copy is not written.
|
|
1791
|
+
const patch = fichiers =>
|
|
1792
|
+
fichiers
|
|
1793
|
+
.map(f =>
|
|
1794
|
+
dansCopie('cat-file', '-e', `HEAD:${f}`).status === 0
|
|
1795
|
+
? dansCopie('diff', '--binary', 'HEAD', '--', f).stdout
|
|
1796
|
+
: dansCopie('diff', '--no-index', '--binary', '/dev/null', f).stdout
|
|
1797
|
+
)
|
|
1798
|
+
.join('');
|
|
1799
|
+
const args = ['--ticket', ctx.ticket];
|
|
1800
|
+
for (const [option, fichiers] of [
|
|
1801
|
+
['--tests', tests],
|
|
1802
|
+
['--code', code],
|
|
1803
|
+
]) {
|
|
1804
|
+
if (!fichiers.length) continue;
|
|
1805
|
+
const f = path.join(ctx.tmp, `${option.slice(2)}.patch`);
|
|
1806
|
+
writeFileSync(f, patch(fichiers));
|
|
1807
|
+
args.push(option, f);
|
|
1808
|
+
}
|
|
1809
|
+
const message = path.join(ctx.tmp, 'message');
|
|
1810
|
+
writeFileSync(message, messageDeCommit(ctx));
|
|
1811
|
+
args.push('--message', message);
|
|
1812
|
+
let r;
|
|
1813
|
+
for (let essai = 1; essai <= 2; essai++) {
|
|
1814
|
+
entete(ctx, 'integration', ctx.composants.join(', '));
|
|
1815
|
+
appendFileSync(ctx.journal, `$ node ${INTEGRER} ${args.join(' ')}\n`);
|
|
1816
|
+
const x = avecAvancement(ctx, 'integration', avancement =>
|
|
1817
|
+
spawnSync(process.execPath, [INTEGRER, ...args], {
|
|
1818
|
+
cwd: ctx.racine,
|
|
1819
|
+
encoding: 'utf8',
|
|
1820
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
1821
|
+
env: { ...process.env, [AVANCEMENT]: avancement },
|
|
1822
|
+
})
|
|
1823
|
+
);
|
|
1824
|
+
r = { code: x.status ?? 1, sortie: `${x.stdout ?? ''}${x.stderr ?? ''}` };
|
|
1825
|
+
appendFileSync(ctx.journal, r.sortie);
|
|
1826
|
+
if (r.code !== A_REPASSER) break;
|
|
1827
|
+
}
|
|
1828
|
+
return r;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// The queue (scripts/integration/file.mjs), launched from the main tree and waited for, its output
|
|
1832
|
+
// added to the log; then the state it leaves to the ticket: `{ etat, raison }`, `etat` ferme (the
|
|
1833
|
+
// ticket closed, its branch on main and its CI read), en-file (the branch still in the queue, the
|
|
1834
|
+
// queue's reason) or rouvert (out of the queue, or a red CI, the queue's last comment). A queue
|
|
1835
|
+
// already running exits 0 at once: it takes the branch, and the state is read at its end too.
|
|
1836
|
+
function file(ctx) {
|
|
1837
|
+
const motsDeLaFile = () => passations(ctx).filter(c => c.author === AUTEUR_DE_LA_FILE);
|
|
1838
|
+
const avant = motsDeLaFile().length;
|
|
1839
|
+
entete(ctx, 'file', ctx.composants.join(', '));
|
|
1840
|
+
appendFileSync(ctx.journal, `$ node ${FILE}\n`);
|
|
1841
|
+
const x = avecAvancement(ctx, null, avancement =>
|
|
1842
|
+
spawnSync(process.execPath, [FILE], {
|
|
1843
|
+
cwd: ctx.racine,
|
|
1844
|
+
encoding: 'utf8',
|
|
1845
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
1846
|
+
env: { ...process.env, [AVANCEMENT]: avancement },
|
|
1847
|
+
})
|
|
1848
|
+
);
|
|
1849
|
+
appendFileSync(ctx.journal, `${x.stdout ?? ''}${x.stderr ?? ''}`);
|
|
1850
|
+
const branche = `agent/${ctx.ticket}`;
|
|
1851
|
+
// Another queue that runs takes the branch: the circuit waits for its end.
|
|
1852
|
+
while (lireEntrees(ctx.racine).some(e => e.branche === branche) && fileEnCours(ctx)) {
|
|
1853
|
+
attendreUnPeu(5_000);
|
|
1854
|
+
}
|
|
1855
|
+
const show = ctx.bd('show', ctx.ticket, '--json');
|
|
1856
|
+
const t = show.status === 0 ? (JSON.parse(show.stdout)[0] ?? {}) : {};
|
|
1857
|
+
// The queue's last word at the ticket, under its author, since this queue began.
|
|
1858
|
+
const mot = motsDeLaFile().slice(avant).at(-1)?.text ?? null;
|
|
1859
|
+
const raison = mot ?? 'aucun commentaire de la file';
|
|
1860
|
+
if (lireEntrees(ctx.racine).some(e => e.branche === branche)) return { etat: 'en-file', raison };
|
|
1861
|
+
if (t.status === 'closed') {
|
|
1862
|
+
// A CI the queue could not read is its last word at a ticket it closes.
|
|
1863
|
+
const motif = t.close_reason || 'ticket fermé';
|
|
1864
|
+
return { etat: 'ferme', raison: mot ? `${motif} ; ${enUneLigne(mot)}` : motif };
|
|
1865
|
+
}
|
|
1866
|
+
return { etat: 'rouvert', raison };
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// True while a queue holds its lock (`.git/file-integration.lock`), its process alive.
|
|
1870
|
+
function fileEnCours(ctx) {
|
|
1871
|
+
const verrou = path.join(dossierCommun(ctx.racine), 'file-integration.lock');
|
|
1872
|
+
const pid = Number.parseInt(lire(verrou), 10);
|
|
1873
|
+
return Number.isInteger(pid) && vivant(pid);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// The type of a conventional commit for the ticket's type in Beads; `feat` for any other.
|
|
1877
|
+
const TYPES_DE_COMMIT = { bug: 'fix', chore: 'chore', docs: 'docs' };
|
|
1878
|
+
|
|
1879
|
+
// The commit message of the lot: `<type>(<composants>): <sujet> (<ticket>)`, the components by
|
|
1880
|
+
// their package name; then the message the sheets of the developpeurs and of the reviewer give,
|
|
1881
|
+
// in their order, written once from the final state (`messageDuLot`).
|
|
1882
|
+
function messageDeCommit(ctx) {
|
|
1883
|
+
const liste = paquets(existsSync(ctx.copie) ? ctx.copie : ctx.racine);
|
|
1884
|
+
const noms = ctx.composants
|
|
1885
|
+
.filter(c => c !== '.')
|
|
1886
|
+
.map(c => liste.find(p => p.dossier === c)?.pkg.name ?? path.basename(c));
|
|
1887
|
+
const type = TYPES_DE_COMMIT[ctx.type] ?? 'feat';
|
|
1888
|
+
const portee = noms.length ? `(${noms.join(',')})` : '';
|
|
1889
|
+
return messageDuLot({
|
|
1890
|
+
entete: `${type}${portee}: ${ctx.sujet} (${ctx.ticket})`,
|
|
1891
|
+
description: leTicket(ctx).description,
|
|
1892
|
+
fiches: lesFiches(ctx)
|
|
1893
|
+
.filter(e => e.role === 'developpeur' || e.role === 'relecteur')
|
|
1894
|
+
.map(e => e.fiche),
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// The commit message of a lot, written once from its final state, as a change merged on Gerrit
|
|
1899
|
+
// carries the one description of its last patch set: <entete>, the title of the ticket; the
|
|
1900
|
+
// section of the criteria of the plan <description> (its heading begins with « Critères »), what
|
|
1901
|
+
// the lot changes; a paragraph per deviation the sheets <fiches> carry (`ecarts`), each subject
|
|
1902
|
+
// once, in the order it first appears, with the value of the last sheet that carries it (a
|
|
1903
|
+
// subject known again whatever its case and spacing); then the trailer.
|
|
1904
|
+
export function messageDuLot({ entete, description, fiches }) {
|
|
1905
|
+
const criteres = [];
|
|
1906
|
+
let dedans = false;
|
|
1907
|
+
for (const ligne of String(description ?? '').split('\n')) {
|
|
1908
|
+
const m = /^##\s+(.*)$/.exec(ligne);
|
|
1909
|
+
if (m) {
|
|
1910
|
+
dedans = /^Critères\b/.test(m[1].trim());
|
|
1911
|
+
} else if (dedans) {
|
|
1912
|
+
criteres.push(ligne);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
const cle = sujet => sujet.toLocaleLowerCase('fr').replace(/\s+/g, ' ').trim();
|
|
1916
|
+
const ecarts = new Map();
|
|
1917
|
+
for (const e of fiches.flatMap(f => f.ecarts ?? [])) {
|
|
1918
|
+
const sujet = ecarts.get(cle(e.sujet))?.sujet ?? e.sujet.trim();
|
|
1919
|
+
ecarts.set(cle(e.sujet), { sujet, texte: e.texte.trim() });
|
|
1920
|
+
}
|
|
1921
|
+
return [
|
|
1922
|
+
entete,
|
|
1923
|
+
criteres.join('\n').trim(),
|
|
1924
|
+
...[...ecarts.values()].map(e => `${e.sujet} : ${e.texte}`),
|
|
1925
|
+
'Co-Authored-By: Claude <noreply@anthropic.com>',
|
|
1926
|
+
]
|
|
1927
|
+
.filter(Boolean)
|
|
1928
|
+
.join('\n\n')
|
|
1929
|
+
.concat('\n');
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// The files the copy changes against its HEAD: [{ fichier, supprime }], relative to the copy.
|
|
1933
|
+
function changements(ctx) {
|
|
1934
|
+
const r = spawnSync(
|
|
1935
|
+
'git',
|
|
1936
|
+
['-C', ctx.copie, 'status', '--porcelain', '-z', '--untracked-files=all'],
|
|
1937
|
+
{ encoding: 'utf8' }
|
|
1938
|
+
);
|
|
1939
|
+
if (r.status !== 0) return [];
|
|
1940
|
+
const entrees = r.stdout.split('\0');
|
|
1941
|
+
const out = [];
|
|
1942
|
+
for (let i = 0; i < entrees.length; i++) {
|
|
1943
|
+
const e = entrees[i];
|
|
1944
|
+
if (e.length < 4) continue;
|
|
1945
|
+
const xy = e.slice(0, 2);
|
|
1946
|
+
out.push({ fichier: e.slice(3), supprime: xy.includes('D') });
|
|
1947
|
+
// A rename or a copy names its source next.
|
|
1948
|
+
if (xy.includes('R') || xy.includes('C')) i++;
|
|
1949
|
+
}
|
|
1950
|
+
return out;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// The test files of the lot that the testwriter writes, present in the copy.
|
|
1954
|
+
function testsDuLot(ctx) {
|
|
1955
|
+
return changements(ctx)
|
|
1956
|
+
.filter(c => !c.supprime)
|
|
1957
|
+
.map(c => c.fichier)
|
|
1958
|
+
.filter(f => NOM_DE_TEST.test(path.basename(f)) && isTesterFile(f, ctx.copie))
|
|
1959
|
+
.sort();
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// The test files of the lot written in <composant> or in the shared test material: the testwriter's
|
|
1963
|
+
// lot of this component.
|
|
1964
|
+
function testsDuComposant(ctx, composant) {
|
|
1965
|
+
const dossiers = [
|
|
1966
|
+
path.join(ctx.copie, composant),
|
|
1967
|
+
...(existsSync(ctx.copie) ? matieres(ctx.copie).map(m => m.dir) : []),
|
|
1968
|
+
];
|
|
1969
|
+
return testsDuLot(ctx).filter(f => {
|
|
1970
|
+
const abs = path.join(ctx.copie, f);
|
|
1971
|
+
return dossiers.some(d => {
|
|
1972
|
+
const rel = path.relative(d, abs);
|
|
1973
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
1974
|
+
});
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// The package directory of the copy that holds <fichier>, or « . ».
|
|
1979
|
+
function paquetDe(liste, fichier) {
|
|
1980
|
+
const p = liste.find(({ dossier }) => `${fichier}`.startsWith(`${dossier}${path.sep}`));
|
|
1981
|
+
return p ? p.dossier : '.';
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// One npm command in the copy, under <env> added to the circuit's environment, its results written
|
|
1985
|
+
// to a directory of its own (RESULTATS, scripts/resultats.mjs: the summaries of Turborepo, the
|
|
1986
|
+
// JSON reports of Vitest, the SARIF of the guards), its output added to the log for the human:
|
|
1987
|
+
// `{ code, dossier }`. The circuit reads its results, never its output. It is a heavy pass (the
|
|
1988
|
+
// proof, the checks, the final pass): it holds the lock of the machine
|
|
1989
|
+
// (scripts/verrou-de-machine.mjs), its wait written to the log, and passes it to the
|
|
1990
|
+
// `npm run verifier` it runs. A wait that stops (its bound, 75; a lock held by an ancestor, 1) is
|
|
1991
|
+
// the command's code, its message in the log. Once it holds the lock, the pass plays under a
|
|
1992
|
+
// progress directory of its own (`avancement`), of the kind `genreDeLaPasse` names.
|
|
1993
|
+
function npm(ctx, etape, composant, args, env = {}) {
|
|
1994
|
+
entete(ctx, etape, composant);
|
|
1995
|
+
const dossier = mkdtempSync(path.join(ctx.base.dossier, 'resultats-'));
|
|
1996
|
+
const prefixe = Object.entries(env).map(([k, v]) => `${k}=${v} `);
|
|
1997
|
+
appendFileSync(ctx.journal, `$ ${prefixe.join('')}npm ${args.join(' ')}\n`);
|
|
1998
|
+
const fd = openSync(ctx.journal, 'a');
|
|
1999
|
+
try {
|
|
2000
|
+
const r = avecLaMachine(
|
|
2001
|
+
envMachine =>
|
|
2002
|
+
avecAvancement(ctx, genreDeLaPasse(etape, composant, args), avancement =>
|
|
2003
|
+
spawnSync('npm', args, {
|
|
2004
|
+
cwd: ctx.copie,
|
|
2005
|
+
stdio: ['ignore', fd, fd],
|
|
2006
|
+
env: {
|
|
2007
|
+
...envMachine,
|
|
2008
|
+
...env,
|
|
2009
|
+
[RESULTATS]: dossier,
|
|
2010
|
+
[AVANCEMENT]: avancement,
|
|
2011
|
+
...(args[0] === 'test' ? { [DEPUIS_LE_VERT]: '1' } : {}),
|
|
2012
|
+
},
|
|
2013
|
+
})
|
|
2014
|
+
),
|
|
2015
|
+
{ dire: l => appendFileSync(ctx.journal, `${l}\n`) }
|
|
2016
|
+
);
|
|
2017
|
+
return { code: r.status ?? 1, dossier };
|
|
2018
|
+
} catch (e) {
|
|
2019
|
+
if (!(e instanceof AttenteDeLaMachine)) throw e;
|
|
2020
|
+
appendFileSync(ctx.journal, `${e.message}\n`);
|
|
2021
|
+
return { code: e.code, dossier };
|
|
2022
|
+
} finally {
|
|
2023
|
+
closeSync(fd);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// The kind of an automatic pass, by which its total is kept from one pass to the next
|
|
2028
|
+
// (scripts/avancement.mjs): its step (the checks once final, the final pass), its component, and
|
|
2029
|
+
// its command without the test files and the filters, which move from one lot to the next.
|
|
2030
|
+
function genreDeLaPasse(etape, composant, args) {
|
|
2031
|
+
const commande = args.filter(
|
|
2032
|
+
a => !NOM_DE_TEST.test(path.basename(a)) && !a.startsWith('--filter=')
|
|
2033
|
+
);
|
|
2034
|
+
return etape === 'controles' && args[1] === 'verifier'
|
|
2035
|
+
? ['passe finale', ...commande].join(' ')
|
|
2036
|
+
: [etape, composant, ...commande].join(' ');
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// Runs <passe> under a progress directory of the kind <genre> (null: none kept), named in the
|
|
2040
|
+
// state while it plays (`avancement`), as the board reads it; then keeps its total, takes it out
|
|
2041
|
+
// of the state and removes it. Returns what <passe> returns.
|
|
2042
|
+
function avecAvancement(ctx, genre, passe) {
|
|
2043
|
+
const avancement = ouvrirAvancement({ racine: ctx.copie, genre, dans: ctx.base.dossier });
|
|
2044
|
+
if (ctx.courant) ecrireEtat(ctx, { ...ctx.courant, avancement: { dossier: avancement } });
|
|
2045
|
+
try {
|
|
2046
|
+
return passe(avancement);
|
|
2047
|
+
} finally {
|
|
2048
|
+
garderLeTotal(avancement, ctx.copie);
|
|
2049
|
+
if (ctx.courant) ecrireEtat(ctx, { ...ctx.courant, avancement: null });
|
|
2050
|
+
rmSync(avancement, { recursive: true, force: true });
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// HEAD's version of each file of <fichiers> that HEAD holds, written beside it in the copy under
|
|
2055
|
+
// the name `<nom>.circuit-avant.<test|spec>.<ext>`, which the same run plays: Map file → sibling,
|
|
2056
|
+
// both relative to the copy. The caller removes the siblings.
|
|
2057
|
+
function versionsDAvant(ctx, fichiers) {
|
|
2058
|
+
const out = new Map();
|
|
2059
|
+
for (const f of fichiers) {
|
|
2060
|
+
const r = spawnSync('git', ['-C', ctx.copie, 'show', `HEAD:${f}`], {
|
|
2061
|
+
encoding: 'utf8',
|
|
2062
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
2063
|
+
});
|
|
2064
|
+
if (r.status !== 0) continue;
|
|
2065
|
+
const soeur = f.replace(/\.(test|spec)(\.[cm]?[jt]sx?)$/, '.circuit-avant.$1$2');
|
|
2066
|
+
if (soeur === f) continue;
|
|
2067
|
+
writeFileSync(path.join(ctx.copie, soeur), r.stdout);
|
|
2068
|
+
out.set(f, soeur);
|
|
2069
|
+
}
|
|
2070
|
+
return out;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// The log of the task <t> of a summary of Turborepo, its file from the root of the copy: its path,
|
|
2074
|
+
// and its first lines, the task's own output.
|
|
2075
|
+
function journalDeTache(ctx, t) {
|
|
2076
|
+
if (!t.journal) return { chemin: null, debut: '' };
|
|
2077
|
+
const chemin = path.join(ctx.copie, t.journal);
|
|
2078
|
+
return { chemin, debut: sansCouleurs(lire(chemin)).trim().split('\n').slice(0, 80).join('\n') };
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// The measure of the proof of the red on <composant>: one run per package, `npm test -- --force
|
|
2082
|
+
// <files>` of the lot that lie in it, HEAD's version of each beside it, or, with no file of the lot in the
|
|
2083
|
+
// component, `npm test -- <package>` for each of its packages, played whole. Each run `{ groupe,
|
|
2084
|
+
// entier, avant, code, resultats, constructions }`. A measure on the same tree (the copy's
|
|
2085
|
+
// fingerprint, `empreinte`) is not played again, as Turborepo replays a task whose inputs did not
|
|
2086
|
+
// move: the last one is kept in `ctx.mesureDeLaPreuve`.
|
|
2087
|
+
function mesureDeLaPreuve(ctx, composant, liste, fichiers) {
|
|
2088
|
+
const cle = `${composant}\0${empreinte(ctx, '.')}`;
|
|
2089
|
+
if (ctx.mesureDeLaPreuve?.cle === cle) return ctx.mesureDeLaPreuve.runs;
|
|
2090
|
+
const index = ctx.composants.indexOf(composant);
|
|
2091
|
+
const entiers = fichiers.length ? [] : paquetsDesComposants(ctx, liste, i => i === index);
|
|
2092
|
+
const groupes = new Map(entiers.map(d => [d, []]));
|
|
2093
|
+
for (const f of fichiers) {
|
|
2094
|
+
const p = paquetDe(liste, f);
|
|
2095
|
+
groupes.set(p, [...(groupes.get(p) ?? []), f]);
|
|
2096
|
+
}
|
|
2097
|
+
const runs = [];
|
|
2098
|
+
for (const [dossier, groupe] of groupes) {
|
|
2099
|
+
const entier = entiers.includes(dossier);
|
|
2100
|
+
const avant = versionsDAvant(ctx, groupe);
|
|
2101
|
+
let r;
|
|
2102
|
+
try {
|
|
2103
|
+
// The files of the lot play for real (`--force`): a run replayed from Turborepo's cache, the
|
|
2104
|
+
// checks' same command on the same inputs, writes no report of Vitest.
|
|
2105
|
+
r = npm(ctx, 'preuve', composant, [
|
|
2106
|
+
'test',
|
|
2107
|
+
'--',
|
|
2108
|
+
...(entier ? [dossier] : ['--force']),
|
|
2109
|
+
...groupe,
|
|
2110
|
+
...avant.values(),
|
|
2111
|
+
]);
|
|
2112
|
+
} finally {
|
|
2113
|
+
for (const soeur of avant.values()) rmSync(path.join(ctx.copie, soeur), { force: true });
|
|
2114
|
+
}
|
|
2115
|
+
runs.push({
|
|
2116
|
+
groupe,
|
|
2117
|
+
entier,
|
|
2118
|
+
avant,
|
|
2119
|
+
code: r.code,
|
|
2120
|
+
resultats: lireRapportsVitest(r.dossier),
|
|
2121
|
+
// The builds of the component's packages that failed: each one a red, not compiling is
|
|
2122
|
+
// failing.
|
|
2123
|
+
constructions: constructionsEchouees(ctx, liste, r.dossier, index),
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
ctx.mesureDeLaPreuve = { cle, runs };
|
|
2127
|
+
return runs;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// The proof of the red on <composant>: `{ ok, rouges, texte, resume }`, `texte` without the list
|
|
2131
|
+
// of the justified greens, which only the whole report, written to its file, holds. A green test
|
|
2132
|
+
// passes by the expected greens of the sheets of the testwriter of <composant> (`verts_attendus`),
|
|
2133
|
+
// read at each proof; the measure plays again only when the copy changed (`mesureDeLaPreuve`).
|
|
2134
|
+
function preuve(ctx, composant) {
|
|
2135
|
+
const fichiers = testsDuComposant(ctx, composant);
|
|
2136
|
+
const verts = fichesDe(ctx, 'testwriter', composant).flatMap(f => f.verts_attendus ?? []);
|
|
2137
|
+
const raisonDuTest = (f, a) =>
|
|
2138
|
+
verts.find(v => v.fichier === f && v.test && (v.test === a.nom || v.test === a.titre))
|
|
2139
|
+
?.raison ?? null;
|
|
2140
|
+
const duFichier = f => verts.find(v => v.fichier === f && !v.test) ?? null;
|
|
2141
|
+
const rouges = [];
|
|
2142
|
+
// The reds of the files of the lot, apart from the existing ones and the builds.
|
|
2143
|
+
let rougesDuLot = 0;
|
|
2144
|
+
const justifies = [];
|
|
2145
|
+
const injustifies = [];
|
|
2146
|
+
const casses = [];
|
|
2147
|
+
const neufs = [];
|
|
2148
|
+
const liste = paquets(ctx.copie).filter(p => !p.hors);
|
|
2149
|
+
let anciens = 0;
|
|
2150
|
+
const reportes = [];
|
|
2151
|
+
const mesure = mesureDeLaPreuve(ctx, composant, liste, fichiers);
|
|
2152
|
+
for (const { groupe, entier, avant, code, resultats, constructions } of mesure) {
|
|
2153
|
+
for (const { nom, erreur } of constructions) {
|
|
2154
|
+
if (reportes.includes(nom)) continue;
|
|
2155
|
+
rouges.push(`construction de ${nom} — ${erreur}`);
|
|
2156
|
+
reportes.push(nom);
|
|
2157
|
+
}
|
|
2158
|
+
// The files of a package played whole, out of the lot, which HEAD holds green: a red of theirs
|
|
2159
|
+
// is a regression of the lot.
|
|
2160
|
+
if (entier) {
|
|
2161
|
+
for (const res of resultats) {
|
|
2162
|
+
const f = path.relative(ctx.copie, res.fichier);
|
|
2163
|
+
if (res.tests.length === 0 && res.etat === 'failed' && !constructions.length) {
|
|
2164
|
+
rouges.push(
|
|
2165
|
+
`rouge existant : ${f} — ne se charge pas : ${premiereLigne(res.message || res.etat)}`
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
for (const a of res.tests.filter(x => x.etat === 'failed')) {
|
|
2169
|
+
rouges.push(`rouge existant : ${f} › ${a.nom} — ${premiereLigne(a.message)}`);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
for (const f of groupe) {
|
|
2174
|
+
const res = resultats.find(t => t.fichier === path.join(ctx.copie, f));
|
|
2175
|
+
const tests = res?.tests ?? [];
|
|
2176
|
+
// The green tests of HEAD's version of the file, by their full name.
|
|
2177
|
+
const deHead = new Set(
|
|
2178
|
+
(
|
|
2179
|
+
resultats.find(t => avant.has(f) && t.fichier === path.join(ctx.copie, avant.get(f)))
|
|
2180
|
+
?.tests ?? []
|
|
2181
|
+
)
|
|
2182
|
+
.filter(a => a.etat === 'passed')
|
|
2183
|
+
.map(a => a.nom)
|
|
2184
|
+
);
|
|
2185
|
+
if (constructions.length && (!res || tests.length === 0)) {
|
|
2186
|
+
// It cannot load before the build: it plays at the checks.
|
|
2187
|
+
} else if (!res) {
|
|
2188
|
+
casses.push(
|
|
2189
|
+
`${f} — ne se charge pas : non joué (code ${code}) ; le journal du ticket : ${ctx.journal}`
|
|
2190
|
+
);
|
|
2191
|
+
} else if (tests.length === 0) {
|
|
2192
|
+
casses.push(`${f} — ne se charge pas : ${premiereLigne(res.message || res.etat)}`);
|
|
2193
|
+
}
|
|
2194
|
+
// The expected green of the whole file justifies its greens only for a file HEAD holds.
|
|
2195
|
+
const duF = duFichier(f);
|
|
2196
|
+
if (duF && !avant.has(f)) {
|
|
2197
|
+
neufs.push(
|
|
2198
|
+
`${f} — fichier neuf, que HEAD ne tient pas : son vert attendu sans test ne justifie aucun de ses verts`
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
for (const a of tests) {
|
|
2202
|
+
if (a.etat === 'failed') {
|
|
2203
|
+
rouges.push(`${f} › ${a.nom} — ${premiereLigne(a.message)}`);
|
|
2204
|
+
rougesDuLot++;
|
|
2205
|
+
} else if (a.etat === 'passed' && deHead.has(a.nom)) {
|
|
2206
|
+
anciens++;
|
|
2207
|
+
} else if (a.etat === 'passed') {
|
|
2208
|
+
const raison = raisonDuTest(f, a);
|
|
2209
|
+
if (raison) {
|
|
2210
|
+
justifies.push(`${f} › ${a.nom} — ${raison}`);
|
|
2211
|
+
} else if (duF && avant.has(f)) {
|
|
2212
|
+
justifies.push(`${f} › ${a.nom} — ${duF.raison} (par le vert attendu du fichier)`);
|
|
2213
|
+
} else {
|
|
2214
|
+
injustifies.push(`${f} › ${a.nom}`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
const section = (titre, l) =>
|
|
2221
|
+
l.length ? [`${titre} (${l.length}) :`, ...l.map(x => `- ${x}`), ''] : [];
|
|
2222
|
+
const ok =
|
|
2223
|
+
(fichiers.length > 0 || rouges.length > 0) &&
|
|
2224
|
+
casses.length === 0 &&
|
|
2225
|
+
injustifies.length === 0 &&
|
|
2226
|
+
rouges.length + justifies.length + anciens > 0;
|
|
2227
|
+
const resume = `${rouges.length} rouge(s), ${justifies.length} vert(s) justifié(s), ${injustifies.length} vert(s) sans vert attendu, ${casses.length} fichier(s) qui ne se charge(nt) pas, ${anciens} test(s) vert(s) que HEAD tient déjà, laissé(s) de côté`;
|
|
2228
|
+
const ecrit = (...sections) =>
|
|
2229
|
+
[
|
|
2230
|
+
`${SUITE.find(e => e.etat === PREUVE).titre} sur ${composant} : ${resume}.`,
|
|
2231
|
+
...[...new Set(reportes)].map(
|
|
2232
|
+
nom => `Les tests de ${nom} se jouent aux contrôles, après le développeur.`
|
|
2233
|
+
),
|
|
2234
|
+
'',
|
|
2235
|
+
...(fichiers.length || rouges.length
|
|
2236
|
+
? []
|
|
2237
|
+
: [`Aucun fichier de test du lot dans ${composant} : rien ne prouve le rouge.`, '']),
|
|
2238
|
+
...section('Rouges', rouges),
|
|
2239
|
+
...sections,
|
|
2240
|
+
...section(
|
|
2241
|
+
'Verts sans vert attendu (un test vert avant le code passe seulement quand ta fiche le nomme dans `verts_attendus` : `fichier` depuis la racine, `test` son nom complet tel que ce rapport l’écrit après « › », et `raison` ; tous les verts d’un fichier de tests que HEAD tient passent par `fichier` et `raison` seuls)',
|
|
2242
|
+
injustifies
|
|
2243
|
+
),
|
|
2244
|
+
...section(
|
|
2245
|
+
'Verts attendus sans test qui ne justifient rien (ils valent seulement pour un fichier de tests que HEAD tient)',
|
|
2246
|
+
neufs
|
|
2247
|
+
),
|
|
2248
|
+
...section('Fichiers qui ne prouvent rien', casses),
|
|
2249
|
+
]
|
|
2250
|
+
.join('\n')
|
|
2251
|
+
.trim();
|
|
2252
|
+
let n = 1;
|
|
2253
|
+
while (existsSync(`${ctx.copie}.preuve-${n}.md`)) n++;
|
|
2254
|
+
const complet = `${ctx.copie}.preuve-${n}.md`;
|
|
2255
|
+
writeFileSync(complet, `${ecrit(...section('Verts justifiés', justifies))}\n`);
|
|
2256
|
+
const texte = ecrit(
|
|
2257
|
+
`Le rapport entier, avec les verts justifiés et leurs raisons : ${complet}`,
|
|
2258
|
+
''
|
|
2259
|
+
);
|
|
2260
|
+
return {
|
|
2261
|
+
ok,
|
|
2262
|
+
rouges: rouges.length,
|
|
2263
|
+
texte,
|
|
2264
|
+
resume,
|
|
2265
|
+
resultat: {
|
|
2266
|
+
ok,
|
|
2267
|
+
rouges: rouges.length,
|
|
2268
|
+
justifies: justifies.length,
|
|
2269
|
+
sans: injustifies.length,
|
|
2270
|
+
casses: casses.length,
|
|
2271
|
+
anciens,
|
|
2272
|
+
vide: rougesDuLot + justifies.length + injustifies.length + casses.length === 0,
|
|
2273
|
+
},
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// The builds that failed in the run whose results are under <dossier> (the summary of Turborepo,
|
|
2278
|
+
// task `<package>#build`) of a package of the component at <index>: `{ nom, erreur }`, `erreur`
|
|
2279
|
+
// its log file and the beginning of its own output.
|
|
2280
|
+
function constructionsEchouees(ctx, liste, dossier, index) {
|
|
2281
|
+
return lireResumes(dossier)
|
|
2282
|
+
.filter(t => t.script === 'build' && t.code !== null && t.code !== 0)
|
|
2283
|
+
.flatMap(t => {
|
|
2284
|
+
const p = liste.find(x => x.pkg.name === t.paquet);
|
|
2285
|
+
if (!p || proprietaire(ctx, p.dossier) !== index) return [];
|
|
2286
|
+
const j = journalDeTache(ctx, t);
|
|
2287
|
+
return [{ nom: t.paquet, erreur: `son journal : ${j.chemin}\n\n${j.debut}\n` }];
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// The component of the run that owns <chemin> (from the root of the copy): `{ proprietaire,
|
|
2292
|
+
// horsTitre }`, the index of that component or -1, and the name of the package of <liste> that
|
|
2293
|
+
// holds the path when no component of the title does.
|
|
2294
|
+
function situer(ctx, liste, chemin) {
|
|
2295
|
+
const i = chemin ? proprietaire(ctx, chemin) : -1;
|
|
2296
|
+
const p = i < 0 && chemin ? liste.find(x => chemin.startsWith(`${x.dossier}/`)) : null;
|
|
2297
|
+
return { proprietaire: i, horsTitre: p ? p.pkg.name : null };
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// The reds of the check <c> (`{ args, env, code, dossier }`), read in its results: each failed
|
|
2301
|
+
// test of the reports of Vitest, each finding of the guards' SARIF, each failed build, and each
|
|
2302
|
+
// failed task none of those situates; a red check with none of them is one red of the command.
|
|
2303
|
+
// Each red `{ texte, fichier, gardes, detail, proprietaire, horsTitre, arbitrer }`: the line that
|
|
2304
|
+
// names it (test or finding, file, message), its file (null for a red of a task), the names its
|
|
2305
|
+
// finding carries (its guard and its rule), the beginning of its
|
|
2306
|
+
// task's log for a build, the index of the
|
|
2307
|
+
// component that owns it (-1 for none: the root, or a red without a file), the package out of the
|
|
2308
|
+
// title that holds it, and whether it is a finding to settle (`a-arbitrer`).
|
|
2309
|
+
function rougesDe(ctx, liste, c) {
|
|
2310
|
+
if (c.code === 0 || c.code === A_REPASSER) return [];
|
|
2311
|
+
const out = [];
|
|
2312
|
+
const testes = new Set();
|
|
2313
|
+
for (const res of lireRapportsVitest(c.dossier)) {
|
|
2314
|
+
const f = path.relative(ctx.copie, res.fichier);
|
|
2315
|
+
const lieu = situer(ctx, liste, f);
|
|
2316
|
+
if (res.tests.length === 0 && res.etat === 'failed') {
|
|
2317
|
+
out.push({
|
|
2318
|
+
texte: `test ${f} — ne se charge pas : ${premiereLigne(res.message)}`,
|
|
2319
|
+
fichier: f,
|
|
2320
|
+
...lieu,
|
|
2321
|
+
});
|
|
2322
|
+
testes.add(paquetDe(liste, f));
|
|
2323
|
+
}
|
|
2324
|
+
for (const a of res.tests.filter(x => x.etat === 'failed')) {
|
|
2325
|
+
out.push({
|
|
2326
|
+
texte: `test ${f} › ${a.nom} — ${premiereLigne(a.message)}`,
|
|
2327
|
+
fichier: f,
|
|
2328
|
+
...lieu,
|
|
2329
|
+
});
|
|
2330
|
+
testes.add(paquetDe(liste, f));
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
const { constats } = lireSarif(c.dossier);
|
|
2334
|
+
for (const k of constats) {
|
|
2335
|
+
const ou = k.fichier ? ` ${k.fichier}${k.ligne ? `:${k.ligne}` : ''}` : '';
|
|
2336
|
+
out.push({
|
|
2337
|
+
texte: `constat ${k.garde}${ou} — ${k.message}${k.genre === 'a-arbitrer' ? ' (à arbitrer)' : ''}`,
|
|
2338
|
+
fichier: k.fichier ?? null,
|
|
2339
|
+
gardes: [k.garde, k.regle].filter(Boolean),
|
|
2340
|
+
...situer(ctx, liste, k.fichier),
|
|
2341
|
+
arbitrer: k.genre === 'a-arbitrer',
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
// The guards of the project wrote findings when one comes from a guard out of the frame's.
|
|
2345
|
+
const duProjet = constats.some(k => !NOMS_DES_GARDES.includes(k.garde));
|
|
2346
|
+
for (const t of lireResumes(c.dossier)) {
|
|
2347
|
+
if (t.code === null || t.code === 0 || t.code === A_REPASSER) continue;
|
|
2348
|
+
if (t.script === 'test:vitest' && testes.has(t.dossier)) continue;
|
|
2349
|
+
if (t.script === 'grillhouse:gardes' && constats.length) continue;
|
|
2350
|
+
if (t.script === 'gardes' && duProjet) continue;
|
|
2351
|
+
const p = liste.find(x => x.pkg.name === t.paquet);
|
|
2352
|
+
const lieu = p ? situer(ctx, liste, `${p.dossier}/`) : { proprietaire: -1, horsTitre: null };
|
|
2353
|
+
if (p && lieu.proprietaire < 0) lieu.horsTitre = p.pkg.name;
|
|
2354
|
+
const j = journalDeTache(ctx, t);
|
|
2355
|
+
out.push({
|
|
2356
|
+
texte:
|
|
2357
|
+
t.script === 'build'
|
|
2358
|
+
? `construction de ${t.paquet} — son journal : ${j.chemin}`
|
|
2359
|
+
: `tâche ${t.tache} rouge (code ${t.code}) — son journal : ${j.chemin}`,
|
|
2360
|
+
detail: t.script === 'build' ? j.debut : '',
|
|
2361
|
+
...lieu,
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
if (out.length === 0) {
|
|
2365
|
+
out.push({
|
|
2366
|
+
texte: `${ligneDeCommande(c)} rouge (code ${c.code}), sans résultat qui situe son rouge — le journal du ticket : ${ctx.journal}`,
|
|
2367
|
+
proprietaire: -1,
|
|
2368
|
+
horsTitre: null,
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
return out;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const ligneDeCommande = c =>
|
|
2375
|
+
`${Object.entries(c.env ?? {})
|
|
2376
|
+
.map(([k, v]) => `${k}=${v} `)
|
|
2377
|
+
.join('')}npm ${c.args.join(' ')}`;
|
|
2378
|
+
|
|
2379
|
+
// The checks after the developpeur of the component at <index>: its own, or, when <finale>, `npm
|
|
2380
|
+
// run verifier`, what the integration plays, restricted by <restreinte> (`{ avant, apres }`, two
|
|
2381
|
+
// lots kept) to the packages that changed from one to the other and their dependents, with the
|
|
2382
|
+
// root's tasks, the guards (`--filter=...[<avant>...<apres>] --filter=//`). `{ rouge, aRepasser,
|
|
2383
|
+
// arbitrer, texte, complet, resume, proprietaire, horsTitre, lieux, faits }`: `texte` the report for the
|
|
2384
|
+
// developpeur the red goes to, its own reds first; `complet` every red, for a stop; `proprietaire`
|
|
2385
|
+
// the index of the component that owns the red, -1 for none; `horsTitre` the packages out of the
|
|
2386
|
+
// title that hold a red; `arbitrer` when every red of that developpeur is a finding to settle;
|
|
2387
|
+
// `lieux` the place of each red, `{ fichier, gardes }`, its file (null for a red without one) and
|
|
2388
|
+
// the names its finding carries (none for a test or a task); `faits` the facts of a stop on
|
|
2389
|
+
// these checks.
|
|
2390
|
+
function controles(ctx, index, finale, restreinte = null) {
|
|
2391
|
+
const composant = ctx.composants[index];
|
|
2392
|
+
const liste = paquets(ctx.copie).filter(p => !p.hors);
|
|
2393
|
+
const filtres = restreinte
|
|
2394
|
+
? ['--', `--filter=...[${restreinte.avant}...${restreinte.apres}]`, '--filter=//']
|
|
2395
|
+
: [];
|
|
2396
|
+
const commandes = finale
|
|
2397
|
+
? [{ args: ['run', 'verifier', ...filtres] }]
|
|
2398
|
+
: commandesDuComposant(ctx, index, liste);
|
|
2399
|
+
const resultats = commandes.map(({ args, env }) => {
|
|
2400
|
+
let r = npm(ctx, 'controles', composant, args, env);
|
|
2401
|
+
// verifier plays a task at 75 again itself, at rest: its 75 is final.
|
|
2402
|
+
if (r.code === A_REPASSER && !finale) r = npm(ctx, 'controles', composant, args, env);
|
|
2403
|
+
const c = { args, env, ...r };
|
|
2404
|
+
return { ...c, rouges: rougesDe(ctx, liste, c) };
|
|
2405
|
+
});
|
|
2406
|
+
const tous = resultats.flatMap(r => r.rouges);
|
|
2407
|
+
// Out of the final pass, the reds of this component are its own; the others (another component,
|
|
2408
|
+
// the root, a red no file situates, a finding of a guard on a file of another package, a package
|
|
2409
|
+
// out of the title) wait for it.
|
|
2410
|
+
const siens = r => finale || r.proprietaire === index;
|
|
2411
|
+
const rouges = tous.filter(siens);
|
|
2412
|
+
const laisses = tous.filter(r => !siens(r));
|
|
2413
|
+
const aRepasser = resultats.filter(r => r.code === A_REPASSER);
|
|
2414
|
+
const nommer = r => `${ligneDeCommande(r)} (code ${r.code})`;
|
|
2415
|
+
const enEchec = resultats.filter(r => r.code !== 0 && r.code !== A_REPASSER);
|
|
2416
|
+
// The owner of the red: in the final pass the first component, provider first, that owns one;
|
|
2417
|
+
// out of it, this one.
|
|
2418
|
+
const nommes = rouges.map(r => r.proprietaire).filter(i => i >= 0);
|
|
2419
|
+
const proprio = !rouges.length ? -1 : finale ? (nommes.length ? Math.min(...nommes) : -1) : index;
|
|
2420
|
+
const a = finale ? rouges.filter(r => r.proprietaire === proprio && proprio >= 0) : rouges;
|
|
2421
|
+
const autres = rouges.filter(r => !a.includes(r));
|
|
2422
|
+
const resume = rouges.length
|
|
2423
|
+
? `rouge : ${rouges.length} rouge(s) — ${enEchec.map(nommer).join(' ; ')}`
|
|
2424
|
+
: aRepasser.length
|
|
2425
|
+
? `à repasser : ${aRepasser.map(nommer).join(' ; ')}`
|
|
2426
|
+
: `verts : ${resultats.map(ligneDeCommande).join(' ; ')}${
|
|
2427
|
+
laisses.length
|
|
2428
|
+
? ` ; laissés à la passe finale, ${laisses.length} rouge(s) hors de ${composant}`
|
|
2429
|
+
: ''
|
|
2430
|
+
}`;
|
|
2431
|
+
// Where the checks played: the final pass out of the envelope, on the whole copy or restricted
|
|
2432
|
+
// to what changed since the last review; a component's checks, its tests and packages, its
|
|
2433
|
+
// guards in its envelope.
|
|
2434
|
+
const ou = !finale
|
|
2435
|
+
? `Contrôles du composant ${composant}, ses tests et ses paquets, ses gardes dans son enveloppe`
|
|
2436
|
+
: restreinte
|
|
2437
|
+
? `Contrôles de la passe finale, hors enveloppe, restreinte à ce que le lot change depuis la relecture d'avant et à ses dépendants (après ${composant})`
|
|
2438
|
+
: `Contrôles de la passe finale, hors enveloppe, sur la copie entière (après ${composant})`;
|
|
2439
|
+
const ligne = r => [`- ${r.texte}`, ...(r.detail ? ['', r.detail, ''] : [])];
|
|
2440
|
+
const nomDuProprio = r =>
|
|
2441
|
+
r.proprietaire >= 0 ? ctx.composants[r.proprietaire] : (r.horsTitre ?? 'hors des composants');
|
|
2442
|
+
const journal = `Le journal du ticket, pour le détail des commandes : ${ctx.journal}`;
|
|
2443
|
+
const texte = [
|
|
2444
|
+
`${ou} : ${resume}.`,
|
|
2445
|
+
'',
|
|
2446
|
+
...(a.length ? [`Tes rouges (${a.length}) :`, ...a.flatMap(ligne), ''] : []),
|
|
2447
|
+
...(aRepasser.length && !rouges.length
|
|
2448
|
+
? [`À repasser (${aRepasser.length}) : ${aRepasser.map(nommer).join(' ; ')}`, '']
|
|
2449
|
+
: []),
|
|
2450
|
+
...(autres.length
|
|
2451
|
+
? [`Les autres rouges (${autres.length}) vont aux agents de leurs composants.`, '']
|
|
2452
|
+
: []),
|
|
2453
|
+
...(laisses.length && !finale
|
|
2454
|
+
? [`Laissés à la passe finale (${laisses.length}) :`, ...laisses.map(r => `- ${r.texte}`), '']
|
|
2455
|
+
: []),
|
|
2456
|
+
journal,
|
|
2457
|
+
].join('\n');
|
|
2458
|
+
const sansListe = [`${ou} : ${resume}.`, '', journal].join('\n');
|
|
2459
|
+
const complet = [
|
|
2460
|
+
`${ou} : ${resume}.`,
|
|
2461
|
+
'',
|
|
2462
|
+
...(rouges.length
|
|
2463
|
+
? [
|
|
2464
|
+
`Les rouges (${rouges.length}) :`,
|
|
2465
|
+
...rouges.flatMap(r => ligne({ ...r, texte: `${nomDuProprio(r)} : ${r.texte}` })),
|
|
2466
|
+
'',
|
|
2467
|
+
]
|
|
2468
|
+
: []),
|
|
2469
|
+
journal,
|
|
2470
|
+
].join('\n');
|
|
2471
|
+
return {
|
|
2472
|
+
rouge: rouges.length > 0,
|
|
2473
|
+
aRepasser: aRepasser.length > 0,
|
|
2474
|
+
arbitrer: a.length > 0 && a.every(r => r.arbitrer),
|
|
2475
|
+
texte,
|
|
2476
|
+
complet,
|
|
2477
|
+
sansListe,
|
|
2478
|
+
// The reds of the stop, each with its component: this component's and those before it, or at
|
|
2479
|
+
// the final pass all of them; never those left to the final pass.
|
|
2480
|
+
rouges: rouges.map(r => ({ composant: nomDuProprio(r), texte: r.texte, detail: r.detail })),
|
|
2481
|
+
resume,
|
|
2482
|
+
proprietaire: proprio,
|
|
2483
|
+
horsTitre: [...new Set(rouges.map(r => r.horsTitre).filter(Boolean))].sort(),
|
|
2484
|
+
lieux: rouges.map(r => ({ fichier: r.fichier ?? null, gardes: r.gardes ?? [] })),
|
|
2485
|
+
faits: {
|
|
2486
|
+
rouges: [...new Set(rouges.map(nomDuProprio))].sort(),
|
|
2487
|
+
aRepasser: aRepasser.length > 0,
|
|
2488
|
+
},
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// The checks of the component at <index>, out of the final pass: `npm test -- <test files of the
|
|
2493
|
+
// lot>` that lie in it (or in the shared test material that reads only the components up to it),
|
|
2494
|
+
// `npm test -- <package>` for each of its packages and each of its the lot touches, then the
|
|
2495
|
+
// guards in the envelope of the component, whose findings on its own files alone count.
|
|
2496
|
+
function commandesDuComposant(ctx, index, liste) {
|
|
2497
|
+
const lot = changements(ctx);
|
|
2498
|
+
const touches = liste.length
|
|
2499
|
+
? [...new Set(lot.map(c => paquetDe(liste, c.fichier)).filter(d => d !== '.'))].sort()
|
|
2500
|
+
: ['.'];
|
|
2501
|
+
const sien = chemin => proprietaire(ctx, chemin) === index;
|
|
2502
|
+
const tests = testsDuLot(ctx).filter(f => sien(f) || matiereFaite(ctx, liste, f, index));
|
|
2503
|
+
const scripts = lireJson(path.join(ctx.copie, 'package.json'))?.scripts ?? {};
|
|
2504
|
+
const enveloppe = { [VARIABLE_ENVELOPPE]: ctx.composants[index] };
|
|
2505
|
+
return [
|
|
2506
|
+
...(tests.length ? [{ args: ['test', '--', ...tests] }] : []),
|
|
2507
|
+
...[
|
|
2508
|
+
...new Set([...touches.filter(sien), ...paquetsDesComposants(ctx, liste, i => i === index)]),
|
|
2509
|
+
]
|
|
2510
|
+
.sort()
|
|
2511
|
+
.map(d => ({ args: d === '.' ? ['test'] : ['test', '--', d] })),
|
|
2512
|
+
...['grillhouse:gardes', 'gardes']
|
|
2513
|
+
.filter(s => s in scripts)
|
|
2514
|
+
.map(s => ({ args: ['run', s], env: enveloppe })),
|
|
2515
|
+
];
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// The directories of the packages of <liste> that play tests (a script `test:vitest`) and whose
|
|
2519
|
+
// component, by its index, <garde> keeps; sorted.
|
|
2520
|
+
function paquetsDesComposants(ctx, liste, garde) {
|
|
2521
|
+
return liste
|
|
2522
|
+
.filter(p => p.pkg.scripts?.['test:vitest'])
|
|
2523
|
+
.filter(p => {
|
|
2524
|
+
const i = proprietaire(ctx, p.dossier);
|
|
2525
|
+
return i >= 0 && garde(i);
|
|
2526
|
+
})
|
|
2527
|
+
.map(p => p.dossier)
|
|
2528
|
+
.sort();
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// Whether the component at <index> carries no test: in a repository of packages, no package of the
|
|
2532
|
+
// frame with a script `test:vitest` lies in it or holds it, as `npm test -- <chemin>`
|
|
2533
|
+
// (scripts/tests.mjs) refuses a path out of every package or in a package without that script. A
|
|
2534
|
+
// repository without packages plays its tests at its root: each component has them.
|
|
2535
|
+
function sansTests(ctx, index) {
|
|
2536
|
+
const liste = paquets(existsSync(ctx.copie) ? ctx.copie : ctx.racine).filter(p => !p.hors);
|
|
2537
|
+
if (liste.length === 0) return false;
|
|
2538
|
+
const c = ctx.composants[index];
|
|
2539
|
+
const touche = d => c === '.' || d === c || d.startsWith(`${c}/`) || c.startsWith(`${d}/`);
|
|
2540
|
+
return !liste.some(p => typeof p.pkg.scripts?.['test:vitest'] === 'string' && touche(p.dossier));
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// Whether the copy holds a change of <composant> against its HEAD.
|
|
2544
|
+
function livre(ctx, composant) {
|
|
2545
|
+
const r = spawnSync(
|
|
2546
|
+
'git',
|
|
2547
|
+
['-C', ctx.copie, 'status', '--porcelain', '--untracked-files=all', '--', composant],
|
|
2548
|
+
{ encoding: 'utf8' }
|
|
2549
|
+
);
|
|
2550
|
+
return String(r.stdout ?? '').trim() !== '';
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
// The fingerprint of the files of <composant> in the copy: `git status --porcelain` of its
|
|
2554
|
+
// directory and the content of each file it lists.
|
|
2555
|
+
function empreinte(ctx, composant) {
|
|
2556
|
+
const r = spawnSync(
|
|
2557
|
+
'git',
|
|
2558
|
+
['-C', ctx.copie, 'status', '--porcelain', '-z', '--untracked-files=all', '--', composant],
|
|
2559
|
+
{ encoding: 'utf8' }
|
|
2560
|
+
);
|
|
2561
|
+
const statut = String(r.stdout ?? '');
|
|
2562
|
+
const h = createHash('sha256').update(statut);
|
|
2563
|
+
for (const e of statut.split('\0')) {
|
|
2564
|
+
if (e.length < 4) continue;
|
|
2565
|
+
const f = path.join(ctx.copie, e.slice(3));
|
|
2566
|
+
try {
|
|
2567
|
+
if (statSync(f).isFile()) h.update(e).update(readFileSync(f));
|
|
2568
|
+
} catch {
|
|
2569
|
+
// A file removed: its line in the status is its trace.
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
return h.digest('hex');
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// The index of the component of the run that holds <chemin> (relative to the copy), the deepest
|
|
2576
|
+
// one, `.` holding every path; -1 for none.
|
|
2577
|
+
function proprietaire(ctx, chemin) {
|
|
2578
|
+
let trouve = -1;
|
|
2579
|
+
let profondeur = -1;
|
|
2580
|
+
ctx.composants.forEach((c, i) => {
|
|
2581
|
+
const dedans = c === '.' || chemin === c || chemin.startsWith(`${c}/`);
|
|
2582
|
+
const p = c === '.' ? 0 : c.split('/').length;
|
|
2583
|
+
if (dedans && p > profondeur) {
|
|
2584
|
+
trouve = i;
|
|
2585
|
+
profondeur = p;
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
return trouve;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// True when <fichier> lies in a package of the shared test material that depends on no component
|
|
2592
|
+
// after <index>: a test it holds then reads only what is done.
|
|
2593
|
+
function matiereFaite(ctx, liste, fichier, index) {
|
|
2594
|
+
const m = matieres(ctx.copie)
|
|
2595
|
+
.map(x => path.relative(ctx.copie, x.dir))
|
|
2596
|
+
.find(d => fichier.startsWith(`${d}/`));
|
|
2597
|
+
if (!m) return false;
|
|
2598
|
+
const pkg =
|
|
2599
|
+
liste.find(p => p.dossier === m)?.pkg ??
|
|
2600
|
+
lireJson(path.join(ctx.copie, m, 'package.json')) ??
|
|
2601
|
+
{};
|
|
2602
|
+
const noms = Object.keys({
|
|
2603
|
+
...pkg.dependencies,
|
|
2604
|
+
...pkg.devDependencies,
|
|
2605
|
+
...pkg.peerDependencies,
|
|
2606
|
+
});
|
|
2607
|
+
return noms.every(n => {
|
|
2608
|
+
const p = liste.find(x => x.pkg.name === n);
|
|
2609
|
+
return !p || proprietaire(ctx, p.dossier) <= index;
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
const lireJson = f => {
|
|
2614
|
+
try {
|
|
2615
|
+
return JSON.parse(readFileSync(f, 'utf8'));
|
|
2616
|
+
} catch {
|
|
2617
|
+
return null;
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
|
|
2621
|
+
// Where the run goes on from the component at <index> to <dernier>: `{ index, etape, tentative,
|
|
2622
|
+
// rapport, finale }`. Out of a round, that component's testwriter. In a round of the reviewer's
|
|
2623
|
+
// findings (or of `--depuis`), the first component from <index> on that the round names, at the
|
|
2624
|
+
// step and with the findings it names, or that the circuit has not reached yet (`atteints`), from
|
|
2625
|
+
// its testwriter; a component it has reached and the round does not name plays nothing, as Bazel
|
|
2626
|
+
// plays only the targets a change affects: the final pass judges it. Past them, the final pass.
|
|
2627
|
+
function suivant(ctx, index, dernier) {
|
|
2628
|
+
for (let i = index; i <= dernier; i++) {
|
|
2629
|
+
const c = ctx.composants[i];
|
|
2630
|
+
const r = ctx.renvois?.[c];
|
|
2631
|
+
if (r) return { index: i, etape: r.etape, tentative: 1, rapport: r[r.etape], finale: false };
|
|
2632
|
+
if (!ctx.renvois || !ctx.atteints.includes(c)) {
|
|
2633
|
+
return { index: i, etape: TESTS, tentative: 1, rapport: null, finale: false };
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return { index: dernier, etape: CONTROLES, tentative: 0, rapport: null, finale: true };
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// The component <composant>, whose work the circuit has carried through (its proof without a red
|
|
2640
|
+
// for its developpeur, its developpeur's delivery, its green checks, the final pass green), kept
|
|
2641
|
+
// among `atteints`: a component, each on its own, whatever the title gains before it.
|
|
2642
|
+
function atteindre(ctx, composant) {
|
|
2643
|
+
if (!ctx.atteints.includes(composant)) ctx.atteints.push(composant);
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// The RENDU verdicts since the ticket's last ACCEPTÉ: a lot the queue sent back starts a new count.
|
|
2647
|
+
function rendusDepuisAccepte(ctx) {
|
|
2648
|
+
const verdicts = fichesDe(ctx, 'relecteur').map(f => f.verdict);
|
|
2649
|
+
return verdicts.length - (verdicts.lastIndexOf('ACCEPTÉ') + 1);
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// The counts of the bounded loops that run at the step <etape> under way, by their key
|
|
2653
|
+
// (scripts/etapes.mjs, `boucles`): the session of the testwriter at its tests and its proof, of the
|
|
2654
|
+
// developpeur at its code and its checks, the review; null at a step without one.
|
|
2655
|
+
function boucles(ctx, etape, tentative, finale) {
|
|
2656
|
+
const compte = {
|
|
2657
|
+
tests: () => tentative,
|
|
2658
|
+
code: () => tentative,
|
|
2659
|
+
relecture: () => rendusDepuisAccepte(ctx) + 1,
|
|
2660
|
+
};
|
|
2661
|
+
const e = etapeDuCircuit({ etape, finale });
|
|
2662
|
+
const l = e ? bouclesDe(e).filter(b => compte[b.cle]) : [];
|
|
2663
|
+
return l.length ? Object.fromEntries(l.map(b => [b.cle, compte[b.cle]()])) : null;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// The circuit from <debut> (where `depart` starts it) to the entry of the lot in the queue.
|
|
2667
|
+
async function derouler(ctx, debut) {
|
|
2668
|
+
let { index, etape, tentative, rapport, dernier, finale } = debut;
|
|
2669
|
+
// The step a `--reprendre` starts at, the first of the loop.
|
|
2670
|
+
let reprise = Boolean(ctx.reprendre);
|
|
2671
|
+
for (;;) {
|
|
2672
|
+
const composant = ctx.composants[index];
|
|
2673
|
+
const reprend = reprise;
|
|
2674
|
+
reprise = false;
|
|
2675
|
+
// The checks of the last component are the final pass: the state says so from its start.
|
|
2676
|
+
const enFinale = finale || (etape === CONTROLES && index === dernier);
|
|
2677
|
+
const etat = {
|
|
2678
|
+
etape,
|
|
2679
|
+
composant,
|
|
2680
|
+
tentative,
|
|
2681
|
+
code: null,
|
|
2682
|
+
raison: null,
|
|
2683
|
+
rapport,
|
|
2684
|
+
dernier: ctx.composants[dernier],
|
|
2685
|
+
finale: enFinale,
|
|
2686
|
+
boucles: boucles(ctx, etape, tentative, enFinale),
|
|
2687
|
+
};
|
|
2688
|
+
// A step the circuit leads itself names its start in the state while it runs, and writes its
|
|
2689
|
+
// line to the register at its end, whatever its outcome.
|
|
2690
|
+
const menee = MENEES.has(etape) ? { etape, composant, debut: new Date().toISOString() } : null;
|
|
2691
|
+
ecrireEtat(ctx, { ...etat, mene: menee?.debut ?? null });
|
|
2692
|
+
try {
|
|
2693
|
+
if (etape === TESTS && sansTests(ctx, index)) {
|
|
2694
|
+
commenter(
|
|
2695
|
+
ctx,
|
|
2696
|
+
composant,
|
|
2697
|
+
'testwriter',
|
|
2698
|
+
'aucun test dans ce composant (aucun paquet avec un script test:vitest ne le tient) : ni testwriter ni preuve, son développeur directement.'
|
|
2699
|
+
);
|
|
2700
|
+
rapport = [
|
|
2701
|
+
`Le composant ${composant} ne porte aucun test (aucun paquet avec un script test:vitest ne le tient) : le circuit le lance sans testwriter ni preuve. Tu écris sa part du ticket d'après ses critères ; les contrôles suivent ta séance.`,
|
|
2702
|
+
rapport,
|
|
2703
|
+
]
|
|
2704
|
+
.filter(Boolean)
|
|
2705
|
+
.join('\n\n');
|
|
2706
|
+
etape = CODE;
|
|
2707
|
+
tentative = 1;
|
|
2708
|
+
} else if (etape === TESTS) {
|
|
2709
|
+
const fiche = await seance(ctx, 'testwriter', composant, {
|
|
2710
|
+
tentative,
|
|
2711
|
+
rapport,
|
|
2712
|
+
derniere: tentative > RELANCES_TESTWRITER,
|
|
2713
|
+
});
|
|
2714
|
+
// A testwriter who declares himself blocked or in need of an answer goes to the regulator.
|
|
2715
|
+
if (fiche.statut !== 'FAIT')
|
|
2716
|
+
throw arretDuStatut(ctx, 'testwriter', composant, tentative, fiche);
|
|
2717
|
+
commenter(ctx, composant, 'testwriter', `séance ${tentative} finie, sa fiche lue.`);
|
|
2718
|
+
etape = PREUVE;
|
|
2719
|
+
} else if (etape === PREUVE) {
|
|
2720
|
+
const p = preuve(ctx, composant);
|
|
2721
|
+
rapport = p.texte;
|
|
2722
|
+
if (p.ok) {
|
|
2723
|
+
// The reviewer's findings for this developpeur follow the proof: a finding launches him,
|
|
2724
|
+
// red or not.
|
|
2725
|
+
const pourLui = ctx.renvois?.[composant]?.developpeur;
|
|
2726
|
+
const auDeveloppeur = p.rouges > 0 || Boolean(pourLui);
|
|
2727
|
+
const suite = p.rouges
|
|
2728
|
+
? 'au développeur'
|
|
2729
|
+
: pourLui
|
|
2730
|
+
? 'aucun rouge ; au développeur, que nomme un constat du relecteur'
|
|
2731
|
+
: 'aucun rouge : un lot de tests seul, aux contrôles';
|
|
2732
|
+
ecrireRapport(
|
|
2733
|
+
ctx,
|
|
2734
|
+
composant,
|
|
2735
|
+
'preuve',
|
|
2736
|
+
`${p.resume} ; ${suite}.\n\n${p.texte}`,
|
|
2737
|
+
p.resultat
|
|
2738
|
+
);
|
|
2739
|
+
etape = auDeveloppeur ? CODE : CONTROLES;
|
|
2740
|
+
tentative = auDeveloppeur ? 1 : 0;
|
|
2741
|
+
if (pourLui) rapport = `${rapport}\n\nLES CONSTATS DU RELECTEUR :\n\n${pourLui}`;
|
|
2742
|
+
if (!auDeveloppeur) atteindre(ctx, composant);
|
|
2743
|
+
// The reds of a proof are its component's.
|
|
2744
|
+
ctx.rougesDuDeveloppeur = { rouges: [composant] };
|
|
2745
|
+
} else if (tentative <= RELANCES_TESTWRITER) {
|
|
2746
|
+
ecrireRapport(
|
|
2747
|
+
ctx,
|
|
2748
|
+
composant,
|
|
2749
|
+
'preuve',
|
|
2750
|
+
`${p.resume} ; testwriter relancé (séance ${tentative + 1}).\n\n${p.texte}`,
|
|
2751
|
+
p.resultat
|
|
2752
|
+
);
|
|
2753
|
+
etape = TESTS;
|
|
2754
|
+
tentative++;
|
|
2755
|
+
} else {
|
|
2756
|
+
throw new Arret('preuve', p.texte, { resultat: p.resultat });
|
|
2757
|
+
}
|
|
2758
|
+
} else if (etape === CODE) {
|
|
2759
|
+
const fort = tentative >= SEANCES_DEVELOPPEUR;
|
|
2760
|
+
const avant = empreinte(ctx, composant);
|
|
2761
|
+
const fiche = await seance(ctx, 'developpeur', composant, {
|
|
2762
|
+
tentative,
|
|
2763
|
+
rapport,
|
|
2764
|
+
fort,
|
|
2765
|
+
derniere: fort,
|
|
2766
|
+
});
|
|
2767
|
+
// A question goes to the regulator; a red out of reach, to the testwriter its sheet sends it
|
|
2768
|
+
// to, else to the regulator.
|
|
2769
|
+
if (fiche.statut === 'BESOIN') {
|
|
2770
|
+
throw arretDuStatut(ctx, 'developpeur', composant, tentative, fiche);
|
|
2771
|
+
}
|
|
2772
|
+
const renvoi = renvoiAuTestwriter(ctx, fiche, index);
|
|
2773
|
+
if (renvoi) {
|
|
2774
|
+
// The red that remains lies in the tests of that component: the gesture of the table of
|
|
2775
|
+
// the stops, its testwriter, as a regulator's `RELANCE --depuis testwriter <composant>`, the
|
|
2776
|
+
// handover as its context.
|
|
2777
|
+
const cible = ctx.composants[renvoi.index];
|
|
2778
|
+
if (ctx.renvoiAuTestwriter === cible) {
|
|
2779
|
+
throw new Arret(
|
|
2780
|
+
'sans-livraison',
|
|
2781
|
+
`La fiche de la séance ${tentative} du développeur de ${composant} renvoie une deuxième fois de suite à \`testwriter ${cible}\`, sans contrôles verts depuis le premier renvoi : le testwriter n'a pas levé ce rouge.\n\n${renvoi.texte}`
|
|
2782
|
+
);
|
|
2783
|
+
}
|
|
2784
|
+
ctx.renvoiAuTestwriter = cible;
|
|
2785
|
+
commenter(
|
|
2786
|
+
ctx,
|
|
2787
|
+
composant,
|
|
2788
|
+
'developpeur',
|
|
2789
|
+
`séance ${tentative}${fort ? ' (opus, effort high)' : ''} finie ; sa fiche renvoie le rouge à \`testwriter ${cible}\` : le circuit rejoue ${cible} depuis son testwriter, puis les composants d'après.`
|
|
2790
|
+
);
|
|
2791
|
+
const d = decisionDuGeste(
|
|
2792
|
+
ctx,
|
|
2793
|
+
gesteDe('renvoi-au-testwriter'),
|
|
2794
|
+
renvoi.index,
|
|
2795
|
+
renvoi.texte
|
|
2796
|
+
);
|
|
2797
|
+
ctx.reprendre = false;
|
|
2798
|
+
ctx.depuis = d.depuis;
|
|
2799
|
+
ctx.contexte = null;
|
|
2800
|
+
({ index, etape, tentative, rapport, dernier, finale } = depart(ctx, null, index));
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
if (fiche.statut === 'BLOQUÉ') {
|
|
2804
|
+
throw arretDuStatut(ctx, 'developpeur', composant, tentative, fiche);
|
|
2805
|
+
}
|
|
2806
|
+
if (empreinte(ctx, composant) === avant && reprend && livre(ctx, composant)) {
|
|
2807
|
+
// A resumed developpeur that finds its lot delivered: the checks judge it.
|
|
2808
|
+
commenter(
|
|
2809
|
+
ctx,
|
|
2810
|
+
composant,
|
|
2811
|
+
'developpeur',
|
|
2812
|
+
`séance ${tentative} reprise sans changement, le composant garde la livraison d'avant : aux contrôles.`
|
|
2813
|
+
);
|
|
2814
|
+
inscrire(ctx, { role: AUTEUR_DU_CIRCUIT, etape: CODE, composant, inchange: true });
|
|
2815
|
+
} else if (empreinte(ctx, composant) === avant) {
|
|
2816
|
+
throw new Arret(
|
|
2817
|
+
'sans-livraison',
|
|
2818
|
+
`La séance ${tentative} du développeur de ${composant} ne change aucun fichier de ${composant} : un développeur n'est lancé que sur un rouge ou un constat du relecteur, et il reste. Sa passation au ticket dit pourquoi :\n\n${passations(ctx).at(-1)?.text ?? 'aucune'}`
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
atteindre(ctx, composant);
|
|
2822
|
+
commenter(
|
|
2823
|
+
ctx,
|
|
2824
|
+
composant,
|
|
2825
|
+
'developpeur',
|
|
2826
|
+
`séance ${tentative}${fort ? ' (opus, effort high)' : ''} finie, sa fiche lue.`
|
|
2827
|
+
);
|
|
2828
|
+
etape = CONTROLES;
|
|
2829
|
+
} else if (etape === CONTROLES) {
|
|
2830
|
+
const passeFinale = enFinale;
|
|
2831
|
+
// The final pass: after a regeneration of the references, restricted to what it changed;
|
|
2832
|
+
// else whole, or restricted to what changed since the last review.
|
|
2833
|
+
const regeneration = passeFinale ? (ctx.regeneration ?? null) : null;
|
|
2834
|
+
ctx.regeneration = null;
|
|
2835
|
+
const passe = !passeFinale
|
|
2836
|
+
? { arbre: null, restreinte: null }
|
|
2837
|
+
: regeneration
|
|
2838
|
+
? { arbre: arbreDeLaCopie(ctx), restreinte: regeneration.restreinte }
|
|
2839
|
+
: laPasseFinale(ctx);
|
|
2840
|
+
const k = controles(ctx, index, passeFinale, passe.restreinte);
|
|
2841
|
+
const resultat = issue => ({ issue, finale: passeFinale });
|
|
2842
|
+
rapport = k.texte;
|
|
2843
|
+
// A stop of the checks: <phrase>, then the report, its reds also kept apart.
|
|
2844
|
+
const arreter = (nom, phrase, faits) => {
|
|
2845
|
+
const avant = phrase ? `${phrase}\n\n` : '';
|
|
2846
|
+
throw new Arret(nom, `${avant}${k.complet}`, {
|
|
2847
|
+
rouges: k.rouges,
|
|
2848
|
+
motif: `${avant}${k.sansListe}`,
|
|
2849
|
+
faits,
|
|
2850
|
+
});
|
|
2851
|
+
};
|
|
2852
|
+
// The developpeur a red goes to: this component's, or in the final pass the owner's (the
|
|
2853
|
+
// table of the stops, scripts/etapes.mjs: rouge-d-un-autre-composant).
|
|
2854
|
+
const vers = k.rouge && passeFinale ? k.proprietaire : index;
|
|
2855
|
+
// One count of sessions for the component's checks, and one for the whole final pass,
|
|
2856
|
+
// whichever component its red goes to.
|
|
2857
|
+
const suivante = tentative + 1;
|
|
2858
|
+
// A final pass red on the references the project declares alone: their regeneration.
|
|
2859
|
+
const aRegenerer = k.rouge && passeFinale ? referencesDesRouges(ctx, k.lieux) : [];
|
|
2860
|
+
const noms = aRegenerer.map(r => r.nom).join(', ');
|
|
2861
|
+
if (aRegenerer.length && regeneration) {
|
|
2862
|
+
arreter(
|
|
2863
|
+
'references',
|
|
2864
|
+
`La passe finale, après la régénération des références (${noms}), reste rouge sur elles : la régénération ne lève pas leurs rouges. La décision revient au régulateur.`,
|
|
2865
|
+
k.faits
|
|
2866
|
+
);
|
|
2867
|
+
} else if (aRegenerer.length) {
|
|
2868
|
+
ecrireRapport(
|
|
2869
|
+
ctx,
|
|
2870
|
+
composant,
|
|
2871
|
+
'controles',
|
|
2872
|
+
`${k.resume} ; ses rouges sont ceux des références que le projet déclare (${noms}) : à leur régénération.`,
|
|
2873
|
+
resultat('rouge'),
|
|
2874
|
+
k.texte
|
|
2875
|
+
);
|
|
2876
|
+
ctx.entiere = !passe.restreinte;
|
|
2877
|
+
etape = REFERENCES;
|
|
2878
|
+
tentative = 0;
|
|
2879
|
+
finale = true;
|
|
2880
|
+
} else if (k.rouge && passeFinale && k.horsTitre.length) {
|
|
2881
|
+
arreter(
|
|
2882
|
+
'controles',
|
|
2883
|
+
`La passe finale est rouge dans ${k.horsTitre.join(', ')}, hors des composants du ticket (${ctx.composants.join(', ')}) : aucun de leurs développeurs n'est relancé. La décision revient au régulateur : faire ajouter ce composant au titre du ticket par le superviseur (\`MONTE\`), le rouge allant ensuite à son développeur ; ou faire corriger le lot par le composant qui le cause (\`RELANCE --depuis developpeur <composant>\`, le rouge en \`contexte\`).`,
|
|
2884
|
+
k.faits
|
|
2885
|
+
);
|
|
2886
|
+
} else if (k.rouge && vers < 0) {
|
|
2887
|
+
arreter(
|
|
2888
|
+
'controles',
|
|
2889
|
+
`Aucun composant du ticket (${ctx.composants.join(', ')}) ne possède ce rouge de la passe finale (un rouge de la racine, ou que rien ne situe) : la décision revient au régulateur.`,
|
|
2890
|
+
k.faits
|
|
2891
|
+
);
|
|
2892
|
+
} else if (k.rouge && k.arbitrer) {
|
|
2893
|
+
arreter(
|
|
2894
|
+
'controles',
|
|
2895
|
+
`Les rouges de ${ctx.composants[vers]} sont des constats à arbitrer : aucun développeur ne les tranche, la décision revient au régulateur.`,
|
|
2896
|
+
{ ...k.faits, genre: 'a-arbitrer' }
|
|
2897
|
+
);
|
|
2898
|
+
} else if (k.rouge && suivante <= SEANCES_DEVELOPPEUR) {
|
|
2899
|
+
const qui = vers === index ? '' : ` de ${ctx.composants[vers]}`;
|
|
2900
|
+
ecrireRapport(
|
|
2901
|
+
ctx,
|
|
2902
|
+
composant,
|
|
2903
|
+
'controles',
|
|
2904
|
+
`${k.resume} ; développeur${qui} relancé (séance ${suivante}).`,
|
|
2905
|
+
resultat('rouge'),
|
|
2906
|
+
k.texte
|
|
2907
|
+
);
|
|
2908
|
+
etape = CODE;
|
|
2909
|
+
tentative = suivante;
|
|
2910
|
+
index = vers;
|
|
2911
|
+
finale = passeFinale;
|
|
2912
|
+
ctx.rougesDuDeveloppeur = k.faits;
|
|
2913
|
+
} else if (k.rouge) {
|
|
2914
|
+
arreter('controles', null, k.faits);
|
|
2915
|
+
} else if (k.aRepasser) {
|
|
2916
|
+
arreter('a-repasser', null, k.faits);
|
|
2917
|
+
} else {
|
|
2918
|
+
ecrireRapport(ctx, composant, 'controles', `${k.resume}.`, resultat('vert'));
|
|
2919
|
+
// Green checks: a sending of the developpeur to a testwriter may come again.
|
|
2920
|
+
ctx.renvoiAuTestwriter = null;
|
|
2921
|
+
// Green checks carry this component, or at the final pass every one, to their end.
|
|
2922
|
+
for (const c of passeFinale ? ctx.composants : [composant]) atteindre(ctx, c);
|
|
2923
|
+
if (!passeFinale) {
|
|
2924
|
+
({ index, etape, tentative, rapport, finale } = suivant(ctx, index + 1, dernier));
|
|
2925
|
+
} else {
|
|
2926
|
+
// The whole pass green on this lot; the lot the last review accepted goes to the
|
|
2927
|
+
// integration, any other to the review.
|
|
2928
|
+
// A pass restricted to a regeneration after a whole one counts as whole.
|
|
2929
|
+
if (passe.arbre && (!passe.restreinte || regeneration?.entiere)) {
|
|
2930
|
+
ctx.verifie = passe.arbre;
|
|
2931
|
+
}
|
|
2932
|
+
const r = relectureDAvant(ctx);
|
|
2933
|
+
const accepte =
|
|
2934
|
+
r?.fiche.verdict === 'ACCEPTÉ' && passe.arbre && r.lot.arbre === passe.arbre;
|
|
2935
|
+
etape = accepte ? INTEGRATION : RELECTURE;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
} else if (etape === REFERENCES) {
|
|
2939
|
+
const g = regenerer(ctx, composant);
|
|
2940
|
+
ecrireRapport(ctx, composant, 'references', g.texte, {
|
|
2941
|
+
regenerees: g.regenerees,
|
|
2942
|
+
fichiers: g.fichiers,
|
|
2943
|
+
});
|
|
2944
|
+
ctx.regeneration = { restreinte: g.restreinte, entiere: ctx.entiere === true };
|
|
2945
|
+
ctx.entiere = false;
|
|
2946
|
+
etape = CONTROLES;
|
|
2947
|
+
tentative = 0;
|
|
2948
|
+
finale = true;
|
|
2949
|
+
} else if (etape === RELECTURE) {
|
|
2950
|
+
const v = await relire(ctx, composant);
|
|
2951
|
+
// A question for the owner: nothing is committed nor relaunched before his answer.
|
|
2952
|
+
if (v.fiche.question) {
|
|
2953
|
+
throw new Arret(
|
|
2954
|
+
'besoin',
|
|
2955
|
+
`Le relecteur attend une réponse : ${v.fiche.question}\n\nLa décision revient au régulateur. Le verdict :\n\n${v.texte}`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
if (v.verdict === 'ACCEPTÉ' && (!v.arbre || ctx.verifie === v.arbre)) {
|
|
2959
|
+
commenter(ctx, composant, 'relecture', 'ACCEPTÉ ; le lot va à l’intégration.');
|
|
2960
|
+
etape = INTEGRATION;
|
|
2961
|
+
} else if (v.verdict === 'ACCEPTÉ') {
|
|
2962
|
+
// The whole final pass has not played on the lot accepted: it plays once, then the
|
|
2963
|
+
// integration.
|
|
2964
|
+
commenter(
|
|
2965
|
+
ctx,
|
|
2966
|
+
composant,
|
|
2967
|
+
'relecture',
|
|
2968
|
+
'ACCEPTÉ ; la passe finale entière, qui n’a pas joué sur ce lot, puis l’intégration.'
|
|
2969
|
+
);
|
|
2970
|
+
index = dernier;
|
|
2971
|
+
etape = CONTROLES;
|
|
2972
|
+
tentative = 0;
|
|
2973
|
+
finale = true;
|
|
2974
|
+
} else {
|
|
2975
|
+
// The reviews since the last ACCEPTÉ: a lot the queue sent back starts a new count.
|
|
2976
|
+
const rendus = rendusDepuisAccepte(ctx);
|
|
2977
|
+
if (rendus >= RELECTURES) {
|
|
2978
|
+
throw new Arret(
|
|
2979
|
+
'relecture-arret',
|
|
2980
|
+
`${rendus} relectures ont rendu le lot : le circuit s'arrête à la troisième. La décision revient au régulateur.\n\nLe dernier verdict :\n\n${v.texte}`
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
const renvois = renvoisDuVerdict(ctx, v.fiche, v.texte);
|
|
2984
|
+
ctx.renvois = Object.fromEntries([...renvois].map(([i, r]) => [ctx.composants[i], r]));
|
|
2985
|
+
dernier = ctx.composants.length - 1;
|
|
2986
|
+
({ index, etape, tentative, rapport, finale } = suivant(ctx, 0, dernier));
|
|
2987
|
+
const qui = [...renvois]
|
|
2988
|
+
.sort(([a], [b]) => a - b)
|
|
2989
|
+
.map(([i, r]) => `${r.etape} ${ctx.composants[i]}`)
|
|
2990
|
+
.join(', ');
|
|
2991
|
+
commenter(
|
|
2992
|
+
ctx,
|
|
2993
|
+
composant,
|
|
2994
|
+
'relecture',
|
|
2995
|
+
`RENDU (relecture ${rendus}) ; le circuit relance ${qui}, puis la passe finale restreinte à ce que le correctif change et à ses dépendants, et le relecteur sur la différence.`
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
} else if (etape === INTEGRATION) {
|
|
2999
|
+
const r = integrer(ctx);
|
|
3000
|
+
if (r.code === A_REPASSER) throw new Arret('a-repasser', fin(r.sortie, 40));
|
|
3001
|
+
if (r.code !== 0) {
|
|
3002
|
+
throw new Arret(
|
|
3003
|
+
'integration',
|
|
3004
|
+
`Le script d'intégration refuse le lot (code ${r.code}) : la décision revient au régulateur.\n\n${fin(r.sortie, 60)}`
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
const branche = `agent/${ctx.ticket}`;
|
|
3008
|
+
const commit = spawnSync('git', ['-C', ctx.racine, 'rev-parse', '--short', branche], {
|
|
3009
|
+
encoding: 'utf8',
|
|
3010
|
+
}).stdout.trim();
|
|
3011
|
+
commenter(
|
|
3012
|
+
ctx,
|
|
3013
|
+
composant,
|
|
3014
|
+
'integration',
|
|
3015
|
+
`${commit ? `commit ${commit} sur ${branche}` : 'lot commité'} ; la branche est en file.`
|
|
3016
|
+
);
|
|
3017
|
+
etape = EN_FILE;
|
|
3018
|
+
} else if (etape === EN_FILE) {
|
|
3019
|
+
const f = file(ctx);
|
|
3020
|
+
if (f.etat === 'ferme') {
|
|
3021
|
+
ecrireEtat(ctx, { ...etat, etape: 'integre', code: 0 });
|
|
3022
|
+
commenter(ctx, composant, 'file', `${f.raison}.`);
|
|
3023
|
+
return { code: 0 };
|
|
3024
|
+
}
|
|
3025
|
+
if (f.etat === 'en-file') throw new Arret('a-repasser', f.raison);
|
|
3026
|
+
throw new Arret(
|
|
3027
|
+
'integration',
|
|
3028
|
+
`La file rouvre le ticket : la décision revient au régulateur.\n\n${f.raison}`
|
|
3029
|
+
);
|
|
3030
|
+
} else {
|
|
3031
|
+
throw new Usage(`étape inconnue dans l'état : ${etape}`);
|
|
3032
|
+
}
|
|
3033
|
+
} catch (e) {
|
|
3034
|
+
if (!(e instanceof Arret)) throw e;
|
|
3035
|
+
const code = ARRETS[e.nom];
|
|
3036
|
+
// A stop of a developpeur's session rests on the reds it was launched on.
|
|
3037
|
+
const faits = etape === CODE ? { ...ctx.rougesDuDeveloppeur, ...e.faits } : e.faits;
|
|
3038
|
+
ecrireEtat(ctx, { ...etat, code, raison: e.nom, rapport: e.rapport });
|
|
3039
|
+
// The stop kept with the circuit's results, which the bilan counts.
|
|
3040
|
+
inscrire(ctx, {
|
|
3041
|
+
role: AUTEUR_DU_CIRCUIT,
|
|
3042
|
+
etape,
|
|
3043
|
+
composant,
|
|
3044
|
+
arret: e.nom,
|
|
3045
|
+
...(etape === CONTROLES && (finale || index === dernier) ? { finale: true } : {}),
|
|
3046
|
+
...(e.resultat ? { resultat: e.resultat } : {}),
|
|
3047
|
+
});
|
|
3048
|
+
const ou = ouDansLeCircuit({
|
|
3049
|
+
etape,
|
|
3050
|
+
finale: finale || (etape === CONTROLES && index === dernier),
|
|
3051
|
+
});
|
|
3052
|
+
commenter(
|
|
3053
|
+
ctx,
|
|
3054
|
+
composant,
|
|
3055
|
+
etape,
|
|
3056
|
+
`arrêt ${e.nom} à ${ou} — ${premiereLigne(e.rapport)} — journal ${ctx.journal}`
|
|
3057
|
+
);
|
|
3058
|
+
process.stderr.write(
|
|
3059
|
+
`⛔ Circuit de ${ctx.ticket} arrêté : ${e.nom}, à ${ou} de ${composant}.\n`
|
|
3060
|
+
);
|
|
3061
|
+
return {
|
|
3062
|
+
code,
|
|
3063
|
+
nom: e.nom,
|
|
3064
|
+
rapport: e.rapport,
|
|
3065
|
+
rouges: e.rouges,
|
|
3066
|
+
motif: e.motif,
|
|
3067
|
+
faits,
|
|
3068
|
+
etat,
|
|
3069
|
+
};
|
|
3070
|
+
} finally {
|
|
3071
|
+
if (menee) inscrireEtape(ctx, menee);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// The decision of the regulator's sheet <fiche>: `{ ligne, reprendre: true, contexte }` or
|
|
3077
|
+
// The decision of the regulator's sheet <fiche>: `{ ligne, reprendre, contexte }` for a RELANCE
|
|
3078
|
+
// --reprendre, `contexte` its context or null; `{ ligne, depuis }` for a RELANCE --depuis,
|
|
3079
|
+
// `depuis` each component it plays again in the order of the title, `{ etape, composant,
|
|
3080
|
+
// contexte }`, as GitLab's « Retry failed jobs » retries every job in one go; `{ ligne, question }`
|
|
3081
|
+
// for a MONTE; `ligne` the decision as the circuit writes it at the ticket. Null for a `--depuis`
|
|
3082
|
+
// that names a component the title does not.
|
|
3083
|
+
function decisionDe(ctx, fiche) {
|
|
3084
|
+
if (fiche.decision === 'MONTE') {
|
|
3085
|
+
return { ligne: `MONTE : ${fiche.question}`, question: fiche.question };
|
|
3086
|
+
}
|
|
3087
|
+
if (fiche.decision === 'RELANCE --reprendre') {
|
|
3088
|
+
const contexte = fiche.contexte?.trim() || null;
|
|
3089
|
+
const ligne = `RELANCE --reprendre${contexte ? `\nCONTEXTE : ${contexte}` : ''}`;
|
|
3090
|
+
return { ligne, reprendre: true, contexte };
|
|
3091
|
+
}
|
|
3092
|
+
const indices = fiche.depuis.map(x => indexDe(ctx, x.composant));
|
|
3093
|
+
if (indices.some(i => i < 0)) return null;
|
|
3094
|
+
const depuis = fiche.depuis
|
|
3095
|
+
.map((x, n) => ({
|
|
3096
|
+
etape: x.role,
|
|
3097
|
+
composant: ctx.composants[indices[n]],
|
|
3098
|
+
contexte: x.contexte?.trim() || null,
|
|
3099
|
+
}))
|
|
3100
|
+
.sort((a, b) => ctx.composants.indexOf(a.composant) - ctx.composants.indexOf(b.composant));
|
|
3101
|
+
return {
|
|
3102
|
+
ligne: [
|
|
3103
|
+
`RELANCE --depuis ${depuis.map(x => `${x.etape} ${x.composant}`).join(', ')}`,
|
|
3104
|
+
...depuis.filter(x => x.contexte).map(x => `CONTEXTE ${x.composant} : ${x.contexte}`),
|
|
3105
|
+
].join('\n'),
|
|
3106
|
+
depuis,
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
// Whether the stops <a> and <b> are the same: their name, step and component.
|
|
3111
|
+
const memeArret = (a, b) =>
|
|
3112
|
+
Boolean(a && b) &&
|
|
3113
|
+
a.nom === b.nom &&
|
|
3114
|
+
a.etat.etape === b.etat.etape &&
|
|
3115
|
+
a.etat.composant === b.etat.composant;
|
|
3116
|
+
|
|
3117
|
+
// Whether the testwriter of the component at <i> has written its tests, as the state <etat> of a stop
|
|
3118
|
+
// says: a component before the one it stopped at, that one past its testwriter's step, every one at
|
|
3119
|
+
// the final pass, one the circuit has carried through (`atteints`), or one without tests.
|
|
3120
|
+
function testwriterPasse(ctx, etat, i) {
|
|
3121
|
+
const ici = ctx.composants.indexOf(etat.composant);
|
|
3122
|
+
return (
|
|
3123
|
+
etat.finale === true ||
|
|
3124
|
+
i < ici ||
|
|
3125
|
+
(i === ici && etat.etape !== TESTS) ||
|
|
3126
|
+
ctx.atteints.includes(ctx.composants[i]) ||
|
|
3127
|
+
sansTests(ctx, i)
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
// The fingerprint of the ticket's description, read now.
|
|
3132
|
+
const empreinteDuTicket = ctx =>
|
|
3133
|
+
createHash('sha256').update(leTicket(ctx).description).digest('hex');
|
|
3134
|
+
|
|
3135
|
+
// The fingerprint of what the stop <a> rests on, taken once: the copy (each file it changes
|
|
3136
|
+
// against its HEAD) and the ticket's description. The same stop come back on the same fingerprint
|
|
3137
|
+
// brings no new fact.
|
|
3138
|
+
function signatureDe(ctx, a) {
|
|
3139
|
+
a.signature ??= createHash('sha256')
|
|
3140
|
+
.update(existsSync(ctx.copie) ? empreinte(ctx, '.') : '')
|
|
3141
|
+
.update(empreinteDuTicket(ctx))
|
|
3142
|
+
.digest('hex');
|
|
3143
|
+
return a.signature;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// How the circuit reads each kind of stop of the table (scripts/etapes.mjs) on the stop <a>; a
|
|
3147
|
+
// kind it reads elsewhere (a developpeur's sending, the title on an answer, the description on a
|
|
3148
|
+
// decision) has none here.
|
|
3149
|
+
const LECTURES = {
|
|
3150
|
+
'meme-arret-sans-fait-nouveau': (ctx, a) => {
|
|
3151
|
+
const avant = ctx.relances.at(-1);
|
|
3152
|
+
return memeArret(avant, a) && avant.signature === signatureDe(ctx, a);
|
|
3153
|
+
},
|
|
3154
|
+
'a-repasser': (ctx, a) => a.nom === 'a-repasser',
|
|
3155
|
+
'a-arbitrer': (ctx, a) => a.faits?.genre === 'a-arbitrer',
|
|
3156
|
+
};
|
|
3157
|
+
|
|
3158
|
+
// The first line of the table of the stops that settles the stop <a>, in the table's order, or
|
|
3159
|
+
// null: the regulator decides.
|
|
3160
|
+
const ligneDeLArret = (ctx, a) => TABLE_DES_ARRETS.find(l => LECTURES[l.genre]?.(ctx, a)) ?? null;
|
|
3161
|
+
|
|
3162
|
+
// The decision the line <ligne> of the table, of gesture `testwriter`, gives for the component at
|
|
3163
|
+
// <i>, <contexte> its context: a RELANCE --depuis testwriter, as a regulator's (decisionDe), `ligne`
|
|
3164
|
+
// as the circuit writes it. The components after it that the circuit has carried through
|
|
3165
|
+
// (`atteints`) play nothing, the final pass judges them.
|
|
3166
|
+
function decisionDuGeste(ctx, ligne, i, contexte) {
|
|
3167
|
+
const composant = ctx.composants[i];
|
|
3168
|
+
const etape = ligne.geste === 'testwriter' ? TESTS : null;
|
|
3169
|
+
return {
|
|
3170
|
+
ligne: `RELANCE --depuis ${etape} ${composant}`,
|
|
3171
|
+
depuis: [{ etape, composant, contexte }],
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// Why the fact <f> of a regulator's sheet is false on the stop <a> and the state, or null when it
|
|
3176
|
+
// is true.
|
|
3177
|
+
function faitFaux(ctx, a, f) {
|
|
3178
|
+
const i = f.composant ? indexDe(ctx, f.composant) : -1;
|
|
3179
|
+
const chemin = i >= 0 ? ctx.composants[i] : f.composant;
|
|
3180
|
+
const titre = ctx.composants.join(', ');
|
|
3181
|
+
const rouges = a.faits?.rouges ?? [];
|
|
3182
|
+
const verifier = {
|
|
3183
|
+
'au-titre': () => [i >= 0, `le titre ne le nomme pas (${titre})`],
|
|
3184
|
+
'hors-titre': () => [i < 0, `le titre le nomme (${titre})`],
|
|
3185
|
+
'rouge-de': () => [
|
|
3186
|
+
rouges.includes(chemin),
|
|
3187
|
+
`les rouges de l'arrêt sont à ${rouges.join(', ') || 'aucun composant'}`,
|
|
3188
|
+
],
|
|
3189
|
+
'sans-changement': () => [
|
|
3190
|
+
i >= 0 && !livre(ctx, chemin),
|
|
3191
|
+
i >= 0 ? `la copie change ${chemin}` : `${f.composant} n'est pas un composant du titre`,
|
|
3192
|
+
],
|
|
3193
|
+
'sous-charge': () => [
|
|
3194
|
+
a.faits?.aRepasser === true,
|
|
3195
|
+
"aucune commande de l'arrêt n'est à repasser : son rouge ne dépend pas d'une durée sous charge",
|
|
3196
|
+
],
|
|
3197
|
+
};
|
|
3198
|
+
const [vrai, pourquoi] = verifier[f.sujet]();
|
|
3199
|
+
return vrai
|
|
3200
|
+
? null
|
|
3201
|
+
: `le fait « ${f.sujet}${f.composant ? ` ${f.composant}` : ''} » est faux : ${pourquoi}`;
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// Why the circuit refuses the decision <d> of the regulator's sheet <fiche> on the stop <a>,
|
|
3205
|
+
// checked on the state before it is applied (an admission control), or null when it admits it,
|
|
3206
|
+
// each component of a RELANCE --depuis judged: a component out of the title, or named twice, a
|
|
3207
|
+
// step the state does not hold, a developpeur before his testwriter, a criterion added and no
|
|
3208
|
+
// testwriter first (the table's `critere-neuf`), a fact of its sheet the state proves false, the
|
|
3209
|
+
// same RELANCE as the one before on the same stop with no new fact.
|
|
3210
|
+
function refusDe(ctx, a, fiche, d) {
|
|
3211
|
+
if (!d) {
|
|
3212
|
+
const hors = fiche.depuis.filter(x => indexDe(ctx, x.composant) < 0).map(x => x.composant);
|
|
3213
|
+
return `sa décision nomme ${hors.join(', ')}, qui n'${hors.length > 1 ? 'en sont' : 'est'} pas un composant du titre (${ctx.composants.join(', ')})`;
|
|
3214
|
+
}
|
|
3215
|
+
const deuxFois = (d.depuis ?? []).find(
|
|
3216
|
+
(x, n, l) => l.findIndex(y => y.composant === x.composant) < n
|
|
3217
|
+
);
|
|
3218
|
+
if (deuxFois) {
|
|
3219
|
+
return `sa décision nomme ${deuxFois.composant} deux fois : un composant se relance depuis une seule étape`;
|
|
3220
|
+
}
|
|
3221
|
+
const faux = (fiche.faits ?? []).map(f => faitFaux(ctx, a, f)).find(Boolean);
|
|
3222
|
+
if (faux) return faux;
|
|
3223
|
+
const plein = k => Boolean(String(fiche[k] ?? '').trim());
|
|
3224
|
+
if (d.question && (plein('arbitrage') || plein('critere'))) {
|
|
3225
|
+
return `une MONTE laisse la décision au superviseur : son arbitrage et son critère vont dans sa question`;
|
|
3226
|
+
}
|
|
3227
|
+
// The run starts at the first component of a --depuis, in the order of the title.
|
|
3228
|
+
if (plein('critere') && d.depuis?.[0].etape !== TESTS) {
|
|
3229
|
+
return `un critère ajouté part au testwriter avant le développeur (table des arrêts, ${gesteDe('critere-neuf').genre}), par RELANCE --depuis testwriter <composant>`;
|
|
3230
|
+
}
|
|
3231
|
+
if (d.question) return null;
|
|
3232
|
+
if (d.reprendre && !etapeDuCircuit(a.etat)) {
|
|
3233
|
+
return `RELANCE --reprendre reprend l'état à l'étape ${a.etat.etape}, qui n'est pas une étape du circuit : la décision nomme son agent (RELANCE --depuis)`;
|
|
3234
|
+
}
|
|
3235
|
+
const avantSesTests = (d.depuis ?? []).find(
|
|
3236
|
+
x => x.etape === CODE && !testwriterPasse(ctx, a.etat, ctx.composants.indexOf(x.composant))
|
|
3237
|
+
);
|
|
3238
|
+
if (avantSesTests) {
|
|
3239
|
+
const { composant } = avantSesTests;
|
|
3240
|
+
return `RELANCE --depuis developpeur ${composant} lance le développeur de ${composant} avant son testwriter, dont les tests ne sont pas écrits : un composant commence par son testwriter (RELANCE --depuis testwriter ${composant})`;
|
|
3241
|
+
}
|
|
3242
|
+
// The run starts at the first component of a --depuis, in the order of the title.
|
|
3243
|
+
const testwriter = d.depuis?.[0].etape === TESTS || (d.reprendre && a.etat.etape === TESTS);
|
|
3244
|
+
if (a.ticket && !testwriter && a.ticket !== empreinteDuTicket(ctx)) {
|
|
3245
|
+
return `la description du ticket a changé pendant l'arbitrage : un critère neuf part au testwriter avant le développeur (table des arrêts, ${gesteDe('critere-neuf').genre}), par RELANCE --depuis testwriter <composant>`;
|
|
3246
|
+
}
|
|
3247
|
+
const avant = ctx.relances.at(-1);
|
|
3248
|
+
const connu = f => (avant?.faits ?? []).some(g => JSON.stringify(g) === JSON.stringify(f));
|
|
3249
|
+
if (memeArret(avant, a) && avant.decision === d.ligne && (fiche.faits ?? []).every(connu)) {
|
|
3250
|
+
return `la même RELANCE que la précédente sur cet arrêt (${d.ligne.split('\n')[0]}), sans fait nouveau : elle a laissé l'arrêt tel qu'il était`;
|
|
3251
|
+
}
|
|
3252
|
+
return null;
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
// The decision <d> on the stop <a> applied: kept among the RELANCE in a row on that stop, with the
|
|
3256
|
+
// facts <faits> of its sheet and the stop's fingerprint, then the run that starts again, from the
|
|
3257
|
+
// state <etat> of the stop for a RELANCE --reprendre.
|
|
3258
|
+
function appliquer(ctx, a, d, etat, faits = []) {
|
|
3259
|
+
const entree = { ...a, decision: d.ligne, faits, signature: signatureDe(ctx, a) };
|
|
3260
|
+
ctx.relances = memeArret(ctx.relances[0], a) ? [...ctx.relances, entree] : [entree];
|
|
3261
|
+
ctx.reprendre = Boolean(d.reprendre);
|
|
3262
|
+
ctx.depuis = d.depuis ?? null;
|
|
3263
|
+
ctx.contexte = d.contexte ?? null;
|
|
3264
|
+
return { depart: depart(ctx, { ...etat, renvois: ctx.renvois }) };
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
// The stop <a> (`{ code, nom, rapport, faits, etat }`, `etat` the state of the step it stopped)
|
|
3268
|
+
// settled by a session of the regulator: a fresh one, or, with <reponse> (the supervisor's answer
|
|
3269
|
+
// to its question), its session `a.seance` resumed. A fresh session's decision is questioned first
|
|
3270
|
+
// (questionnerLaDecision): the regulator answers in its session, and the decision it gives at the
|
|
3271
|
+
// end is the one checked; a questioning that does not converge ends on « question », its
|
|
3272
|
+
// question for the supervisor. Its decision is checked on the state before it
|
|
3273
|
+
// is applied (refusDe); a decision refused asks the same session again once, the reason given, and
|
|
3274
|
+
// a second refusal ends the circuit on the stop. `{ code }` when the circuit ends: on « question »,
|
|
3275
|
+
// or on the stop itself for an answer in no form of decision, a decision refused twice or a session
|
|
3276
|
+
// in error; `{ depart }` for a RELANCE, where the run starts again.
|
|
3277
|
+
async function trancher(ctx, a, reponse = null) {
|
|
3278
|
+
const seance = reponse === null ? randomUUID() : a.seance;
|
|
3279
|
+
const garde = {
|
|
3280
|
+
seance,
|
|
3281
|
+
arret: a.nom,
|
|
3282
|
+
code: a.code,
|
|
3283
|
+
rapport: a.rapport,
|
|
3284
|
+
faits: a.faits ?? {},
|
|
3285
|
+
ticket: a.ticket ?? null,
|
|
3286
|
+
signature: a.signature ?? null,
|
|
3287
|
+
};
|
|
3288
|
+
const { composant } = a.etat;
|
|
3289
|
+
// The same stop come back after a RELANCE counts its pass at the arbitrage.
|
|
3290
|
+
const passe = memeArret(ctx.relances[0], a) ? ctx.relances.length + 1 : 1;
|
|
3291
|
+
const arret = {
|
|
3292
|
+
...a.etat,
|
|
3293
|
+
code: a.code,
|
|
3294
|
+
raison: a.nom,
|
|
3295
|
+
rapport: a.rapport,
|
|
3296
|
+
boucles: { arret: passe },
|
|
3297
|
+
};
|
|
3298
|
+
ecrireEtat(ctx, { ...arret, regulateur: garde });
|
|
3299
|
+
const valeurs = {
|
|
3300
|
+
depot: ctx.racine,
|
|
3301
|
+
ticket: ctx.ticket,
|
|
3302
|
+
sujet: ctx.sujet,
|
|
3303
|
+
arret: a.nom,
|
|
3304
|
+
code: a.code,
|
|
3305
|
+
// The step by its number and title, « 2.3 Code ».
|
|
3306
|
+
etape: ouDansLeCircuit(a.etat),
|
|
3307
|
+
composant,
|
|
3308
|
+
raison: String(a.motif ?? a.rapport ?? '').trim() || 'aucune',
|
|
3309
|
+
// The reds of the stop, read in the structured results, each with its component.
|
|
3310
|
+
rouges: a.rouges?.length
|
|
3311
|
+
? a.rouges
|
|
3312
|
+
.map(r =>
|
|
3313
|
+
[`- ${r.composant} : ${r.texte}`, ...(r.detail ? ['', r.detail, ''] : [])].join('\n')
|
|
3314
|
+
)
|
|
3315
|
+
.join('\n')
|
|
3316
|
+
: 'aucun',
|
|
3317
|
+
etat: ctx.etat,
|
|
3318
|
+
journal: ctx.journal,
|
|
3319
|
+
reponse,
|
|
3320
|
+
// The plan it judges on: the ticket's description, read now, and on the supervisor's answer
|
|
3321
|
+
// what the answer made of it (verserAuPlan).
|
|
3322
|
+
description: leTicket(ctx).description.trim() || 'aucune',
|
|
3323
|
+
plan: ctx.plan ?? 'inchangé',
|
|
3324
|
+
};
|
|
3325
|
+
const options = {
|
|
3326
|
+
horsEnveloppe: true,
|
|
3327
|
+
droits: horsDeLEnveloppe(ctx, REGULATEUR, 'sonnet'),
|
|
3328
|
+
id: seance,
|
|
3329
|
+
};
|
|
3330
|
+
// Its sheet, read as any session's: one that is absent or out of its schema resumes the session
|
|
3331
|
+
// once, asking for it; twice none, or a session that cannot end, leaves no decision.
|
|
3332
|
+
let d = null;
|
|
3333
|
+
let fiche = null;
|
|
3334
|
+
let pourquoi = null;
|
|
3335
|
+
const refus = [];
|
|
3336
|
+
try {
|
|
3337
|
+
fiche = await jusquALaFiche(
|
|
3338
|
+
ctx,
|
|
3339
|
+
REGULATEUR,
|
|
3340
|
+
composant,
|
|
3341
|
+
consigne(valeurs, reponse === null ? 2 : 3),
|
|
3342
|
+
{ ...options, premiere: reponse === null ? 'ouvre' : 'reprend' }
|
|
3343
|
+
);
|
|
3344
|
+
// Its decision questioned before the admission control, save on the supervisor's answer.
|
|
3345
|
+
const q =
|
|
3346
|
+
reponse === null
|
|
3347
|
+
? await questionnerLaDecision(ctx, a, fiche, {
|
|
3348
|
+
options,
|
|
3349
|
+
etat: { ...arret, regulateur: garde },
|
|
3350
|
+
valeurs,
|
|
3351
|
+
})
|
|
3352
|
+
: { fiche, question: null };
|
|
3353
|
+
fiche = q.fiche;
|
|
3354
|
+
// A questioning that does not converge goes up to the supervisor, as the regulator's question.
|
|
3355
|
+
if (q.question) d = { ligne: `MONTE : ${q.question}`, question: q.question };
|
|
3356
|
+
while (!d && !pourquoi) {
|
|
3357
|
+
d = decisionDe(ctx, fiche);
|
|
3358
|
+
const r = refusDe(ctx, a, fiche, d);
|
|
3359
|
+
if (!r) break;
|
|
3360
|
+
d = null;
|
|
3361
|
+
refus.push(r);
|
|
3362
|
+
commenter(ctx, composant, REGULATEUR, `décision refusée — ${r}`);
|
|
3363
|
+
if (refus.length > 1) {
|
|
3364
|
+
pourquoi = `sa décision, vérifiée sur l'état, est refusée deux fois : ${refus.join(' ; ')}`;
|
|
3365
|
+
break;
|
|
3366
|
+
}
|
|
3367
|
+
fiche = await jusquALaFiche(
|
|
3368
|
+
ctx,
|
|
3369
|
+
REGULATEUR,
|
|
3370
|
+
composant,
|
|
3371
|
+
consigne({ ticket: ctx.ticket, sujet: ctx.sujet, refus: r }, 6),
|
|
3372
|
+
{ ...options, premiere: 'reprend' }
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
} catch (e) {
|
|
3376
|
+
if (!(e instanceof Arret)) throw e;
|
|
3377
|
+
pourquoi = e.rapport;
|
|
3378
|
+
}
|
|
3379
|
+
if (!d) {
|
|
3380
|
+
ecrireEtat(ctx, {
|
|
3381
|
+
...arret,
|
|
3382
|
+
rapport: `${String(a.rapport ?? '').trim()}\n\nLE RÉGULATEUR ne rend pas de décision : ${pourquoi}`,
|
|
3383
|
+
});
|
|
3384
|
+
process.stderr.write(
|
|
3385
|
+
`⛔ Circuit de ${ctx.ticket} arrêté : ${a.nom} ; le régulateur ne rend pas de décision.\n`
|
|
3386
|
+
);
|
|
3387
|
+
return { code: a.code };
|
|
3388
|
+
}
|
|
3389
|
+
commenter(ctx, composant, REGULATEUR, d.ligne);
|
|
3390
|
+
if (d.question) {
|
|
3391
|
+
ctx.relances = [];
|
|
3392
|
+
ecrireEtat(ctx, {
|
|
3393
|
+
...arret,
|
|
3394
|
+
code: ARRETS.question,
|
|
3395
|
+
raison: 'question',
|
|
3396
|
+
rapport: d.question,
|
|
3397
|
+
regulateur: garde,
|
|
3398
|
+
});
|
|
3399
|
+
process.stderr.write(
|
|
3400
|
+
`⛔ Circuit de ${ctx.ticket} arrêté : question du régulateur au superviseur, qui y répond ou la porte au responsable ; la réponse se passe par \`npx grillhouse circuit ${ctx.ticket} --reponse "<texte>"\`.\n`
|
|
3401
|
+
);
|
|
3402
|
+
return { code: ARRETS.question };
|
|
3403
|
+
}
|
|
3404
|
+
ecrireLArbitrage(ctx, composant, fiche);
|
|
3405
|
+
return appliquer(ctx, a, d, arret, fiche.faits ?? []);
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// What the regulator's sheet <fiche> settled alone, written by the circuit: its `arbitrage` at the
|
|
3409
|
+
// ticket (`Circuit · <composant> · regulateur : arbitrage — <texte>`, which
|
|
3410
|
+
// scripts/consommateurs-arbitres.mjs reads), and its `critere` at the end of the description, the
|
|
3411
|
+
// criteria's place, numbered after its last numbered line.
|
|
3412
|
+
function ecrireLArbitrage(ctx, composant, fiche) {
|
|
3413
|
+
const arbitrage = String(fiche.arbitrage ?? '').trim();
|
|
3414
|
+
if (arbitrage) commenter(ctx, composant, REGULATEUR, `arbitrage — ${arbitrage}`);
|
|
3415
|
+
const critere = String(fiche.critere ?? '').trim();
|
|
3416
|
+
if (!critere) return;
|
|
3417
|
+
const description = leTicket(ctx).description.trimEnd();
|
|
3418
|
+
const n = Number([...description.matchAll(/^(\d+)\.\s/gm)].at(-1)?.[1] ?? 0) + 1;
|
|
3419
|
+
const r = ctx.bd('update', ctx.ticket, '-d', `${description}\n${n}. ${critere}\n`);
|
|
3420
|
+
if (r.status !== 0) {
|
|
3421
|
+
commenter(ctx, composant, REGULATEUR, `critère ${n} non écrit : ${r.stderr}`);
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
// The stop <a> ended for the supervisor by the line <ligne> of the table: the same stop come back
|
|
3426
|
+
// with no new fact. Its exit code.
|
|
3427
|
+
function rendreAuSuperviseur(ctx, a, ligne) {
|
|
3428
|
+
const decisions = ctx.relances.map((x, i) => `${i + 1}. ${x.decision}`).join('\n\n');
|
|
3429
|
+
ecrireEtat(ctx, {
|
|
3430
|
+
...a.etat,
|
|
3431
|
+
code: a.code,
|
|
3432
|
+
raison: a.nom,
|
|
3433
|
+
rapport: `${String(a.rapport ?? '').trim()}\n\nLE MÊME ARRÊT revient sans fait nouveau, la copie et la description du ticket telles qu'à la décision d'avant : le circuit s'arrête, pour le superviseur (table des arrêts, ${ligne.genre}). Les décisions sur cet arrêt :\n\n${decisions}`,
|
|
3434
|
+
});
|
|
3435
|
+
commenter(
|
|
3436
|
+
ctx,
|
|
3437
|
+
a.etat.composant,
|
|
3438
|
+
'arbitrage',
|
|
3439
|
+
`${ligne.genre} → le superviseur : l'arrêt ${a.nom} revient sans fait nouveau.`
|
|
3440
|
+
);
|
|
3441
|
+
return a.code;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
// The run from <suite> on: each stop other than 0 read in the table of the stops (scripts/
|
|
3445
|
+
// etapes.mjs), whose gesture the circuit applies itself, or settled by the regulator, whose
|
|
3446
|
+
// RELANCE runs the circuit again, until the circuit ends; its exit code. The stop that follows the
|
|
3447
|
+
// third RELANCE in a row on it ends the circuit without a regulator.
|
|
3448
|
+
async function conduire(ctx, suite) {
|
|
3449
|
+
for (;;) {
|
|
3450
|
+
if (suite.code !== undefined) return suite.code;
|
|
3451
|
+
const a = await derouler(ctx, suite.depart);
|
|
3452
|
+
if (a.code === 0) return a.code;
|
|
3453
|
+
// The description and the copy as the stop finds them: a decision is checked against the
|
|
3454
|
+
// first, and the stop that comes back against both.
|
|
3455
|
+
a.ticket = empreinteDuTicket(ctx);
|
|
3456
|
+
signatureDe(ctx, a);
|
|
3457
|
+
const ligne = ligneDeLArret(ctx, a);
|
|
3458
|
+
if (ligne?.geste === 'a-repasser') return a.code;
|
|
3459
|
+
if (ligne?.geste === 'superviseur') return rendreAuSuperviseur(ctx, a, ligne);
|
|
3460
|
+
if (ctx.relances.length >= RELANCES_DU_REGULATEUR && memeArret(ctx.relances[0], a)) {
|
|
3461
|
+
const decisions = ctx.relances.map((x, i) => `${i + 1}. ${x.decision}`).join('\n\n');
|
|
3462
|
+
ecrireEtat(ctx, {
|
|
3463
|
+
...a.etat,
|
|
3464
|
+
code: a.code,
|
|
3465
|
+
raison: a.nom,
|
|
3466
|
+
rapport: `${String(a.rapport ?? '').trim()}\n\nLE RÉGULATEUR a rendu ${ctx.relances.length} décisions RELANCE d'affilée sur cet arrêt : le circuit s'arrête au lieu d'une ${ctx.relances.length + 1}e relance. Ses décisions :\n\n${decisions}`,
|
|
3467
|
+
});
|
|
3468
|
+
commenter(
|
|
3469
|
+
ctx,
|
|
3470
|
+
a.etat.composant,
|
|
3471
|
+
REGULATEUR,
|
|
3472
|
+
`${ctx.relances.length} RELANCE d'affilée sur l'arrêt ${a.nom} : le circuit s'arrête.`
|
|
3473
|
+
);
|
|
3474
|
+
return a.code;
|
|
3475
|
+
}
|
|
3476
|
+
suite = await trancher(ctx, a);
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
// Where the run starts: the first component's testwriter, the first component of a --depuis in the
|
|
3481
|
+
// order of the title, at its step, or the step and component of the state <e> (null without one).
|
|
3482
|
+
// With <enCours>, the index of the component whose run sends it there, the renvois of that component
|
|
3483
|
+
// and of the components after it stay pending, as a queue of jobs grows and is never replaced: each
|
|
3484
|
+
// plays after it, its findings kept.
|
|
3485
|
+
function depart(ctx, e, enCours = null) {
|
|
3486
|
+
const fin = ctx.composants.length - 1;
|
|
3487
|
+
if (ctx.depuis) {
|
|
3488
|
+
const attente = Object.fromEntries(
|
|
3489
|
+
Object.entries(enCours === null ? {} : (ctx.renvois ?? {})).filter(
|
|
3490
|
+
([c]) => ctx.composants.indexOf(c) >= enCours
|
|
3491
|
+
)
|
|
3492
|
+
);
|
|
3493
|
+
ctx.renvois = {
|
|
3494
|
+
...attente,
|
|
3495
|
+
...Object.fromEntries(
|
|
3496
|
+
ctx.depuis.map(({ etape, composant, contexte }) => [
|
|
3497
|
+
composant,
|
|
3498
|
+
{
|
|
3499
|
+
developpeur: null,
|
|
3500
|
+
...attente[composant],
|
|
3501
|
+
etape,
|
|
3502
|
+
testwriter: null,
|
|
3503
|
+
contexte: contexte ?? null,
|
|
3504
|
+
},
|
|
3505
|
+
])
|
|
3506
|
+
),
|
|
3507
|
+
};
|
|
3508
|
+
const [{ etape, composant }] = [...ctx.depuis].sort(
|
|
3509
|
+
(a, b) => ctx.composants.indexOf(a.composant) - ctx.composants.indexOf(b.composant)
|
|
3510
|
+
);
|
|
3511
|
+
return {
|
|
3512
|
+
index: ctx.composants.indexOf(composant),
|
|
3513
|
+
etape,
|
|
3514
|
+
tentative: 1,
|
|
3515
|
+
rapport: null,
|
|
3516
|
+
dernier: fin,
|
|
3517
|
+
finale: false,
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
if (!ctx.reprendre) {
|
|
3521
|
+
// The run starts over: no component is reached yet, no lot verified.
|
|
3522
|
+
ctx.atteints = [];
|
|
3523
|
+
ctx.verifie = null;
|
|
3524
|
+
return { index: 0, etape: TESTS, tentative: 1, rapport: null, dernier: fin, finale: false };
|
|
3525
|
+
}
|
|
3526
|
+
if (!e) throw new Usage(`rien à reprendre : ${ctx.etat} n'existe pas.`);
|
|
3527
|
+
if (e.etape === 'integre')
|
|
3528
|
+
throw new Usage(`rien à reprendre : le lot de ${ctx.ticket} est intégré.`);
|
|
3529
|
+
const index = ctx.composants.indexOf(e.composant);
|
|
3530
|
+
if (index < 0)
|
|
3531
|
+
throw new Usage(`le composant de l'état, ${e.composant}, n'est plus dans le titre.`);
|
|
3532
|
+
const dernier = ctx.composants.indexOf(e.dernier);
|
|
3533
|
+
ctx.renvois = e.renvois ?? null;
|
|
3534
|
+
return {
|
|
3535
|
+
index,
|
|
3536
|
+
etape: e.etape,
|
|
3537
|
+
tentative: e.etape === CONTROLES ? 0 : 1,
|
|
3538
|
+
rapport: e.rapport,
|
|
3539
|
+
dernier: dernier < 0 ? fin : dernier,
|
|
3540
|
+
finale: e.finale === true,
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// The end of the run, its exit code <code>: an end on a stop kept with the circuit's results
|
|
3545
|
+
// (`montee`: its code and its stop), from which the bilan measures the supervisor's wait to the run
|
|
3546
|
+
// that answers it, then the ticket's bilan at the ticket (scripts/bilan.mjs); a bilan it cannot
|
|
3547
|
+
// write leaves the code as it is.
|
|
3548
|
+
function aLaFin(ctx, code) {
|
|
3549
|
+
try {
|
|
3550
|
+
if (code !== 0) {
|
|
3551
|
+
inscrire(ctx, {
|
|
3552
|
+
role: AUTEUR_DU_CIRCUIT,
|
|
3553
|
+
composant: ctx.courant?.composant ?? ctx.composants.at(-1),
|
|
3554
|
+
montee: { code, raison: ctx.courant?.raison ?? null },
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
const texte = ecrireLeBilan(ctx.racine, ctx.ticket);
|
|
3558
|
+
commenter(ctx, ctx.courant?.composant ?? ctx.composants.at(-1), 'bilan', texte);
|
|
3559
|
+
} catch (e) {
|
|
3560
|
+
process.stderr.write(`⚠️ bilan non écrit au ticket : ${e.message}\n`);
|
|
3561
|
+
}
|
|
3562
|
+
return code;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
// The questions <questions> of a questionneur's sheet, one per line, each with what it aims at.
|
|
3566
|
+
const lignesDesQuestions = questions =>
|
|
3567
|
+
(questions ?? []).map((q, i) => `${i + 1}. (${q.vise}) ${q.question}`).join('\n');
|
|
3568
|
+
|
|
3569
|
+
// The answers <reponses> of an author's sheet, one per line: CHANGE and what the proposal
|
|
3570
|
+
// becomes, or GARDE and why.
|
|
3571
|
+
const lignesDesReponses = reponses =>
|
|
3572
|
+
(reponses ?? []).length
|
|
3573
|
+
? reponses.map((r, i) => `${i + 1}. ${r.choix} — ${r.question} — ${r.texte}`).join('\n')
|
|
3574
|
+
: 'aucune réponse dans sa fiche';
|
|
3575
|
+
|
|
3576
|
+
// The rubric « ## <titre> » of the project's charter (CLAUDE.md of the main tree), up to the next
|
|
3577
|
+
// `##` heading, or what stands in its place.
|
|
3578
|
+
function rubrique(ctx, titre) {
|
|
3579
|
+
const lignes = lire(path.join(ctx.racine, 'CLAUDE.md')).split('\n');
|
|
3580
|
+
const i = lignes.findIndex(l => l.trim() === `## ${titre}`);
|
|
3581
|
+
if (i < 0) return 'absente de la charte du projet (CLAUDE.md)';
|
|
3582
|
+
const fin = lignes.findIndex((l, j) => j > i && l.startsWith('## '));
|
|
3583
|
+
return (
|
|
3584
|
+
lignes
|
|
3585
|
+
.slice(i + 1, fin < 0 ? undefined : fin)
|
|
3586
|
+
.join('\n')
|
|
3587
|
+
.trim() || 'vide'
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
// The meta-frame, the frame's plugin's, read at the plugin's root (scripts/cadre.mjs).
|
|
3592
|
+
const METACADRE = path.join(GREFFON, 'METACADRE.md');
|
|
3593
|
+
|
|
3594
|
+
// The questioning of a proposal by the questionneur, in rounds with its author, never side by
|
|
3595
|
+
// side: the questionneur asks (its sheet, `questions`), the author answers by resuming its own
|
|
3596
|
+
// session (<q>.repondre: its answers, whether its proposal changed and what it becomes), the
|
|
3597
|
+
// questionneur reads the answer by resuming its own, TOURS rounds at most. <q>: `{ objet, nom,
|
|
3598
|
+
// composant, etat, auteur, proposition, description, releve, empreinte, repondre }`, <objet> plan
|
|
3599
|
+
// or decision, <nom> the proposal in words, <etat> the state the questioning runs in, which it
|
|
3600
|
+
// names (`questionnement`, its round in `boucles`) while it runs and leaves as it was at its end.
|
|
3601
|
+
// Each question and each answer is written at the ticket, and the end is kept with the circuit's
|
|
3602
|
+
// results (`questionnement`, which the bilan counts, with what <empreinte> gives). `{ issue,
|
|
3603
|
+
// tours, change, reste }`: AUCUNE, a sheet without question; DÉSACCORD, the questionneur's; MONTE,
|
|
3604
|
+
// the last round still questioned; <reste> what remains for the supervisor. Throws the Arret of a
|
|
3605
|
+
// session that cannot end.
|
|
3606
|
+
async function questionner(ctx, q) {
|
|
3607
|
+
const seance = randomUUID();
|
|
3608
|
+
const options = {
|
|
3609
|
+
horsEnveloppe: true,
|
|
3610
|
+
droits: horsDeLEnveloppe(ctx, QUESTIONNEUR, 'sonnet'),
|
|
3611
|
+
id: seance,
|
|
3612
|
+
};
|
|
3613
|
+
const dire = texte => commenter(ctx, q.composant, 'questionnement', texte);
|
|
3614
|
+
let change = false;
|
|
3615
|
+
let reponses = null;
|
|
3616
|
+
let proposition = q.proposition;
|
|
3617
|
+
const fin = (issue, tours, reste = null) => {
|
|
3618
|
+
ecrireEtat(ctx, q.etat);
|
|
3619
|
+
inscrire(ctx, {
|
|
3620
|
+
role: AUTEUR_DU_CIRCUIT,
|
|
3621
|
+
composant: q.composant,
|
|
3622
|
+
questionnement: { objet: q.objet, tours, change, issue, ...(q.empreinte?.() ?? {}) },
|
|
3623
|
+
});
|
|
3624
|
+
return { issue, tours, change, reste };
|
|
3625
|
+
};
|
|
3626
|
+
for (let tour = 1; ; tour++) {
|
|
3627
|
+
ecrireEtat(ctx, {
|
|
3628
|
+
...q.etat,
|
|
3629
|
+
questionnement: { objet: q.objet, tour, seance },
|
|
3630
|
+
boucles: { ...q.etat.boucles, questionnement: tour },
|
|
3631
|
+
});
|
|
3632
|
+
const ou = `${q.nom}, tour ${tour}/${TOURS}`;
|
|
3633
|
+
const texte =
|
|
3634
|
+
tour === 1
|
|
3635
|
+
? consigne(
|
|
3636
|
+
{
|
|
3637
|
+
depot: ctx.racine,
|
|
3638
|
+
ticket: ctx.ticket,
|
|
3639
|
+
sujet: ctx.sujet,
|
|
3640
|
+
objet: q.nom,
|
|
3641
|
+
auteur: enMots(q.auteur),
|
|
3642
|
+
proposition,
|
|
3643
|
+
max: TOURS,
|
|
3644
|
+
metacadre: lire(METACADRE).trim(),
|
|
3645
|
+
arbitrer: rubrique(ctx, 'Comment on arbitre'),
|
|
3646
|
+
monte: rubrique(ctx, 'Ce qui monte au superviseur'),
|
|
3647
|
+
description: q.description.trim() || 'aucune',
|
|
3648
|
+
releve: q.releve || 'aucun',
|
|
3649
|
+
},
|
|
3650
|
+
7
|
|
3651
|
+
)
|
|
3652
|
+
: consigne(
|
|
3653
|
+
{
|
|
3654
|
+
ticket: ctx.ticket,
|
|
3655
|
+
sujet: ctx.sujet,
|
|
3656
|
+
tour,
|
|
3657
|
+
max: TOURS,
|
|
3658
|
+
auteur: enMots(q.auteur),
|
|
3659
|
+
reponses,
|
|
3660
|
+
proposition,
|
|
3661
|
+
},
|
|
3662
|
+
8
|
|
3663
|
+
);
|
|
3664
|
+
const fiche = await jusquALaFiche(ctx, QUESTIONNEUR, q.composant, texte, {
|
|
3665
|
+
...options,
|
|
3666
|
+
premiere: tour === 1 ? 'ouvre' : 'reprend',
|
|
3667
|
+
});
|
|
3668
|
+
if (fiche.issue === 'AUCUNE') {
|
|
3669
|
+
dire(
|
|
3670
|
+
`${ou} — aucune question au tour ${tour}/${TOURS} ; ${change ? 'la proposition a changé' : 'la proposition reste telle quelle'}.`
|
|
3671
|
+
);
|
|
3672
|
+
return fin('AUCUNE', tour);
|
|
3673
|
+
}
|
|
3674
|
+
if (fiche.issue === 'DÉSACCORD') {
|
|
3675
|
+
dire(`${ou} — désaccord du questionneur : ${fiche.desaccord}`);
|
|
3676
|
+
return fin('DÉSACCORD', tour, `Le désaccord du questionneur : ${fiche.desaccord}`);
|
|
3677
|
+
}
|
|
3678
|
+
const questions = lignesDesQuestions(fiche.questions);
|
|
3679
|
+
dire(`${ou} — les questions du questionneur :\n${questions}`);
|
|
3680
|
+
if (tour >= TOURS) {
|
|
3681
|
+
return fin(
|
|
3682
|
+
'MONTE',
|
|
3683
|
+
tour,
|
|
3684
|
+
`Les questions du questionneur au tour ${tour}/${TOURS}, sans réponse :\n${questions}`
|
|
3685
|
+
);
|
|
3686
|
+
}
|
|
3687
|
+
const r = await q.repondre(fiche.questions, tour);
|
|
3688
|
+
reponses = lignesDesReponses(r.reponses);
|
|
3689
|
+
dire(`${ou} — les réponses du ${enMots(q.auteur)} :\n${reponses}`);
|
|
3690
|
+
change ||= r.change;
|
|
3691
|
+
proposition = r.change ? r.proposition : 'inchangée depuis le tour d’avant';
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
// The fingerprint of a plan, its title and its description.
|
|
3696
|
+
const empreinteDuPlan = ({ titre, description }) =>
|
|
3697
|
+
createHash('sha256').update(`${titre}\n${description}`).digest('hex');
|
|
3698
|
+
|
|
3699
|
+
// Whether the plan of fingerprint <empreinte> has passed its questioning: a questioning that
|
|
3700
|
+
// ended without question on it, or the supervisor's answer to its stop.
|
|
3701
|
+
const planPasse = (ctx, empreinte) =>
|
|
3702
|
+
lesFiches(ctx).some(
|
|
3703
|
+
e =>
|
|
3704
|
+
e.role === AUTEUR_DU_CIRCUIT &&
|
|
3705
|
+
((e.questionnement?.objet === 'plan' &&
|
|
3706
|
+
e.questionnement.issue === 'AUCUNE' &&
|
|
3707
|
+
e.questionnement.empreinte === empreinte) ||
|
|
3708
|
+
e.plan?.empreinte === empreinte)
|
|
3709
|
+
);
|
|
3710
|
+
|
|
3711
|
+
// The plan as the technical referent's sheet <fiche> changes it, written to the ticket by the
|
|
3712
|
+
// circuit (`bd update --title`, `-d`), never by the session: the changes made, in words. A title
|
|
3713
|
+
// whose components the repository does not hold stays as it was, said at the ticket.
|
|
3714
|
+
function changerLePlan(ctx, fiche, composant) {
|
|
3715
|
+
const avant = leTicket(ctx);
|
|
3716
|
+
const faits = [];
|
|
3717
|
+
const ecrire = (option, valeur, quoi) => {
|
|
3718
|
+
const r = ctx.bd('update', ctx.ticket, option, valeur);
|
|
3719
|
+
if (r.status === 0) faits.push(quoi);
|
|
3720
|
+
else commenter(ctx, composant, 'questionnement', `${quoi} non écrit : ${r.stderr}`);
|
|
3721
|
+
return r.status === 0;
|
|
3722
|
+
};
|
|
3723
|
+
const titre = fiche.titre?.trim();
|
|
3724
|
+
if (titre && titre !== avant.titre.trim()) {
|
|
3725
|
+
try {
|
|
3726
|
+
const lu = ctx.duTitre(titre);
|
|
3727
|
+
if (ecrire('--title', titre, `le titre : « ${titre} »`)) Object.assign(ctx, lu);
|
|
3728
|
+
} catch (e) {
|
|
3729
|
+
if (!(e instanceof Usage)) throw e;
|
|
3730
|
+
commenter(
|
|
3731
|
+
ctx,
|
|
3732
|
+
composant,
|
|
3733
|
+
'questionnement',
|
|
3734
|
+
`titre neuf refusé, le titre reste : ${e.message}`
|
|
3735
|
+
);
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
const description = fiche.description?.trim();
|
|
3739
|
+
if (description && description !== avant.description.trim()) {
|
|
3740
|
+
ecrire('-d', fiche.description, 'la description');
|
|
3741
|
+
}
|
|
3742
|
+
return faits;
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
// The plan of the ticket questioned before the testwriter of the first component, after the survey
|
|
3746
|
+
// of its components (<releve>), or once an answer or a decision changed it (verserAuPlan): the
|
|
3747
|
+
// technical referent answers for it, a fresh session that defends the plan with the index and the
|
|
3748
|
+
// mature references, resumed from its second answer, or, with <auteur>, the referent's session
|
|
3749
|
+
// that changed it, resumed from its first; its changes are written to the ticket by the circuit.
|
|
3750
|
+
// <etat> the state the questioning runs in, the first testwriter's by default. A plan that has
|
|
3751
|
+
// passed its questioning, its title and description unchanged since, passes. Null when the run
|
|
3752
|
+
// goes on; else the exit code of the stop it ends on: « questionnement », for the supervisor, or
|
|
3753
|
+
// « composants-manquants » when the plan changed and the survey of its new text finds a component
|
|
3754
|
+
// its title misses.
|
|
3755
|
+
async function questionnerLePlan(ctx, releve, { etat: dans = null, auteur = null } = {}) {
|
|
3756
|
+
const t = leTicket(ctx);
|
|
3757
|
+
if (planPasse(ctx, empreinteDuPlan(t))) return null;
|
|
3758
|
+
const composant = dans?.composant ?? ctx.composants[0];
|
|
3759
|
+
const etat = dans ?? {
|
|
3760
|
+
etape: TESTS,
|
|
3761
|
+
composant,
|
|
3762
|
+
tentative: 1,
|
|
3763
|
+
code: null,
|
|
3764
|
+
raison: null,
|
|
3765
|
+
rapport: null,
|
|
3766
|
+
dernier: ctx.composants.at(-1),
|
|
3767
|
+
};
|
|
3768
|
+
const auteurDuPlan = auteur ?? randomUUID();
|
|
3769
|
+
let r;
|
|
3770
|
+
try {
|
|
3771
|
+
r = await questionner(ctx, {
|
|
3772
|
+
objet: 'plan',
|
|
3773
|
+
nom: 'le plan',
|
|
3774
|
+
composant,
|
|
3775
|
+
etat,
|
|
3776
|
+
auteur: REFERENT,
|
|
3777
|
+
proposition: `le plan du ticket : son titre, « ${t.titre} », sa description (LE TICKET, ci-dessous) et les composants qu'elle touche (LE RELEVÉ DES COMPOSANTS).`,
|
|
3778
|
+
description: t.description,
|
|
3779
|
+
releve,
|
|
3780
|
+
empreinte: () => ({ empreinte: empreinteDuPlan(leTicket(ctx)) }),
|
|
3781
|
+
repondre: async (questions, tour) => {
|
|
3782
|
+
const maintenant = leTicket(ctx);
|
|
3783
|
+
const ouvre = tour === 1 && !auteur;
|
|
3784
|
+
const texte = ouvre
|
|
3785
|
+
? consigne(
|
|
3786
|
+
{
|
|
3787
|
+
depot: ctx.racine,
|
|
3788
|
+
ticket: ctx.ticket,
|
|
3789
|
+
sujet: ctx.sujet,
|
|
3790
|
+
titre: maintenant.titre,
|
|
3791
|
+
description: maintenant.description.trim() || 'aucune',
|
|
3792
|
+
releve: releve || 'aucun',
|
|
3793
|
+
tour,
|
|
3794
|
+
max: TOURS,
|
|
3795
|
+
questions: lignesDesQuestions(questions),
|
|
3796
|
+
},
|
|
3797
|
+
10
|
|
3798
|
+
)
|
|
3799
|
+
: consigne(
|
|
3800
|
+
{
|
|
3801
|
+
ticket: ctx.ticket,
|
|
3802
|
+
sujet: ctx.sujet,
|
|
3803
|
+
tour,
|
|
3804
|
+
max: TOURS,
|
|
3805
|
+
questions: lignesDesQuestions(questions),
|
|
3806
|
+
forme:
|
|
3807
|
+
'chaque réponse dans `reponses` ; quand une réponse CHANGE le plan, le titre ou la description qu’il devient, entiers, dans `titre` ou `description`',
|
|
3808
|
+
},
|
|
3809
|
+
9
|
|
3810
|
+
);
|
|
3811
|
+
const f = await jusquALaFiche(ctx, REFERENT, composant, texte, {
|
|
3812
|
+
horsEnveloppe: true,
|
|
3813
|
+
droits: horsDeLEnveloppe(ctx, REFERENT, 'sonnet'),
|
|
3814
|
+
id: auteurDuPlan,
|
|
3815
|
+
premiere: ouvre ? 'ouvre' : 'reprend',
|
|
3816
|
+
});
|
|
3817
|
+
const faits = changerLePlan(ctx, f, composant);
|
|
3818
|
+
const neuf = leTicket(ctx);
|
|
3819
|
+
return {
|
|
3820
|
+
reponses: f.reponses,
|
|
3821
|
+
change: faits.length > 0,
|
|
3822
|
+
proposition: `le plan change (${faits.join(', ')}) ; son titre : « ${neuf.titre} » ; sa description :\n\n${neuf.description.trim()}`,
|
|
3823
|
+
};
|
|
3824
|
+
},
|
|
3825
|
+
});
|
|
3826
|
+
} catch (x) {
|
|
3827
|
+
// A session of the questioning that cannot end stops the circuit on its stop, before any
|
|
3828
|
+
// testwriter.
|
|
3829
|
+
if (!(x instanceof Arret)) throw x;
|
|
3830
|
+
ecrireEtat(ctx, { ...etat, code: ARRETS[x.nom], raison: x.nom, rapport: x.rapport });
|
|
3831
|
+
const ou = ouDansLeCircuit({ ...etat, questionnement: 'plan' });
|
|
3832
|
+
commenter(
|
|
3833
|
+
ctx,
|
|
3834
|
+
composant,
|
|
3835
|
+
'questionnement',
|
|
3836
|
+
`arrêt ${x.nom} à ${ou} — ${premiereLigne(x.rapport)}`
|
|
3837
|
+
);
|
|
3838
|
+
process.stderr.write(`⛔ Circuit de ${ctx.ticket} arrêté : ${x.nom}, à ${ou}.\n`);
|
|
3839
|
+
return ARRETS[x.nom];
|
|
3840
|
+
}
|
|
3841
|
+
if (r.issue === 'AUCUNE') {
|
|
3842
|
+
// A plan changed: its new text surveyed again, before any session.
|
|
3843
|
+
return r.change ? composantsManquants(ctx) : null;
|
|
3844
|
+
}
|
|
3845
|
+
const code = ARRETS.questionnement;
|
|
3846
|
+
const rapport = [
|
|
3847
|
+
`Le questionnement du plan ${r.issue === 'DÉSACCORD' ? 'finit sur un désaccord' : `reste questionné au tour ${TOURS}/${TOURS}`} : la décision revient au superviseur.`,
|
|
3848
|
+
'',
|
|
3849
|
+
r.reste,
|
|
3850
|
+
'',
|
|
3851
|
+
`L'échange entier est au ticket (\`Circuit · … · questionnement\`). Le superviseur change le plan (\`bd update ${ctx.ticket} …\`) ou le garde, puis répond : \`npx grillhouse circuit ${ctx.ticket} --reponse "<texte>"\`, qui fait passer le plan.`,
|
|
3852
|
+
].join('\n');
|
|
3853
|
+
ecrireEtat(ctx, {
|
|
3854
|
+
...etat,
|
|
3855
|
+
code,
|
|
3856
|
+
raison: 'questionnement',
|
|
3857
|
+
rapport,
|
|
3858
|
+
boucles: { questionnement: r.tours },
|
|
3859
|
+
});
|
|
3860
|
+
inscrire(ctx, { role: AUTEUR_DU_CIRCUIT, etape: TESTS, composant, arret: 'questionnement' });
|
|
3861
|
+
const ou = ouDansLeCircuit({ ...etat, raison: 'questionnement' });
|
|
3862
|
+
commenter(
|
|
3863
|
+
ctx,
|
|
3864
|
+
composant,
|
|
3865
|
+
'questionnement',
|
|
3866
|
+
`arrêt questionnement à ${ou} — ${premiereLigne(rapport)}`
|
|
3867
|
+
);
|
|
3868
|
+
process.stderr.write(
|
|
3869
|
+
`⛔ Circuit de ${ctx.ticket} arrêté : questionnement, à ${ou}.\n${rapport}\n`
|
|
3870
|
+
);
|
|
3871
|
+
return code;
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
// The decision of the regulator's sheet <fiche> as the circuit writes it, or its own words when it
|
|
3875
|
+
// names a component out of the title.
|
|
3876
|
+
const ligneDeLaDecision = (ctx, fiche) =>
|
|
3877
|
+
decisionDe(ctx, fiche)?.ligne ??
|
|
3878
|
+
`${fiche.decision} ${(fiche.depuis ?? []).map(x => `${x.role} ${x.composant}`).join(', ')}`.trim();
|
|
3879
|
+
|
|
3880
|
+
// The decision of the regulator's sheet <fiche> on the stop <a>, as the questionneur reads it: the
|
|
3881
|
+
// decision, its facts, then the stop it settles (<valeurs>: its step, component, reason and reds,
|
|
3882
|
+
// as the regulator received them).
|
|
3883
|
+
const laDecision = (ctx, a, fiche, valeurs) =>
|
|
3884
|
+
[
|
|
3885
|
+
`la décision du régulateur sur l'arrêt ${a.nom} (code ${a.code}), à l'étape ${valeurs.etape} du composant ${valeurs.composant} :`,
|
|
3886
|
+
'',
|
|
3887
|
+
ligneDeLaDecision(ctx, fiche),
|
|
3888
|
+
...((fiche.faits ?? []).length
|
|
3889
|
+
? [
|
|
3890
|
+
'',
|
|
3891
|
+
`Ses faits : ${fiche.faits.map(f => `${f.sujet}${f.composant ? ` ${f.composant}` : ''}`).join(', ')}.`,
|
|
3892
|
+
]
|
|
3893
|
+
: []),
|
|
3894
|
+
'',
|
|
3895
|
+
`LA RAISON DE L'ARRÊT, écrite par le circuit :\n\n${valeurs.raison}`,
|
|
3896
|
+
'',
|
|
3897
|
+
`LES ROUGES DE L'ARRÊT :\n\n${valeurs.rouges}`,
|
|
3898
|
+
].join('\n');
|
|
3899
|
+
|
|
3900
|
+
// The decision of the regulator's sheet <premiere> on the stop <a> questioned before the admission
|
|
3901
|
+
// control: the regulator answers in its own session, resumed (<options>), and may change its
|
|
3902
|
+
// decision; <etat> the state of the arbitrage, <valeurs> the regulator's instruction. `{ fiche,
|
|
3903
|
+
// question }`: the sheet of its last answer, and the question for the supervisor when the
|
|
3904
|
+
// questioning ends on a disagreement or on its last round still questioned, else null.
|
|
3905
|
+
async function questionnerLaDecision(ctx, a, premiere, { options, etat, valeurs }) {
|
|
3906
|
+
let fiche = premiere;
|
|
3907
|
+
const r = await questionner(ctx, {
|
|
3908
|
+
objet: 'decision',
|
|
3909
|
+
nom: 'la décision du régulateur',
|
|
3910
|
+
composant: valeurs.composant,
|
|
3911
|
+
etat,
|
|
3912
|
+
auteur: REGULATEUR,
|
|
3913
|
+
proposition: laDecision(ctx, a, fiche, valeurs),
|
|
3914
|
+
description: leTicket(ctx).description,
|
|
3915
|
+
releve: null,
|
|
3916
|
+
repondre: async (questions, tour) => {
|
|
3917
|
+
const avant = ligneDeLaDecision(ctx, fiche);
|
|
3918
|
+
fiche = await jusquALaFiche(
|
|
3919
|
+
ctx,
|
|
3920
|
+
REGULATEUR,
|
|
3921
|
+
valeurs.composant,
|
|
3922
|
+
consigne(
|
|
3923
|
+
{
|
|
3924
|
+
ticket: ctx.ticket,
|
|
3925
|
+
sujet: ctx.sujet,
|
|
3926
|
+
tour,
|
|
3927
|
+
max: TOURS,
|
|
3928
|
+
questions: lignesDesQuestions(questions),
|
|
3929
|
+
forme:
|
|
3930
|
+
'chaque réponse dans `reponses`, et ta décision, changée ou gardée, sous l’une des formes de ta consigne, ses `faits` vrais sur l’état',
|
|
3931
|
+
},
|
|
3932
|
+
9
|
|
3933
|
+
),
|
|
3934
|
+
{ ...options, premiere: 'reprend' }
|
|
3935
|
+
);
|
|
3936
|
+
const apres = ligneDeLaDecision(ctx, fiche);
|
|
3937
|
+
return {
|
|
3938
|
+
reponses: fiche.reponses,
|
|
3939
|
+
change: apres !== avant,
|
|
3940
|
+
proposition: laDecision(ctx, a, fiche, valeurs),
|
|
3941
|
+
};
|
|
3942
|
+
},
|
|
3943
|
+
});
|
|
3944
|
+
if (r.issue === 'AUCUNE') return { fiche, question: null };
|
|
3945
|
+
return {
|
|
3946
|
+
fiche,
|
|
3947
|
+
question: `Le questionnement de la décision du régulateur ${r.issue === 'DÉSACCORD' ? 'finit sur un désaccord' : `reste questionné au tour ${TOURS}/${TOURS}`} : la décision revient au superviseur.\n\nSa décision : ${ligneDeLaDecision(ctx, fiche)}\n\n${r.reste}\n\nL'échange entier est au ticket (\`Circuit · … · questionnement\`).`,
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
// The components the survey of the ticket finds and its title does not name
|
|
3952
|
+
// (scripts/composants-affectes.mjs), before the testwriter of the first component: none, null; else
|
|
3953
|
+
// the stop « composants-manquants », its code, before any session and without a regulator. The
|
|
3954
|
+
// survey's text is kept for the questioning of the plan (`ctx.releve`).
|
|
3955
|
+
function composantsManquants(ctx) {
|
|
3956
|
+
const { rapport, releve } = verifierLeTitre({
|
|
3957
|
+
racine: existsSync(ctx.copie) ? ctx.copie : ctx.racine,
|
|
3958
|
+
ticket: ctx.ticket,
|
|
3959
|
+
description: leTicket(ctx).description,
|
|
3960
|
+
titre: ctx.composants,
|
|
3961
|
+
fichier: `${ctx.copie}.composants.json`,
|
|
3962
|
+
});
|
|
3963
|
+
ctx.releve = releve;
|
|
3964
|
+
if (!rapport) return null;
|
|
3965
|
+
const [composant] = ctx.composants;
|
|
3966
|
+
const code = ARRETS['composants-manquants'];
|
|
3967
|
+
ecrireEtat(ctx, {
|
|
3968
|
+
etape: TESTS,
|
|
3969
|
+
composant,
|
|
3970
|
+
tentative: 1,
|
|
3971
|
+
code,
|
|
3972
|
+
raison: 'composants-manquants',
|
|
3973
|
+
rapport,
|
|
3974
|
+
dernier: ctx.composants.at(-1),
|
|
3975
|
+
});
|
|
3976
|
+
const ou = ouDansLeCircuit({ etape: TESTS, raison: 'composants-manquants' });
|
|
3977
|
+
commenter(
|
|
3978
|
+
ctx,
|
|
3979
|
+
composant,
|
|
3980
|
+
'composants',
|
|
3981
|
+
`arrêt composants-manquants à ${ou} — ${premiereLigne(rapport)}`
|
|
3982
|
+
);
|
|
3983
|
+
process.stderr.write(
|
|
3984
|
+
`⛔ Circuit de ${ctx.ticket} arrêté : composants-manquants, à ${ou}.\n${rapport}\n`
|
|
3985
|
+
);
|
|
3986
|
+
return code;
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
// What pours into the plan (verserAuPlan), by its kind: the supervisor's answer to the
|
|
3990
|
+
// regulator's question (`--reponse`), or a decision of the owner given in session, out of any stop
|
|
3991
|
+
// « question » (`--decision`). Each one's words in the technical referent's instruction (the
|
|
3992
|
+
// twelfth template: `objet`, `quoi`, `suite`), at the ticket (`nom`, `garde`) and on a session of
|
|
3993
|
+
// the referent that cannot end (`reste`).
|
|
3994
|
+
const VERSEMENTS = {
|
|
3995
|
+
reponse: {
|
|
3996
|
+
objet: 'la réponse du superviseur à la question du régulateur',
|
|
3997
|
+
quoi: "LA RÉPONSE DU SUPERVISEUR, mot pour mot (la sienne, ou celle du responsable qu'il porte)",
|
|
3998
|
+
suite: "puis le régulateur tranche l'arrêt sur le plan",
|
|
3999
|
+
nom: 'la réponse du superviseur',
|
|
4000
|
+
garde: 'le régulateur reprend sans questionnement',
|
|
4001
|
+
reste: "l'arrêt « question » reste, pour une autre réponse",
|
|
4002
|
+
},
|
|
4003
|
+
decision: {
|
|
4004
|
+
objet: 'une décision du responsable donnée en séance, hors de toute question du circuit',
|
|
4005
|
+
quoi: 'LA DÉCISION DU RESPONSABLE, mot pour mot (que le superviseur porte)',
|
|
4006
|
+
suite:
|
|
4007
|
+
'puis le circuit reprend là où il en était, et rejoue les seuls composants que le plan changé touche',
|
|
4008
|
+
nom: 'la décision du responsable',
|
|
4009
|
+
garde: 'le circuit reprend tel quel',
|
|
4010
|
+
reste: 'le circuit reste arrêté là où il en était, pour une autre décision',
|
|
4011
|
+
},
|
|
4012
|
+
};
|
|
4013
|
+
|
|
4014
|
+
// The text <texte> of the kind <genre> (VERSEMENTS), on the question <question>, poured into the
|
|
4015
|
+
// plan, as an RFC amended or an ADR: the plan is what every agent reads, the exchange at the
|
|
4016
|
+
// ticket its history. A fresh session of the technical referent, out of the envelope (the twelfth
|
|
4017
|
+
// template: the question, the text, the plan), changes the plan (a criterion added, removed or
|
|
4018
|
+
// made precise, its title) or keeps it, and the circuit writes the plan its sheet gives
|
|
4019
|
+
// (changerLePlan). A plan changed is surveyed again, then questioned (questionnerLePlan, the
|
|
4020
|
+
// referent answering in its own session, in the state <e>, the first testwriter's without one);
|
|
4021
|
+
// a plan the text leaves as it was is said at the ticket, with no questioning. `{ plan, avant }`,
|
|
4022
|
+
// the plan in words for the regulator's resumed session and the ticket before the text; `{ code }`
|
|
4023
|
+
// for the stop the run ends on: one of the survey or of the questioning, or the stop of a session
|
|
4024
|
+
// of the referent that cannot end, the state left as it was.
|
|
4025
|
+
async function verserAuPlan(ctx, e, { genre, texte, question }) {
|
|
4026
|
+
const v = VERSEMENTS[genre];
|
|
4027
|
+
const composant = e?.composant ?? ctx.composants[0];
|
|
4028
|
+
const avant = leTicket(ctx);
|
|
4029
|
+
const seance = randomUUID();
|
|
4030
|
+
const dire = t => commenter(ctx, composant, 'questionnement', t);
|
|
4031
|
+
let f;
|
|
4032
|
+
try {
|
|
4033
|
+
f = await jusquALaFiche(
|
|
4034
|
+
ctx,
|
|
4035
|
+
REFERENT,
|
|
4036
|
+
composant,
|
|
4037
|
+
consigne(
|
|
4038
|
+
{
|
|
4039
|
+
depot: ctx.racine,
|
|
4040
|
+
ticket: ctx.ticket,
|
|
4041
|
+
sujet: ctx.sujet,
|
|
4042
|
+
objet: v.objet,
|
|
4043
|
+
titre: avant.titre,
|
|
4044
|
+
description: avant.description.trim() || 'aucune',
|
|
4045
|
+
question: String(question ?? '').trim() || 'aucune',
|
|
4046
|
+
quoi: v.quoi,
|
|
4047
|
+
reponse: texte,
|
|
4048
|
+
suite: v.suite,
|
|
4049
|
+
},
|
|
4050
|
+
11
|
|
4051
|
+
),
|
|
4052
|
+
{
|
|
4053
|
+
horsEnveloppe: true,
|
|
4054
|
+
droits: horsDeLEnveloppe(ctx, REFERENT, 'sonnet'),
|
|
4055
|
+
id: seance,
|
|
4056
|
+
premiere: 'ouvre',
|
|
4057
|
+
}
|
|
4058
|
+
);
|
|
4059
|
+
} catch (x) {
|
|
4060
|
+
if (!(x instanceof Arret)) throw x;
|
|
4061
|
+
dire(`arrêt ${x.nom} : ${v.nom} n'entre pas dans le plan — ${premiereLigne(x.rapport)}`);
|
|
4062
|
+
process.stderr.write(
|
|
4063
|
+
`⛔ Circuit de ${ctx.ticket} arrêté : ${x.nom}, ${v.nom} n'entre pas dans le plan ; ${v.reste}.\n`
|
|
4064
|
+
);
|
|
4065
|
+
return { code: ARRETS[x.nom] };
|
|
4066
|
+
}
|
|
4067
|
+
const faits = changerLePlan(ctx, f, composant);
|
|
4068
|
+
if (!faits.length) {
|
|
4069
|
+
dire(`${v.nom} ne change pas le plan : ${lignesDesReponses(f.reponses)} ; ${v.garde}.`);
|
|
4070
|
+
return { plan: `inchangé, ${v.nom} ne change aucun critère`, avant };
|
|
4071
|
+
}
|
|
4072
|
+
dire(
|
|
4073
|
+
`${v.nom} entre dans le plan, par le référent technique (${faits.join(', ')}) :\n${lignesDesReponses(f.reponses)}`
|
|
4074
|
+
);
|
|
4075
|
+
const code =
|
|
4076
|
+
composantsManquants(ctx) ??
|
|
4077
|
+
(await questionnerLePlan(ctx, ctx.releve, { etat: e, auteur: seance }));
|
|
4078
|
+
if (code !== null) return { code };
|
|
4079
|
+
return { plan: `changé par ${v.nom} (${faits.join(', ')}), puis questionné`, avant };
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
// The steps of a component's own circuit, testwriter to checks, out of the final pass.
|
|
4083
|
+
const DU_COMPOSANT = [TESTS, PREUVE, CODE, CONTROLES];
|
|
4084
|
+
// The stops whose answer is `--reponse`: a decision given there is that answer.
|
|
4085
|
+
const A_REPONDRE = ['question', 'questionnement', 'composants-manquants'];
|
|
4086
|
+
// The steps of the integration: a lot that has reached them takes no decision more.
|
|
4087
|
+
const INTEGREE = [INTEGRATION, EN_FILE, 'integre'];
|
|
4088
|
+
|
|
4089
|
+
// The process <cible> (a process group as -<groupe>) stopped, as a cancelled job of GitLab CI:
|
|
4090
|
+
// SIGTERM, then SIGKILL when it still lives after ARRET_MS; true once it is gone.
|
|
4091
|
+
const ARRET_MS = 10_000;
|
|
4092
|
+
async function terminer(cible) {
|
|
4093
|
+
const vit = () => {
|
|
4094
|
+
try {
|
|
4095
|
+
process.kill(cible, 0);
|
|
4096
|
+
return true;
|
|
4097
|
+
} catch (x) {
|
|
4098
|
+
return x.code === 'EPERM';
|
|
4099
|
+
}
|
|
4100
|
+
};
|
|
4101
|
+
for (const signal of ['SIGTERM', 'SIGKILL']) {
|
|
4102
|
+
if (!vit()) return true;
|
|
4103
|
+
try {
|
|
4104
|
+
process.kill(cible, signal);
|
|
4105
|
+
} catch {
|
|
4106
|
+
return true;
|
|
4107
|
+
}
|
|
4108
|
+
const limite = Date.now() + ARRET_MS;
|
|
4109
|
+
while (vit() && Date.now() < limite) await new Promise(r => setTimeout(r, 200));
|
|
4110
|
+
}
|
|
4111
|
+
return !vit();
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
// The circuit running on the ticket, the process its state <e> names (`processus`), stopped before
|
|
4115
|
+
// a decision of the owner enters the plan, as GitLab CI cancels a pipeline a new one makes
|
|
4116
|
+
// redundant and Zuul the jobs of a change superseded, said at the ticket; then the session it led
|
|
4117
|
+
// (`seance`, its process group), which outlives it. The state as the stopped circuit leaves it,
|
|
4118
|
+
// its session gone, or null without one.
|
|
4119
|
+
async function arreterLeCircuit(ctx, e) {
|
|
4120
|
+
if (!e) return null;
|
|
4121
|
+
const pid = e.processus;
|
|
4122
|
+
if (Number.isInteger(pid) && pid !== process.pid && vivant(pid)) {
|
|
4123
|
+
commenter(
|
|
4124
|
+
ctx,
|
|
4125
|
+
e.composant ?? ctx.composants[0],
|
|
4126
|
+
'decision',
|
|
4127
|
+
`le circuit en cours (processus ${pid}) s'arrête, sa séance avec lui : la décision du responsable entre dans le plan, puis le circuit reprend là où il en était.`
|
|
4128
|
+
);
|
|
4129
|
+
if (!(await terminer(pid))) {
|
|
4130
|
+
throw new Error(`le circuit en cours (processus ${pid}) ne s'arrête pas.`);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
const lu = existsSync(ctx.etat) ? JSON.parse(readFileSync(ctx.etat, 'utf8')) : e;
|
|
4134
|
+
if (lu.seance && signalerGroupe(lu.seance, 0)) await terminer(-lu.seance);
|
|
4135
|
+
return { ...lu, seance: null };
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
// The components of the title that the change of the plan from <avant> (the ticket before the
|
|
4139
|
+
// decision, the title's components then <composantsDAvant>) reaches, in the title's order: the
|
|
4140
|
+
// survey of the components (scripts/composants-affectes.mjs), as at the opening, of the lines the
|
|
4141
|
+
// change adds or removes, with each component the title gained. A change the survey situates in
|
|
4142
|
+
// no component reaches them all, as a pipeline runs whole on a change it cannot scope.
|
|
4143
|
+
function composantsDuChangement(ctx, avant, composantsDAvant) {
|
|
4144
|
+
const lignes = t =>
|
|
4145
|
+
String(t)
|
|
4146
|
+
.split('\n')
|
|
4147
|
+
.map(l => l.trim())
|
|
4148
|
+
.filter(Boolean);
|
|
4149
|
+
const maintenant = leTicket(ctx).description;
|
|
4150
|
+
const a = new Set(lignes(avant.description));
|
|
4151
|
+
const b = new Set(lignes(maintenant));
|
|
4152
|
+
const change = [
|
|
4153
|
+
...lignes(maintenant).filter(l => !a.has(l)),
|
|
4154
|
+
...lignes(avant.description).filter(l => !b.has(l)),
|
|
4155
|
+
];
|
|
4156
|
+
const gagnes = ctx.composants.filter(c => !composantsDAvant.includes(c));
|
|
4157
|
+
if (!change.length) return gagnes;
|
|
4158
|
+
const r = releveDuTexte(
|
|
4159
|
+
existsSync(ctx.copie) ? ctx.copie : ctx.racine,
|
|
4160
|
+
`## Critères\n\n${change.join('\n')}`
|
|
4161
|
+
);
|
|
4162
|
+
const nommes = new Set(
|
|
4163
|
+
r.composants.flatMap(n => {
|
|
4164
|
+
try {
|
|
4165
|
+
return [ctx.resoudre(n)];
|
|
4166
|
+
} catch {
|
|
4167
|
+
return [];
|
|
4168
|
+
}
|
|
4169
|
+
})
|
|
4170
|
+
);
|
|
4171
|
+
if (!ctx.composants.some(c => nommes.has(c))) return [...ctx.composants];
|
|
4172
|
+
return ctx.composants.filter(c => nommes.has(c) || gagnes.includes(c));
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
// The state <e> the run resumes from once a decision changed the plan: where it was, save the
|
|
4176
|
+
// components of <touches> it had passed (carried through, before its component, or all of them at
|
|
4177
|
+
// the final pass and after) and its own component past its testwriter when the change reaches it,
|
|
4178
|
+
// each played again from its testwriter (`renvois`, the plan changed as its report), the run
|
|
4179
|
+
// starting at the first of them; its own component, the change not reaching it, then resumes at
|
|
4180
|
+
// its step. With none, the state as it is.
|
|
4181
|
+
function etatApresLaDecision(ctx, e, touches) {
|
|
4182
|
+
const i = ctx.composants.indexOf(e.composant);
|
|
4183
|
+
const auComposant = DU_COMPOSANT.includes(e.etape) && e.finale !== true;
|
|
4184
|
+
const passe = c => !auComposant || ctx.atteints.includes(c) || ctx.composants.indexOf(c) < i;
|
|
4185
|
+
const rejouer = touches.filter(c => passe(c) || (c === e.composant && e.etape !== TESTS));
|
|
4186
|
+
const dernier = ctx.composants.at(-1);
|
|
4187
|
+
if (!rejouer.length) return { ...e, dernier };
|
|
4188
|
+
const rapport =
|
|
4189
|
+
"Le plan du ticket a changé depuis la séance d'avant de ce composant, par une décision du responsable que le référent technique y a versée : relis sa description et ses critères ; tes tests suivent le plan tel qu'il est.";
|
|
4190
|
+
const renvois = { ...(e.renvois ?? {}) };
|
|
4191
|
+
for (const c of rejouer) {
|
|
4192
|
+
renvois[c] = {
|
|
4193
|
+
etape: TESTS,
|
|
4194
|
+
testwriter: rapport,
|
|
4195
|
+
developpeur: null,
|
|
4196
|
+
contexte: renvois[c]?.contexte ?? null,
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
const premier = Math.min(...rejouer.map(c => ctx.composants.indexOf(c)));
|
|
4200
|
+
if (auComposant && !rejouer.includes(e.composant) && premier < i) {
|
|
4201
|
+
renvois[e.composant] = {
|
|
4202
|
+
testwriter: null,
|
|
4203
|
+
developpeur: null,
|
|
4204
|
+
contexte: null,
|
|
4205
|
+
...renvois[e.composant],
|
|
4206
|
+
etape: e.etape,
|
|
4207
|
+
[e.etape]: e.rapport ?? null,
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
return {
|
|
4211
|
+
...e,
|
|
4212
|
+
etape: TESTS,
|
|
4213
|
+
composant: ctx.composants[premier],
|
|
4214
|
+
tentative: 1,
|
|
4215
|
+
rapport,
|
|
4216
|
+
finale: false,
|
|
4217
|
+
dernier,
|
|
4218
|
+
renvois,
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
export async function main(argv) {
|
|
4223
|
+
let ctx = null;
|
|
4224
|
+
try {
|
|
4225
|
+
ctx = contexte(lireArguments(argv));
|
|
4226
|
+
mkdirSync(path.dirname(ctx.copie), { recursive: true });
|
|
4227
|
+
let e = existsSync(ctx.etat) ? JSON.parse(readFileSync(ctx.etat, 'utf8')) : null;
|
|
4228
|
+
// A decision of the owner given in session, out of any stop « question »: the circuit running
|
|
4229
|
+
// on the ticket stops, the decision enters the plan by the path of an answer, then the run
|
|
4230
|
+
// resumes where it was, the components the plan changed reaches played again
|
|
4231
|
+
// (etatApresLaDecision); without a state, the run starts on the plan it leaves.
|
|
4232
|
+
if (ctx.decision !== null) {
|
|
4233
|
+
const integree = x =>
|
|
4234
|
+
INTEGREE.includes(x?.etape) &&
|
|
4235
|
+
new Usage(
|
|
4236
|
+
`--decision : le lot de ${ctx.ticket} est à l'intégration (${x.etape}) ; une décision neuve ouvre son ticket.`
|
|
4237
|
+
);
|
|
4238
|
+
if (A_REPONDRE.includes(e?.raison)) {
|
|
4239
|
+
throw new Usage(
|
|
4240
|
+
`--decision : l'arrêt « ${e.raison} » de ${ctx.ticket} attend sa réponse, --reponse "<texte>".`
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4243
|
+
if (integree(e)) throw integree(e);
|
|
4244
|
+
e = await arreterLeCircuit(ctx, e);
|
|
4245
|
+
if (integree(e)) throw integree(e);
|
|
4246
|
+
ctx.atteints = [...(e?.atteints ?? [])];
|
|
4247
|
+
ctx.verifie = e?.verifie ?? null;
|
|
4248
|
+
ctx.reprise = e?.reprise ?? null;
|
|
4249
|
+
ctx.renvois = e?.renvois ?? null;
|
|
4250
|
+
const titre = [...ctx.composants];
|
|
4251
|
+
const verse = await verserAuPlan(ctx, e, { genre: 'decision', texte: ctx.decision });
|
|
4252
|
+
if (verse.code !== undefined) return aLaFin(ctx, verse.code);
|
|
4253
|
+
if (e) {
|
|
4254
|
+
e = etatApresLaDecision(ctx, e, composantsDuChangement(ctx, verse.avant, titre));
|
|
4255
|
+
ctx.reprendre = true;
|
|
4256
|
+
}
|
|
4257
|
+
ctx.decision = null;
|
|
4258
|
+
}
|
|
4259
|
+
// The supervisor's answer to the questioning of the plan lets the plan pass, from the start.
|
|
4260
|
+
if (ctx.reponse !== null && e?.raison === 'questionnement') {
|
|
4261
|
+
commenter(
|
|
4262
|
+
ctx,
|
|
4263
|
+
ctx.composants[0],
|
|
4264
|
+
'questionnement',
|
|
4265
|
+
`le plan, réponse du superviseur : ${ctx.reponse}`
|
|
4266
|
+
);
|
|
4267
|
+
inscrire(ctx, {
|
|
4268
|
+
role: AUTEUR_DU_CIRCUIT,
|
|
4269
|
+
composant: ctx.composants[0],
|
|
4270
|
+
plan: { empreinte: empreinteDuPlan(leTicket(ctx)) },
|
|
4271
|
+
});
|
|
4272
|
+
ctx.reponse = null;
|
|
4273
|
+
}
|
|
4274
|
+
// The supervisor's answer to the components the title misses lets them pass, from the start.
|
|
4275
|
+
if (ctx.reponse !== null && e?.raison === 'composants-manquants') {
|
|
4276
|
+
const hors = repondre(`${ctx.copie}.composants.json`, ctx.reponse);
|
|
4277
|
+
const texte = `${hors.join(', ')} hors du titre, réponse du superviseur : ${ctx.reponse}`;
|
|
4278
|
+
commenter(ctx, ctx.composants[0], 'composants', texte);
|
|
4279
|
+
ctx.reponse = null;
|
|
4280
|
+
}
|
|
4281
|
+
if (ctx.reponse !== null && (e?.raison !== 'question' || !e?.regulateur?.seance)) {
|
|
4282
|
+
throw new Usage(
|
|
4283
|
+
`--reponse : aucune séance du régulateur gardée dans ${ctx.etat} ; elle suit un arrêt « question ».`
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
// The components the runs before carried through, the lot the whole final pass verified, and
|
|
4287
|
+
// the session a relaunch resumes.
|
|
4288
|
+
ctx.atteints = [...(e?.atteints ?? [])];
|
|
4289
|
+
ctx.verifie = e?.verifie ?? null;
|
|
4290
|
+
ctx.reprise = e?.reprise ?? null;
|
|
4291
|
+
let d = ctx.reponse === null ? depart(ctx, e) : null;
|
|
4292
|
+
// A session a dead circuit left running ends before this run launches its own.
|
|
4293
|
+
await attendreLaSeance(ctx, e?.seance);
|
|
4294
|
+
if (ctx.reponse === null) {
|
|
4295
|
+
// From the first testwriter: the survey of the components, then the questioning of the plan,
|
|
4296
|
+
// which may change the title, and so where the run starts.
|
|
4297
|
+
if (d.index === 0 && d.etape === TESTS) {
|
|
4298
|
+
// The survey names its start in the state while it runs (`releve`), which the board reads.
|
|
4299
|
+
ecrireEtat(ctx, {
|
|
4300
|
+
etape: TESTS,
|
|
4301
|
+
composant: ctx.composants[0],
|
|
4302
|
+
tentative: 1,
|
|
4303
|
+
code: null,
|
|
4304
|
+
raison: null,
|
|
4305
|
+
rapport: null,
|
|
4306
|
+
dernier: ctx.composants.at(-1),
|
|
4307
|
+
releve: new Date().toISOString(),
|
|
4308
|
+
});
|
|
4309
|
+
const manque = composantsManquants(ctx);
|
|
4310
|
+
if (manque === null) ecrireEtat(ctx, { ...ctx.courant, releve: null });
|
|
4311
|
+
const arret = manque ?? (await questionnerLePlan(ctx, ctx.releve));
|
|
4312
|
+
if (arret !== null) return aLaFin(ctx, arret);
|
|
4313
|
+
d = depart(ctx, e);
|
|
4314
|
+
}
|
|
4315
|
+
return aLaFin(ctx, await conduire(ctx, { depart: d }));
|
|
4316
|
+
}
|
|
4317
|
+
// The supervisor's answer enters the plan first; then it goes to the regulator's session kept
|
|
4318
|
+
// on the stop it settles, which judges on the plan.
|
|
4319
|
+
ctx.renvois = e.renvois ?? null;
|
|
4320
|
+
const verse = await verserAuPlan(ctx, e, {
|
|
4321
|
+
genre: 'reponse',
|
|
4322
|
+
texte: ctx.reponse,
|
|
4323
|
+
question: e.rapport,
|
|
4324
|
+
});
|
|
4325
|
+
if (verse.code !== undefined) return aLaFin(ctx, verse.code);
|
|
4326
|
+
ctx.plan = verse.plan;
|
|
4327
|
+
const arret = {
|
|
4328
|
+
code: e.regulateur.code,
|
|
4329
|
+
nom: e.regulateur.arret,
|
|
4330
|
+
rapport: e.regulateur.rapport,
|
|
4331
|
+
faits: e.regulateur.faits ?? {},
|
|
4332
|
+
ticket: e.regulateur.ticket ?? null,
|
|
4333
|
+
signature: e.regulateur.signature ?? undefined,
|
|
4334
|
+
seance: e.regulateur.seance,
|
|
4335
|
+
etat: {
|
|
4336
|
+
etape: e.etape,
|
|
4337
|
+
composant: e.composant,
|
|
4338
|
+
tentative: e.tentative,
|
|
4339
|
+
code: null,
|
|
4340
|
+
raison: null,
|
|
4341
|
+
rapport: e.regulateur.rapport,
|
|
4342
|
+
dernier: e.dernier,
|
|
4343
|
+
finale: e.finale === true,
|
|
4344
|
+
},
|
|
4345
|
+
};
|
|
4346
|
+
// A component the title gained since the question: the gesture of the table, its testwriter, the
|
|
4347
|
+
// stop as its context (the answer is in the plan), with no regulator resumed.
|
|
4348
|
+
const ajoute = ctx.composants.find(c => !(e.composants ?? ctx.composants).includes(c));
|
|
4349
|
+
if (ajoute) {
|
|
4350
|
+
const ligne = gesteDe('composant-ajoute');
|
|
4351
|
+
const i = ctx.composants.indexOf(ajoute);
|
|
4352
|
+
const decision = decisionDuGeste(ctx, ligne, i, e.rapport);
|
|
4353
|
+
commenter(
|
|
4354
|
+
ctx,
|
|
4355
|
+
e.composant,
|
|
4356
|
+
'arbitrage',
|
|
4357
|
+
`${ligne.genre} → ${decision.ligne} (table des arrêts, sans régulateur).`
|
|
4358
|
+
);
|
|
4359
|
+
return aLaFin(ctx, await conduire(ctx, appliquer(ctx, arret, decision, arret.etat)));
|
|
4360
|
+
}
|
|
4361
|
+
return aLaFin(ctx, await conduire(ctx, await trancher(ctx, arret, ctx.reponse)));
|
|
4362
|
+
} catch (e) {
|
|
4363
|
+
if (!(e instanceof Usage)) throw e;
|
|
4364
|
+
process.stderr.write(
|
|
4365
|
+
`usage: npx grillhouse circuit <ticket> [--reprendre] [--depuis <testwriter|developpeur> <composant>] [--contexte <texte>] | --reponse <texte> | --decision <texte>\n⛔ ${e.message}\n`
|
|
4366
|
+
);
|
|
4367
|
+
return CODE_USAGE;
|
|
4368
|
+
} finally {
|
|
4369
|
+
ctx?.base.fermer();
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
4374
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
4375
|
+
}
|