git-preflight-gate 0.1.6__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Platino Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ # HUELLA-MANIFEST-SDIST-20261106
2
+ # El sdist es el archivo que viaja; el wheel lleva el paquete `sim`.
3
+ # Medido antes de escribir (2026-11-06, tag v0.1.3): el sdist pesaba 8 920 B y
4
+ # NO contenia preflight-gate.sh, action.yml, sim/ ni templates/ — 0 de 4 piezas
5
+ # del producto. Un sdist que omite el producto no es un paquete pequeno: es un
6
+ # paquete que nombra otra cosa.
7
+ include LICENSE
8
+ include README.md
9
+ include README.es.md
10
+ include action.yml
11
+ include preflight-gate.sh
12
+ include pyproject.toml
13
+ recursive-include sim *.py
14
+ recursive-include templates *.yml
15
+ recursive-include tests *
16
+ # El aparato de CI no viaja: es del repositorio, no del paquete.
17
+ prune .github
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-preflight-gate
3
+ Version: 0.1.6
4
+ Summary: Pre-write preflight gate (bash) and CI authority gate (Python composite action): fail-closed, standard library only except PyYAML for the CI extra.
5
+ Author: alexisromtz-lab
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/alexisromtz-lab/git-preflight-gate
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: PyYAML>=6.0
12
+ Provides-Extra: ci-authority-gate
13
+ Requires-Dist: PyYAML==6.0.3; extra == "ci-authority-gate"
14
+ Dynamic: license-file
15
+
16
+ <!-- HUELLA-README-EN-20261105 -->
17
+ **Language:** English · [Español](README.es.md)
18
+
19
+ # CI Authority Gate
20
+
21
+ **Repository:** [`git-preflight-gate`](https://github.com/alexisromtz-lab/git-preflight-gate) · **Marketplace:** [Install the action](https://github.com/marketplace/actions/ci-authority-gate)
22
+
23
+ **An integrity gate for autonomous AI agents: so that a bot does not dirty the tree
24
+ and does not commit concurrency collisions either.**
25
+
26
+ Before an agent writes a line, the gate demands a destination tuple —
27
+ `TARGET` (absolute) · `BLOB_PRE` · `HUELLA` · `LINEAS_PRE` — and returns a code that
28
+ **authorizes or blocks** the write. Without a prior run ending in `RC 0`, nothing is written.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install git-preflight-gate # Python CI gate (PyYAML comes with it)
34
+ ci-authority-gate /path/to/your/repo # RC 0 = pass · RC > 0 = fail-closed, cause printed
35
+ ```
36
+
37
+ Two doors, measured separately — do not confuse them:
38
+
39
+ | door | command | what it needs |
40
+ |---|---|---|
41
+ | **CI authority gate** | `pip install git-preflight-gate` | Python ≥ 3.9 + PyYAML (pip resolves it) |
42
+ | **pre-write gate (bash)** | `curl -fsSL https://raw.githubusercontent.com/alexisromtz-lab/git-preflight-gate/main/preflight-gate.sh -o preflight-gate.sh` | bash + git, **zero dependencies** |
43
+
44
+ The wheel ships the Python gate (`sim/`); the bash gate ships in the source distribution and in the repository, and is deliberately dependency-free so it can run on a machine with nothing installed. Neither is a wrapper around the other.
45
+
46
+ ### The pre-write gate, at the shell
47
+
48
+ ```bash
49
+ bash preflight-gate.sh /absolute/path/file.py <blob_pre> <exclusive_marker> <lineas_pre>
50
+ ```
51
+
52
+ ## The codes (fail-closed)
53
+
54
+ | RC | meaning |
55
+ |---:|---|
56
+ | `0` | PRE-FLIGHT OK — the target accepts an edit on `BLOB_PRE` |
57
+ | `10` | the object does not exist (creation) |
58
+ | `11` | **the marker already lives in the target** ⇒ collision: the row is already written |
59
+ | `12` | measured blob ≠ contract ⇒ the tree moved |
60
+ | `13` | dirty git state ⇒ unadjudicated modifications |
61
+ | `14` | the target is not inside a git tree ⇒ the gate cannot verify identity |
62
+ | `15` | line count differs from the contract |
63
+ | `20` | misuse (incomplete tuple, or `TARGET` not absolute) |
64
+
65
+ Every code comes out **with its cause printed**. A correct `RC` for the wrong reason is not a gate: it is a coincidence.
66
+
67
+ ## Why `RC 11` is the case that matters
68
+
69
+ The most expensive defect of an agent is not picking the wrong path: it is **writing again something another hand already wrote**, and reporting success. The marker is an exclusive literal fragment of the text to be inserted; if it is already in the target, the write is rejected before the file is touched. The gate does not guess intent: it denies the collision.
70
+
71
+ ## Why `TARGET` must be absolute
72
+
73
+ The gate rebases with `git -C <directory>` to measure the blob and the state. A relative path would be counted from **two different bases** (`cwd` and the object's directory) and could hash *another* file that does exist. Fail-closed: relative path ⇒ `RC 20`.
74
+
75
+ ## Why `RC 14` exists
76
+
77
+ Without a git tree, `git status` returns 128 with empty output. A naive gate reads that empty output as "clean" and goes green. Here the impossibility is declared: without a tree there is no blob identity worth anything.
78
+
79
+ ## Case bank
80
+
81
+ ```bash
82
+ bash tests/must_fail.sh
83
+ ```
84
+
85
+ 9 cases, **9/9**, each verifying the code **and** the cause: `20` (incomplete tuple), `20` (relative path), `10`, `11`, `12`, `15`, `13`, `14`, and the positive control `0`. A bank without a positive control only proves the instrument knows how to say no.
86
+
87
+ ## Out-of-band telemetry
88
+
89
+ Each run records 7 fields (`hora_utc target blob_pre blob_medido rc hash_arbol_ini hash_arbol_fin`) in `${GATE_TELEMETRIA_LOG:-$HOME/preflight-telemetria.log}`. Telemetry **cannot move the RC**, **cannot abort** and **cannot refresh the index** (`--no-optional-locks`). The path is never relative: a log inside the tree becomes an untracked file, and then the gate ends up measuring its own telemetry — the instrument counting itself.
90
+
91
+ ## Declared limits
92
+
93
+ - On a nonexistent `TARGET` the gate stops at `RC 10` **before** comparing blob and lines: in the creation branch, the tuple is a **contract**, not a measurement. It is declared, not repaired (fail-closed is correct).
94
+ - It does not judge content: it certifies identity and collision, not correctness.
95
+
96
+ ## The root Action: CI Authority Gate
97
+
98
+ The `action.yml` at this repository's root is a **composite action** running a second instrument, `sim/gate_workflows_ci.py`: a structural verifier of the authority topology in `.github/workflows/`. Five counting invariants, seven typed causes, `RC = min(causes)`, `UNIVERSO_VACIO` = cause `31` (zero files is failure, not approval).
99
+
100
+ Measured identity of the instrument copied into this tree:
101
+
102
+ | property | value |
103
+ |---|---|
104
+ | lines / bytes | 615 / 21470 |
105
+ | git blob | `ff9c69a944e7965b3fda0901f7745d80a9e85c7c` |
106
+ | sha256-16 | `073954a84f3c84fd` |
107
+
108
+ Using it as a consumer:
109
+
110
+ ```yaml
111
+ - uses: alexisromtz-lab/git-preflight-gate@v0.1.4
112
+ with:
113
+ workflows-dir: .github/workflows
114
+ fail-on-red: "true"
115
+ ```
116
+
117
+ <!-- HUELLA-RELEASE-V013-20261031 -->
118
+ `v0.1.3`, `v0.1.2` and `v0.1.1` run the same `action.yml` (`9ef420ec`) and the same `sim/gate_workflows_ci.py` (`ff9c69a9`): the gate's code did not change across the three. What changes is the adoption wrapper — `v0.1.2` added `templates/platino-gate.yml` and `v0.1.3` added `action-selftest.yml`, the manifest alignment and this documentation. If you pin by SHA, use the commit — not the tag.
119
+
120
+ The four inputs (`workflows-dir`, `root`, `python-bin`, `fail-on-red`) and the two
121
+ outputs (`rc`, `veredicto`) preserve the contract of the `action.yml` that this
122
+ repository **replaces**: any CI that pointed at the root action of a working repository
123
+ migrates by changing only the repository name in `uses:`; the rest of the `with:` block
124
+ does not change.
125
+
126
+ The action does not guess the domain: the instrument resolves a single point, `<root>/.github/workflows`. That is why `workflows-dir` routes to `--file` when the path exists and to `--root` when it does not. A `--root` that accepted any directory with YAML files would be editing the ruler so the demo fits.
127
+
128
+ Single dependency: `PyYAML==6.0.3`, declared as an optional extra (`pip install ".[ci-authority-gate]"`). The rest is stdlib. The instrument does not read the network, does not write files, does not use randomness and does not consult the clock.
129
+
130
+ Installable without cloning the repository, from the release wheel. The
131
+ `ci-authority-gate` extra is mandatory for the CLI: without PyYAML the instrument does
132
+ not measure and exits `RC=30` (fail-closed), not green.
133
+
134
+ ```bash
135
+ pip install "git-preflight-gate[ci-authority-gate] @ https://github.com/alexisromtz-lab/git-preflight-gate/releases/download/v0.1.4/git_preflight_gate-0.1.4-py3-none-any.whl"
136
+ ci-authority-gate --self-test # built-in must-fail bank; touches no disk
137
+ ci-authority-gate --root . # measures <root>/.github/workflows and returns its RC
138
+ ```
139
+
140
+ <!-- HUELLA-README-V014-20261106 -->
141
+ `v0.1.4` does not change the gate: `action.yml` (`9ef420ec`) and
142
+ `sim/gate_workflows_ci.py` (`ff9c69a9`) are the same blobs as in `v0.1.3`. What changes
143
+ is the package: `MANIFEST.in` makes the sdist carry the product, and the manifest
144
+ declares the `sim` package and the `ci-authority-gate` script. Before this change the
145
+ wheel was 6 768 B of `dist-info` only and no CLI existed. It is not on PyPI yet: the
146
+ only `pip` path today is the release URL.
147
+
148
+ Measured positive control: workflow with `permissions: contents: read` and `uses` pinned by full SHA ⇒ `CAUSAS PRESENTES: ninguna`, `VEREDICTO: PASA`, `RC=0`. Negative control: `pull_request_target` + `permissions: write-all` + `curl | bash` + `actions/checkout@v4` ⇒ `CAUSAS PRESENTES: [1, 2, 5]`, `RC=1`. Empty universe ⇒ `CAUSAS PRESENTES: [31]`, `RC=31`.
149
+
150
+ ## The two instruments are not to be confused
151
+
152
+ | instrument | question | form |
153
+ |---|---|---|
154
+ | `preflight-gate.sh` | may this agent write *this file* now? | bash CLI, target tuple |
155
+ | `sim/gate_workflows_ci.py` | does this CI topology concentrate authority without control? | composite action + Python CLI |
156
+
157
+ The first protects a write; the second audits a workflow tree. They share the doctrine (fail-closed, printed cause, zero files = declared impossibility), not the code.
158
+
159
+ ## Install it in your repository in 1 minute
160
+
161
+ <!-- HUELLA-BADGE-PLATINO-README-20261031 -->
162
+
163
+ ```bash
164
+ mkdir -p .github/workflows
165
+ curl -fsSL https://raw.githubusercontent.com/alexisromtz-lab/git-preflight-gate/main/templates/platino-gate.yml \
166
+ -o .github/workflows/platino-gate.yml
167
+ git add .github/workflows/platino-gate.yml
168
+ git commit -m "ci: platino preflight gate"
169
+ git push
170
+ ```
171
+
172
+ That is all: the workflow fires on the next `push` or PR.
173
+
174
+ ### The file must be named `platino-gate.yml` (this is a contract, not advice)
175
+
176
+ If you rename it — `ci.yml`, `gate.yml`, `platino-gate.yaml` — the first job fails closed with `RC 20` and tells you why. The reason is measured, not aesthetic: the status badge resolves **by file name** (`.../workflow/status/YOUR-REPO/YOUR-REPO/platino-gate.yml`). With another name, the badge would read *your* CI workflow, not this gate, and would publish a green that measures nothing. The contract lives inside the workflow, not on this page.
177
+
178
+ Falsified by `tests/test_platino_gate_template.py`: 5 name cases and 5 structural cases, `RC 0` only if all 10 agree; a template mutated to accept any `.yml` returns `RC 1` naming the offending row.
179
+
180
+ ## The badge
181
+
182
+ When the run finishes, the **Summary** tab publishes the badge already filled in with your `owner/repo`. There are two, and they are not the same:
183
+
184
+ | badge | what it claims | permissions it asks for |
185
+ |---|---|---|
186
+ | **status** (dynamic) | the real result of the most recent run of `platino-gate.yml` | `contents: read` |
187
+ | **seal** (static) | "this repository passed the gate" | `contents: read` |
188
+
189
+ ```markdown
190
+ [![Platino Preflight Gate](https://img.shields.io/github/actions/workflow/status/OWNER/REPO/platino-gate.yml?label=preflight%20gate&branch=main)](https://github.com/alexisromtz-lab/la-auditora)
191
+
192
+ [![Audited by Platino Labs](https://img.shields.io/badge/audited_by-Platino_Labs-6f42c1)](https://github.com/alexisromtz-lab/la-auditora)
193
+ ```
194
+
195
+ The status one cannot lie: GitHub draws it from the run. **The seal can**: it is a literal anyone can paste in green without having run anything. Use it only when the run is green — the gate cannot verify that for you.
196
+
197
+ **Zero write permissions, in both cases.** The workflow does not touch your README, does not ask for `contents: write` and needs no service of ours: the badge is a `shields.io` URL that reads the public status of your own workflow. There is no endpoint to host, no token to expire.
198
+
199
+ ### What it measures and what it does not
200
+
201
+ | measures | does not measure |
202
+ |---|---|
203
+ | authority topology in `.github/workflows/`: `permissions`, triggers, `uses` pinned by SHA, execution sinks | whether your code does what it says |
204
+ | seven typed causes, `RC = min(causes)`, `UNIVERSO_VACIO` = cause `31` | default `GITHUB_TOKEN` permissions at organization level |
205
+ | the tree you have, at the commit that runs | OIDC / Trusted Publishing trust in the destination registry |
206
+
207
+ The second block is printed on every run, under `RESIDUO DECLARADO`. A gate that does not declare its limits is not a gate: it is a promise.
208
+
209
+ ### How the wrapper tests itself
210
+
211
+ <!-- HUELLA-BADGE-SELFTEST-DOC-20261031 -->
212
+ `action.yml` is not tested by reading its YAML: it is executed. On every push to `main`, the workflow [`action-selftest.yml`](.github/workflows/action-selftest.yml) runs the action twice with `uses: ./` — once on this repository itself, where it demands `rc=0` / `PASA`, and once on a poisoned fixture in `tests/fixtures/sumidero`, where it demands `rc=2` / `SUMIDERO_IMPUDO`. If the extra's installation (`pip install "<action_dir>[ci-authority-gate]"`) or the input routing breaks, it goes red here before going red on your runner.
213
+
214
+ A self-test that only knows how to be green discriminates nothing: that is why the second case demands the action **fail** with the correct cause.
215
+
216
+ ## License
217
+
218
+ MIT. See `LICENSE`.
@@ -0,0 +1,258 @@
1
+ <!-- HUELLA-README-ES-20261105 -->
2
+ **Idioma:** Español · [English](README.md)
3
+
4
+ # CI Authority Gate
5
+
6
+ **Repository:** [`git-preflight-gate`](https://github.com/alexisromtz-lab/git-preflight-gate) · **Marketplace:** [Install the action](https://github.com/marketplace/actions/ci-authority-gate)
7
+
8
+ **Un gate de integridad para agentes de IA autónomos: que un bot no ensucie el árbol
9
+ ni cometa colisiones de concurrencia.**
10
+
11
+ Antes de que un agente escriba una línea, el gate exige una tupla de destino —
12
+ `TARGET` (absoluto) · `BLOB_PRE` · `HUELLA` · `LINEAS_PRE` — y devuelve un código que
13
+ **autoriza o bloquea** la escritura. Sin corrida previa con `RC 0`, no se escribe.
14
+
15
+ ## Instalación
16
+
17
+ ```bash
18
+ pip install git-preflight-gate # gate de CI en Python (PyYAML viaja con él)
19
+ ci-authority-gate /ruta/a/tu/repo # RC 0 = pasa · RC > 0 = fallo cerrado con causa
20
+ ```
21
+
22
+ Dos puertas, medidas por separado — no se confunden:
23
+
24
+ | puerta | comando | qué necesita |
25
+ |---|---|---|
26
+ | **gate de CI** | `pip install git-preflight-gate` | Python ≥ 3.9 + PyYAML (lo resuelve pip) |
27
+ | **pre-write gate (bash)** | `curl -fsSL https://raw.githubusercontent.com/alexisromtz-lab/git-preflight-gate/main/preflight-gate.sh -o preflight-gate.sh` | bash + git, **cero dependencias** |
28
+
29
+ El wheel trae el gate de Python (`sim/`); el gate de bash viaja en el sdist y en el repositorio, y es deliberadamente sin dependencias para correr en una máquina sin nada instalado. Ninguno envuelve al otro.
30
+
31
+ ### El pre-write gate, en la terminal
32
+
33
+ ```bash
34
+ bash preflight-gate.sh /ruta/absoluta/archivo.py <blob_pre> <huella_exclusiva> <lineas_pre>
35
+ ```
36
+
37
+ ## Los códigos (fail-closed)
38
+
39
+ | RC | significado |
40
+ |---:|---|
41
+ | `0` | PRE-VUELO OK — el destino acepta la edición sobre `BLOB_PRE` |
42
+ | `10` | el objeto no existe (creación) |
43
+ | `11` | **la huella ya habita en el destino** ⇒ colisión: la fila ya está escrita |
44
+ | `12` | blob medido ≠ contrato ⇒ el árbol se movió |
45
+ | `13` | estado sucio en git ⇒ modificaciones no adjudicadas |
46
+ | `14` | el destino no vive en un árbol git ⇒ el gate no puede validar identidad |
47
+ | `15` | líneas distintas al contrato |
48
+ | `20` | uso incorrecto (tupla incompleta, o `TARGET` no absoluto) |
49
+
50
+ Cada código sale **con su causa impresa**. Un `RC` correcto por el motivo equivocado
51
+ no es un gate: es una coincidencia.
52
+
53
+ ## Por qué `RC 11` es el caso que importa
54
+
55
+ El defecto más caro de un agente no es equivocarse de ruta: es **volver a escribir algo
56
+ que otra mano ya escribió**, y reportar éxito. La huella es un fragmento literal
57
+ exclusivo del texto a insertar; si ya está en el destino, la escritura se rechaza antes
58
+ de tocar el archivo. El gate no adivina intenciones: niega la colisión.
59
+
60
+ ## Por qué `TARGET` debe ser absoluto
61
+
62
+ El gate re-basa con `git -C <directorio>` para medir el blob y el estado. Una ruta
63
+ relativa se contaría desde **dos bases distintas** (`cwd` y el directorio del objeto) y
64
+ podría hashear *otro* archivo que sí existe. Fail-closed: ruta relativa ⇒ `RC 20`.
65
+
66
+ ## Por qué `RC 14` existe
67
+
68
+ Sin árbol git, `git status` devuelve 128 con salida vacía. Un gate ingenuo lee esa
69
+ salida vacía como «limpio» y da verde. Aquí se declara la imposibilidad: sin árbol no
70
+ hay identidad de blob que valga.
71
+
72
+ ## Banco de casos
73
+
74
+ ```bash
75
+ bash tests/must_fail.sh
76
+ ```
77
+
78
+ 9 casos, **9/9**, cada uno verificando código **y** causa: `20` (tupla incompleta),
79
+ `20` (ruta relativa), `10`, `11`, `12`, `15`, `13`, `14` y el control positivo `0`.
80
+ Un banco sin control positivo sólo demuestra que el instrumento sabe decir que no.
81
+
82
+ ## Telemetría fuera de banda
83
+
84
+ Cada corrida anota 7 campos (`hora_utc target blob_pre blob_medido rc hash_arbol_ini
85
+ hash_arbol_fin`) en `${GATE_TELEMETRIA_LOG:-$HOME/preflight-telemetria.log}`. La
86
+ telemetría **no puede mover el RC**, **no puede abortar** y **no puede refrescar el
87
+ índice** (`--no-optional-locks`). La ruta nunca es relativa: un log dentro del árbol se
88
+ vuelve un archivo no rastreado, y entonces el gate termina midiendo su propia
89
+ telemetría — el instrumento que se cuenta a sí mismo.
90
+
91
+ ## Límites declarados
92
+
93
+ - Sobre un `TARGET` inexistente el gate corta en `RC 10` **antes** de comparar blob y
94
+ líneas: en la rama de creación, la tupla es **contrato**, no medición. Se declara; no
95
+ se repara (fail-closed es correcto).
96
+ - No juzga el contenido: certifica identidad y colisión, no corrección.
97
+
98
+ ## La Action de la raíz: CI Authority Gate
99
+
100
+ El `action.yml` de este repositorio es una **action composite** que ejecuta un segundo
101
+ instrumento, `sim/gate_workflows_ci.py`: un verificador estructural de la topología de
102
+ autoridad en `.github/workflows/`. Cinco invariantes contables, siete causas tipadas,
103
+ `RC = min(causas)`, `UNIVERSO_VACIO` = causa `31` (cero archivos es fracaso, no
104
+ aprobación).
105
+
106
+ Identidad medida del instrumento copiado a este árbol:
107
+
108
+ | propiedad | valor |
109
+ |---|---|
110
+ | líneas / bytes | 615 / 21470 |
111
+ | git blob | `ff9c69a944e7965b3fda0901f7745d80a9e85c7c` |
112
+ | sha256-16 | `073954a84f3c84fd` |
113
+
114
+ Uso como consumidor:
115
+
116
+ ```yaml
117
+ - uses: alexisromtz-lab/git-preflight-gate@v0.1.4
118
+ with:
119
+ workflows-dir: .github/workflows
120
+ fail-on-red: "true"
121
+ ```
122
+
123
+ <!-- HUELLA-RELEASE-V013-20261031 -->
124
+ `v0.1.3`, `v0.1.2` y `v0.1.1` ejecutan el mismo `action.yml` (`9ef420ec`) y el mismo
125
+ `sim/gate_workflows_ci.py` (`ff9c69a9`): el código del gate no cambió entre los tres.
126
+ Lo que cambia es el envoltorio de adopción —`v0.1.2` añadió `templates/platino-gate.yml`
127
+ y `v0.1.3` añadió `action-selftest.yml`, la alineación del manifiesto y esta
128
+ documentación—. Si fijas por SHA, usa el commit —no el tag—.
129
+
130
+ Los cuatro inputs (`workflows-dir`, `root`, `python-bin`, `fail-on-red`) y los dos
131
+ outputs (`rc`, `veredicto`) conservan el contrato del `action.yml` que este repositorio
132
+ **reemplaza**: cualquier CI que apuntaba a la action de la raíz de un repositorio de
133
+ trabajo migra cambiando sólo el nombre del repositorio en `uses:`; el resto del bloque
134
+ `with:` no cambia.
135
+
136
+ La acción no adivina el dominio: el instrumento resuelve un único punto,
137
+ `<raiz>/.github/workflows`. Por eso `workflows-dir` se enruta a `--file` cuando el path
138
+ existe y a `--root` cuando no. Un `--root` que aceptara cualquier directorio con YAMLs
139
+ sería editar el metro para que quepa la demo.
140
+
141
+ Dependencia única: `PyYAML==6.0.3`, declarada como extra opcional
142
+ (`pip install ".[ci-authority-gate]"`). El resto es stdlib. El instrumento no lee red,
143
+ no escribe archivos, no usa aleatoriedad y no consulta el reloj.
144
+
145
+ Instalable sin clonar el repositorio, desde el wheel de la release. El extra
146
+ `ci-authority-gate` es obligatorio para el CLI: sin PyYAML el instrumento no mide y
147
+ sale con `RC=30` (fail-closed), no con verde.
148
+
149
+ ```bash
150
+ pip install "git-preflight-gate[ci-authority-gate] @ https://github.com/alexisromtz-lab/git-preflight-gate/releases/download/v0.1.4/git_preflight_gate-0.1.4-py3-none-any.whl"
151
+ ci-authority-gate --self-test # banco must-fail incorporado; no toca disco
152
+ ci-authority-gate --root . # mide <raiz>/.github/workflows y devuelve su RC
153
+ ```
154
+
155
+ <!-- HUELLA-README-V014-20261106 -->
156
+ `v0.1.4` no cambia el gate: `action.yml` (`9ef420ec`) y `sim/gate_workflows_ci.py`
157
+ (`ff9c69a9`) son los mismos blobs que en `v0.1.3`. Lo que cambia es el paquete:
158
+ `MANIFEST.in` hace que el sdist lleve el producto, y el manifiesto declara el paquete
159
+ `sim` y el script `ci-authority-gate`. Antes de este cambio el wheel eran 6 768 B de
160
+ solo `dist-info` y no existía ningún CLI. En PyPI todavía no está publicado: la única
161
+ vía de `pip` hoy es la URL de la release.
162
+
163
+ Control positivo medido: workflow con `permissions: contents: read` y `uses` fijado por
164
+ SHA completo ⇒ `CAUSAS PRESENTES: ninguna`, `VEREDICTO: PASA`, `RC=0`. Control negativo:
165
+ `pull_request_target` + `permissions: write-all` + `curl | bash` + `actions/checkout@v4`
166
+ ⇒ `CAUSAS PRESENTES: [1, 2, 5]`, `RC=1`. Universo vacío ⇒ `CAUSAS PRESENTES: [31]`,
167
+ `RC=31`.
168
+
169
+ ## Los dos instrumentos no se confunden
170
+
171
+ | instrumento | pregunta | forma |
172
+ |---|---|---|
173
+ | `preflight-gate.sh` | ¿este agente puede escribir *este archivo* ahora? | CLI bash, tupla de destino |
174
+ | `sim/gate_workflows_ci.py` | ¿esta topología de CI concentra autoridad sin control? | action composite + CLI Python |
175
+
176
+ El primero protege una escritura; el segundo audita un árbol de workflows. Comparten la
177
+ doctrina (fail-closed, causa impresa, cero archivos = imposibilidad declarada), no el
178
+ código.
179
+
180
+ ## Instalarlo en tu repositorio en 1 minuto
181
+
182
+ <!-- HUELLA-BADGE-PLATINO-README-20261031 -->
183
+
184
+ ```bash
185
+ mkdir -p .github/workflows
186
+ curl -fsSL https://raw.githubusercontent.com/alexisromtz-lab/git-preflight-gate/main/templates/platino-gate.yml \
187
+ -o .github/workflows/platino-gate.yml
188
+ git add .github/workflows/platino-gate.yml
189
+ git commit -m "ci: platino preflight gate"
190
+ git push
191
+ ```
192
+
193
+ Eso es todo: el workflow se dispara en el siguiente `push` o PR.
194
+
195
+ ### El archivo debe llamarse `platino-gate.yml` (esto es un contrato, no un consejo)
196
+
197
+ Si lo renombras —`ci.yml`, `gate.yml`, `platino-gate.yaml`— el primer job falla
198
+ cerrado con `RC 20` y te dice por qué. La razón es medida, no estética: el badge
199
+ de estado se resuelve **por nombre de archivo** (`.../workflow/status/TU-REPO/TU-REPO/platino-gate.yml`).
200
+ Con otro nombre, el badge leería *tu* workflow de CI, no este gate, y publicaría un
201
+ verde que no mide nada. El contrato está dentro del workflow, no en esta página.
202
+
203
+ Falsado por `tests/test_platino_gate_template.py`: 5 casos de nombre y 5
204
+ estructurales, `RC 0` solo si los 10 concuerdan; un template mutado para aceptar
205
+ cualquier `.yml` devuelve `RC 1` nombrando la fila culpable.
206
+
207
+ ## El badge
208
+
209
+ Al terminar el run, la pestaña **Summary** publica el badge ya rellenado con tu
210
+ `owner/repo`. Hay dos, y no son lo mismo:
211
+
212
+ | badge | qué afirma | permisos que pide |
213
+ |---|---|---|
214
+ | de **estado** (dinámico) | el resultado real del run más reciente de `platino-gate.yml` | `contents: read` |
215
+ | **sello** (estático) | «este repositorio pasó el gate» | `contents: read` |
216
+
217
+ ```markdown
218
+ [![Platino Preflight Gate](https://img.shields.io/github/actions/workflow/status/OWNER/REPO/platino-gate.yml?label=preflight%20gate&branch=main)](https://github.com/alexisromtz-lab/la-auditora)
219
+
220
+ [![Audited by Platino Labs](https://img.shields.io/badge/audited_by-Platino_Labs-6f42c1)](https://github.com/alexisromtz-lab/la-auditora)
221
+ ```
222
+
223
+ El de estado no puede mentir: lo dibuja GitHub desde el run. **El sello, sí**: es un
224
+ literal que cualquiera puede pegar en verde sin haber corrido nada. Ponlo solo cuando
225
+ el run esté en verde — el gate no lo puede verificar por ti.
226
+
227
+ **Cero permisos de escritura, en los dos casos.** El workflow no toca tu README, no
228
+ pide `contents: write` y no necesita ningún servicio nuestro: el badge es una URL de
229
+ `shields.io` que lee el estado público de tu propio workflow. No hay endpoint que
230
+ hospedar, ni token que caducar.
231
+
232
+ ### Qué mide y qué no
233
+
234
+ | mide | no mide |
235
+ |---|---|
236
+ | topología de autoridad en `.github/workflows/`: `permissions`, disparadores, `uses` por SHA, sumideros de ejecución | si tu código hace lo que dice |
237
+ | siete causas tipadas, `RC = min(causas)`, `UNIVERSO_VACIO` = causa `31` | los permisos por defecto del `GITHUB_TOKEN` a nivel de organización |
238
+ | el árbol que tienes, en el commit que corre | confianza OIDC / Trusted Publishing en el registro destino |
239
+
240
+ El segundo bloque está impreso en cada run, bajo `RESIDUO DECLARADO`. Un gate que no
241
+ declara sus límites no es un gate: es una promesa.
242
+
243
+ ### Cómo se prueba el propio envoltorio
244
+
245
+ <!-- HUELLA-BADGE-SELFTEST-DOC-20261031 -->
246
+ El `action.yml` no se prueba leyendo su YAML: se ejecuta. En cada push a `main`, el
247
+ workflow [`action-selftest.yml`](.github/workflows/action-selftest.yml) corre la acción
248
+ dos veces con `uses: ./` —sobre este mismo repositorio, donde exige `rc=0` / `PASA`, y
249
+ sobre una fixture envenenada en `tests/fixtures/sumidero`, donde exige `rc=2` /
250
+ `SUMIDERO_IMPUDO`—. Si la instalación del extra (`pip install "<action_dir>[ci-authority-gate]"`)
251
+ o el enrutado de inputs se rompen, enrojece aquí antes de enrojecer en tu runner.
252
+
253
+ Un self-test que sólo sabe estar verde no discrimina nada: por eso el segundo caso exige
254
+ que la acción **falle** con la causa correcta.
255
+
256
+ ## Licencia
257
+
258
+ MIT. Ver `LICENSE`.