bmad-plus 0.18.0 → 0.19.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/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to BMAD+ will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.19.0] - 2026-09-27
9
+
10
+ ### Added
11
+
12
+ - **`bmad-plus review` — evidence for code review.** `review scope <id>` seals what a review must cover: files changed since the merge base (or the working tree), each excluded file with its reason (secrets first and never re-included, binaries, deletions, generated or vendored files), ordered review units and a scope hash. `review anchor <id>` locates every finding by the code it quotes, never by a line number, and reports ambiguous or unlocated quotes. `review gate <id>` derives the verdict from coverage — incomplete, findings or clean — never from the number of findings. Findings use closed enums that are refused, not coerced, when unknown. The Dev Studio code-review workflow and Sentinel use it when the CLI is available. See `docs/specs/code-review-evidence.md`.
13
+ - Review triage keeps a finding by default and refutes it only on a quoted ground; security, data-loss and money findings without one stay unresolved. `tools/build/verify-action-pins.js` makes CI refuse a third-party action not pinned to a full commit.
14
+
15
+ ### Fixed
16
+
17
+ - `review` and `uat` read JSON written by Windows PowerShell 5.1, whose `Set-Content -Encoding utf8` adds a byte-order mark that JSON parsing refused.
18
+ - The release workflow accepts the one-element array current npm returns for `npm view … --json`; 0.18.0 was published intact and then wrongly reported as mismatched.
19
+
8
20
  ## [0.18.0] - 2026-09-25
9
21
 
10
22
  ### Added
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # BMAD+
2
2
 
3
- [![Version](https://img.shields.io/badge/version-0.18.0-blue)](https://www.npmjs.com/package/bmad-plus)
3
+ [![Version](https://img.shields.io/badge/version-0.19.0-blue)](https://www.npmjs.com/package/bmad-plus)
4
4
 
5
- **Version 0.18.0** · Node.js `>=20.0.0` · MIT
5
+ **Version 0.19.0** · Node.js `>=20.0.0` · MIT
6
6
 
7
7
  Project-local AI development workflows with clear roles, shared context, safe updates and coding-tool adapters
8
8
 
@@ -35,7 +35,7 @@ Run the installer. Select the adapters for the AI tools you use and the packs yo
35
35
  **In your project terminal:**
36
36
 
37
37
  ```sh
38
- npx bmad-plus@0.18.0 install
38
+ npx bmad-plus@0.19.0 install
39
39
  ```
40
40
 
41
41
  The installer creates the agent instructions, shared project spine and selected tool adapters. Optional packs may need additional runtimes or API access.
@@ -71,7 +71,7 @@ Ask Sentinel to check the change against its acceptance criteria. Ask for test e
71
71
  **In your AI assistant:**
72
72
 
73
73
  ```text
74
- Sentinel, review this change against the acceptance criteria. Check the main success and failure paths, report remaining issues and summarize what the next session needs to know.
74
+ Sentinel, review this change against the acceptance criteria. Check the main success and failure paths, report remaining issues and summarize what the next session needs to know. If the BMAD+ CLI is available, seal the scope with bmad-plus review scope, quote the code of every finding, account for every selected file, then report what bmad-plus review gate answers.
75
75
  ```
76
76
 
77
77
  Read the changes and test results. The assistant follows the permissions and capabilities of its host tool.
@@ -171,20 +171,18 @@ Read AGENTS.md and the available project memory. Summarize the last verified sta
171
171
 
172
172
  Expected result: a short, evidence-based restart. Project notes are useful context and still need to be checked against the current work.
173
173
 
174
- ## What’s new in 0.18.0
174
+ ## What’s new in 0.19.0
175
175
 
176
- Acceptance pages that keep every answer
176
+ Code reviews you can check
177
177
 
178
- The acceptance page now verifies every save, restores your answers on reload before anything else, keeps runs from other revisions and other tabs apart, and names sixteen guarantees the build refuses to lose. Nothing changes in how you install or update; rebuild your recipe pages to get the new page.
178
+ bmad-plus review seals what a review must cover, locates every finding by the code it quotes, and derives the verdict from coverage instead of the number of findings. Review triage now keeps a finding unless a quoted ground refutes it. Nothing changes in how you install or update.
179
179
 
180
- - A write to the browser counts only once it reads back. A refused save (quota, private window, blocked storage) is named on the page, questions leaving, and the export stays available — the page never says saved on its own word.
181
- - Your answers come back on reload before the local server or the artifact database answer, and the newer copy always wins: an older remote copy never overwrites what you just ticked.
182
- - A run that answered another revision of the recipe is offered, not poured in: unchanged lines keep their answers, changed lines are asked again, the earlier run stays untouched and exportable, and the new run records where its answers came from.
183
- - Two tabs or two devices on the same run converge on the latest change and say so. A saved run that cannot be read is reported and exportable, never deleted.
184
- - The progress bar exposes the share of lines answered — answered, not passed — with the seen, not seen and blocked counts; the page says where answers live and what can make them disappear.
185
- - Finishing with unanswered lines is questioned on the page itself and confirmed with a second click; a finished run says it is not an acceptance, and a change made after finishing is dated and shown.
186
- - uat build lists the sixteen guarantees the page carries and refuses a template that lost one; uat serve refuses an older copy of a run and any host name but its own. A name with no Latin letters signs its run with a stable hash.
187
- - The acceptance skill tells the agent to build the page with the command, never write it by hand, play the essential pass in a real browser before handing it over, and say which checks ran. The page strings gain twenty-two sentences in ten languages.
180
+ - bmad-plus review scope lists the files changed since the base branch or in your working tree, names why every other changed file is left out (secret, binary, deleted, generated), splits the work into ordered units and seals it with a hash. Secret files are excluded before any include pattern and never come back.
181
+ - Findings go in findings.json and quote the code they are about, word for word. bmad-plus review anchor finds that code in the file and reports a quote it cannot find, or finds twice, instead of trusting a line number. Unknown categories or severities are refused, not guessed.
182
+ - bmad-plus review gate answers incomplete, findings or clean. A review is incomplete while any selected file is missing from coverage.json or an open finding is not anchored; clean covers the reviewed scope only.
183
+ - Sentinel and the Dev Studio code-review workflow keep a finding by default and refute it only on a quoted ground: the code is absent, the code contradicts it, or it duplicates another finding. Security, data-loss and money findings without such a quote stay unresolved.
184
+ - The maintainer README now states what BMAD+ deliberately does not do: no model runtime, no background process, no review engine that repositions findings, no automatic fixes from a review.
185
+ - Release tooling accepts the one-element array npm now returns for integrity checks. Continuous integration checks that every third-party GitHub Action is pinned to a full commit.
188
186
 
189
187
  ## Version History
190
188
 
@@ -192,9 +190,9 @@ These dates identify reviewed CHANGELOG notes, not npm publication dates. The we
192
190
 
193
191
  | Version | Release-notes date | Reviewed summary |
194
192
  | --- | --- | --- |
193
+ | 0.19.0 | 2026-09-27 | bmad-plus review seals what a review must cover, locates every finding by the code it quotes, and derives the verdict from coverage instead of the number of findings. Review triage now keeps a finding unless a quoted ground refutes it. Nothing changes in how you install or update. |
195
194
  | 0.18.0 | 2026-09-25 | The acceptance page now verifies every save, restores your answers on reload before anything else, keeps runs from other revisions and other tabs apart, and names sixteen guarantees the build refuses to lose. Nothing changes in how you install or update; rebuild your recipe pages to get the new page. |
196
195
  | 0.17.1 | 2026-09-24 | Installing over a different version now stops and points to update, re-installs keep your settings, and updates remove files a version no longer ships while keeping a restorable backup. Some install behaviors changed; review your scripts before rerunning them. |
197
- | 0.17.0 | 2026-09-22 | Acceptance pages now prevent silent answer loss and empty exports, recipes build by their own identifier, and project release checks work directly with Node on Windows. Guides add practical prompts for reading results and preserving project context. |
198
196
 
199
197
  [Release history and update guide](https://bmad-plus.rochetta.fr/docs/#changelog) · [All published npm versions](https://www.npmjs.com/package/bmad-plus?activeTab=versions)
200
198
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-plus",
4
- "version": "0.18.0",
4
+ "version": "0.19.0",
5
5
  "description": "Project-local AI development workflows with clear roles, shared context, safe updates and coding-tool adapters",
6
6
  "homepage": "https://bmad-plus.rochetta.fr",
7
7
  "keywords": [
@@ -4,9 +4,9 @@
4
4
  <a href="../README.md">English</a> | <a href="README.fr.md">Français</a> | <a href="README.es.md">Español</a> | 🌐 <b>Deutsch</b>
5
5
  </div>
6
6
 
7
- [![Version](https://img.shields.io/badge/version-0.18.0-blue)](https://www.npmjs.com/package/bmad-plus)
7
+ [![Version](https://img.shields.io/badge/version-0.19.0-blue)](https://www.npmjs.com/package/bmad-plus)
8
8
 
9
- **Version 0.18.0** · Node.js `>=20.0.0` · MIT
9
+ **Version 0.19.0** · Node.js `>=20.0.0` · MIT
10
10
 
11
11
  Projektlokale KI-Entwicklungsworkflows mit klaren Rollen, gemeinsamem Kontext, sicheren Updates und Adaptern für Coding-Tools.
12
12
 
@@ -39,7 +39,7 @@ Starte das Installationsprogramm. Wähle die Adapter deiner KI-Werkzeuge und die
39
39
  **Im Terminal deines Projekts:**
40
40
 
41
41
  ```sh
42
- npx bmad-plus@0.18.0 install
42
+ npx bmad-plus@0.19.0 install
43
43
  ```
44
44
 
45
45
  Die Installation erstellt Agentenanweisungen, die gemeinsame Projektbasis und die gewählten Adapter. Optionale Packs können weitere Laufzeitumgebungen oder API-Zugänge benötigen.
@@ -75,7 +75,7 @@ Bitte Sentinel, die Änderung anhand der Akzeptanzkriterien zu prüfen. Fordere
75
75
  **In deinem KI-Assistenten:**
76
76
 
77
77
  ```text
78
- Sentinel, prüfe diese Änderung anhand der Akzeptanzkriterien. Überprüfe die wichtigsten Erfolgs- und Fehlerfälle, nenne offene Probleme und fasse zusammen, was die nächste Sitzung wissen muss.
78
+ Sentinel, prüfe diese Änderung anhand der Akzeptanzkriterien. Überprüfe die wichtigsten Erfolgs- und Fehlerfälle, nenne offene Probleme und fasse zusammen, was die nächste Sitzung wissen muss. Wenn das BMAD+ CLI verfügbar ist, versiegle den Umfang mit bmad-plus review scope, zitiere den Code jedes Befunds, belege jede ausgewählte Datei und berichte, was bmad-plus review gate antwortet.
79
79
  ```
80
80
 
81
81
  Lies die Änderungen und Testergebnisse. Der Assistent arbeitet innerhalb der Rechte und Fähigkeiten seines Host-Werkzeugs.
@@ -175,20 +175,18 @@ Lies AGENTS.md und die verfügbare Projekterinnerung. Fasse den letzten geprüft
175
175
 
176
176
  Erwartetes Ergebnis: ein kurzer, belegter Wiedereinstieg. Projektnotizen liefern Kontext und müssen mit dem aktuellen Stand abgeglichen werden.
177
177
 
178
- ## Neu in 0.18.0
178
+ ## Neu in 0.19.0
179
179
 
180
- Abnahmeseiten, die jede Antwort behalten
180
+ Code-Reviews, die sich prüfen lassen
181
181
 
182
- Die Abnahmeseite prüft jetzt jedes Speichern, stellt deine Antworten beim Neuladen vor allem anderen wieder her, hält Durchläufe anderer Fassungen und anderer Tabs auseinander und benennt sechzehn Garantien, die der Build nicht verlieren darf. An Installation und Update ändert sich nichts; baue deine Rezeptseiten neu, um die neue Seite zu erhalten.
182
+ bmad-plus review versiegelt, was ein Review abdecken muss, findet jeden Befund über den Code, den er zitiert, und leitet das Urteil aus der Abdeckung ab statt aus der Zahl der Befunde. Die Review-Sichtung behält einen Befund jetzt, solange kein zitierter Grund ihn widerlegt. An Installation und Update ändert sich nichts.
183
183
 
184
- - Ein Schreibvorgang im Browser zählt erst, wenn er zurückgelesen wurde. Ein abgelehntes Speichern (Kontingent, privates Fenster, gesperrter Speicher) wird auf der Seite benannt, das Verlassen wird hinterfragt, und der Export bleibt möglich — die Seite sagt nie auf eigenes Wort „gespeichert“.
185
- - Deine Antworten kommen beim Neuladen zurück, bevor der lokale Server oder die Artefakt-Datenbank antworten, und die neuere Kopie gewinnt immer: Eine ältere entfernte Kopie überschreibt nie, was du gerade angekreuzt hast.
186
- - Ein Durchlauf, der eine andere Fassung der Checkliste beantwortet hat, wird angeboten, nicht hineingekippt: Unveränderte Zeilen behalten ihre Antworten, geänderte werden erneut abgefragt, der frühere Durchlauf bleibt unverändert und exportierbar, und der neue vermerkt, woher seine Antworten stammen.
187
- - Zwei Tabs oder zwei Geräte auf demselben Durchlauf einigen sich auf die letzte Änderung und sagen es. Ein gespeicherter, nicht lesbarer Durchlauf wird gemeldet und bleibt exportierbar, nie gelöscht.
188
- - Der Fortschrittsbalken zeigt den Anteil beantworteter Zeilen — beantwortet, nicht bestanden — mit den Zählern gesehen, nicht gesehen und blockiert; die Seite sagt, wo die Antworten liegen und was sie verschwinden lassen kann.
189
- - Das Abschließen mit unbeantworteten Zeilen wird auf der Seite selbst hinterfragt und mit einem zweiten Klick bestätigt; ein abgeschlossener Durchlauf sagt, dass er keine Abnahme ist, und eine Änderung nach dem Abschluss wird datiert und angezeigt.
190
- - uat build listet die sechzehn Garantien der Seite auf und lehnt eine Vorlage ab, die eine verloren hat; uat serve lehnt eine ältere Kopie eines Durchlaufs und jeden fremden Hostnamen ab. Ein Name ohne lateinische Buchstaben signiert seinen Durchlauf mit einem stabilen Hash.
191
- - Der Abnahme-Skill weist den Agenten an, die Seite mit dem Befehl zu bauen, nie von Hand, den wesentlichen Durchgang vor der Übergabe in einem echten Browser zu spielen und zu sagen, welche Prüfungen liefen. Die Seitentexte erhalten zweiundzwanzig Sätze in zehn Sprachen.
184
+ - bmad-plus review scope listet die seit dem Basis-Branch oder im Arbeitsverzeichnis geänderten Dateien, nennt für jede andere geänderte Datei den Grund des Ausschlusses (Geheimnis, binär, gelöscht, generiert), teilt die Arbeit in geordnete Einheiten und versiegelt alles mit einem Hash. Geheime Dateien werden vor jedem Einschlussmuster ausgeschlossen und kommen nie zurück.
185
+ - Befunde stehen in findings.json und zitieren den betroffenen Code wörtlich. bmad-plus review anchor findet diesen Code in der Datei und meldet ein Zitat, das es nicht oder doppelt findet, statt einer Zeilennummer zu vertrauen. Eine unbekannte Kategorie oder Schwere wird abgelehnt, nie erraten.
186
+ - bmad-plus review gate antwortet incomplete, findings oder clean. Ein Review bleibt unvollständig, solange eine ausgewählte Datei in coverage.json fehlt oder ein offener Befund nicht verankert ist; clean gilt nur für den geprüften Umfang.
187
+ - Sentinel und der Dev-Studio-Workflow code-review behalten einen Befund standardmäßig und widerlegen ihn nur mit einem zitierten Grund: Der Code fehlt, der Code widerspricht ihm, oder er doppelt einen anderen Befund. Befunde zu Sicherheit, Datenverlust oder Geld ohne ein solches Zitat bleiben ungeklärt.
188
+ - Das Maintainer-README nennt jetzt, was BMAD+ bewusst nicht tut: keine Modell-Laufzeit, keine Hintergrundprozesse, keine Review-Engine, die Befunde verschiebt, keine automatischen Korrekturen aus einem Review.
189
+ - Die Veröffentlichungswerkzeuge akzeptieren das Ein-Element-Array, das npm jetzt bei Integritätsprüfungen liefert. Die Continuous Integration prüft, dass jede GitHub-Action von Dritten auf einen vollständigen Commit festgelegt ist.
192
190
 
193
191
  ## Versionsverlauf
194
192
 
@@ -196,9 +194,9 @@ Diese Daten bezeichnen geprüfte CHANGELOG-Einträge, nicht die Veröffentlichun
196
194
 
197
195
  | Version | Datum der Notizen | Geprüfte Zusammenfassung |
198
196
  | --- | --- | --- |
197
+ | 0.19.0 | 2026-09-27 | bmad-plus review versiegelt, was ein Review abdecken muss, findet jeden Befund über den Code, den er zitiert, und leitet das Urteil aus der Abdeckung ab statt aus der Zahl der Befunde. Die Review-Sichtung behält einen Befund jetzt, solange kein zitierter Grund ihn widerlegt. An Installation und Update ändert sich nichts. |
199
198
  | 0.18.0 | 2026-09-25 | Die Abnahmeseite prüft jetzt jedes Speichern, stellt deine Antworten beim Neuladen vor allem anderen wieder her, hält Durchläufe anderer Fassungen und anderer Tabs auseinander und benennt sechzehn Garantien, die der Build nicht verlieren darf. An Installation und Update ändert sich nichts; baue deine Rezeptseiten neu, um die neue Seite zu erhalten. |
200
199
  | 0.17.1 | 2026-09-24 | Eine Installation über eine andere Version bricht jetzt ab und verweist auf update, eine erneute Installation behält deine Einstellungen, und Updates entfernen Dateien, die eine Version nicht mehr mitliefert, mit wiederherstellbarer Sicherung. Einige Installationsabläufe ändern sich: Prüfe deine Skripte, bevor du sie erneut ausführst. |
201
- | 0.17.0 | 2026-09-22 | Abnahmeseiten verhindern jetzt unbemerkt verlorene Antworten und leere Exporte. Rezepte lassen sich anhand ihrer Kennung erzeugen, und Veröffentlichungsprüfungen verwenden unter Windows direkt Node. Die Anleitungen ergänzen Prompts für die Ergebnisprüfung und die Sicherung des Projektkontexts. |
202
200
 
203
201
  [Versionsverlauf und Update-Anleitung](https://bmad-plus.rochetta.fr/de/docs/#changelog) · [Alle veröffentlichten Versionen auf npm](https://www.npmjs.com/package/bmad-plus?activeTab=versions)
204
202
 
@@ -4,9 +4,9 @@
4
4
  <a href="../README.md">English</a> | <a href="README.fr.md">Français</a> | 🌐 <b>Español</b> | <a href="README.de.md">Deutsch</a>
5
5
  </div>
6
6
 
7
- [![Version](https://img.shields.io/badge/version-0.18.0-blue)](https://www.npmjs.com/package/bmad-plus)
7
+ [![Version](https://img.shields.io/badge/version-0.19.0-blue)](https://www.npmjs.com/package/bmad-plus)
8
8
 
9
- **Versión 0.18.0** · Node.js `>=20.0.0` · MIT
9
+ **Versión 0.19.0** · Node.js `>=20.0.0` · MIT
10
10
 
11
11
  Flujos de desarrollo con IA propios de cada proyecto, con roles claros, contexto compartido, actualizaciones seguras y adaptadores para herramientas de programación.
12
12
 
@@ -39,7 +39,7 @@ Ejecuta el instalador. Selecciona los adaptadores de tus herramientas de IA y lo
39
39
  **En la terminal del proyecto:**
40
40
 
41
41
  ```sh
42
- npx bmad-plus@0.18.0 install
42
+ npx bmad-plus@0.19.0 install
43
43
  ```
44
44
 
45
45
  El instalador crea las instrucciones de los agentes, la base compartida y los adaptadores seleccionados. Algunos packs opcionales necesitan otros entornos o acceso a API.
@@ -75,7 +75,7 @@ Pide a Sentinel que compruebe el cambio según los criterios de aceptación. Sol
75
75
  **En tu asistente de IA:**
76
76
 
77
77
  ```text
78
- Sentinel, revisa este cambio según los criterios de aceptación. Comprueba los principales casos de éxito y fallo, informa de los problemas pendientes y resume lo que necesita saber la próxima sesión.
78
+ Sentinel, revisa este cambio según los criterios de aceptación. Comprueba los principales casos de éxito y fallo, informa de los problemas pendientes y resume lo que necesita saber la próxima sesión. Si el CLI de BMAD+ está disponible, sella el alcance con bmad-plus review scope, cita el código de cada hallazgo, da cuenta de cada archivo seleccionado y comunica lo que responde bmad-plus review gate.
79
79
  ```
80
80
 
81
81
  Lee los cambios y los resultados de las pruebas. El asistente sigue los permisos y las capacidades de su herramienta anfitriona.
@@ -175,20 +175,18 @@ Lee AGENTS.md y la memoria de proyecto disponible. Resume el último estado veri
175
175
 
176
176
  Resultado esperado: una reanudación breve y fundamentada. Las notas aportan contexto que debe contrastarse con el trabajo actual.
177
177
 
178
- ## Novedades de la 0.18.0
178
+ ## Novedades de la 0.19.0
179
179
 
180
- Páginas de aceptación que conservan cada respuesta
180
+ Revisiones de código verificables
181
181
 
182
- La página de aceptación ahora verifica cada guardado, restaura tus respuestas al recargar antes que nada, mantiene separadas las pruebas de otras versiones y otras pestañas, y nombra dieciséis garantías que la construcción se niega a perder. Nada cambia en la instalación ni en la actualización; reconstruye tus páginas de receta para obtener la nueva página.
182
+ bmad-plus review sella lo que una revisión debe cubrir, localiza cada hallazgo por el código que cita y deduce el veredicto de la cobertura en lugar del número de hallazgos. La clasificación de las revisiones conserva ahora un hallazgo mientras ningún motivo citado lo refute. Nada cambia en la instalación ni en la actualización.
183
183
 
184
- - Una escritura en el navegador solo cuenta una vez releída. Un guardado rechazado (cuota, ventana privada, almacenamiento bloqueado) se indica en la página, salir se cuestiona y la exportación sigue disponible: la página nunca dice «guardado» sin comprobarlo.
185
- - Tus respuestas vuelven al recargar antes de que respondan el servidor local o la base del artefacto, y la copia más reciente siempre gana: una copia remota más antigua nunca sobrescribe lo que acabas de marcar.
186
- - Una prueba que respondió a otra versión de la receta se ofrece, no se vuelca: las líneas sin cambios conservan sus respuestas, las modificadas se preguntan de nuevo, la prueba anterior queda intacta y exportable, y la nueva registra de dónde vienen sus respuestas.
187
- - Dos pestañas o dos dispositivos sobre la misma prueba convergen en el último cambio y lo indican. Una prueba guardada que no se puede leer se informa y se puede exportar, nunca se borra.
188
- - La barra de progreso muestra la proporción de líneas respondidas —respondidas, no superadas— con los recuentos de visto, no visto y bloqueado; la página dice dónde viven las respuestas y qué puede hacerlas desaparecer.
189
- - Terminar con líneas sin responder se cuestiona en la propia página y se confirma con un segundo clic; una prueba terminada indica que no es una aceptación, y un cambio hecho después de terminar queda fechado y visible.
190
- - uat build enumera las dieciséis garantías que lleva la página y rechaza una plantilla que haya perdido una; uat serve rechaza una copia más antigua de una prueba y cualquier nombre de host distinto del suyo. Un nombre sin letras latinas firma su prueba con una huella estable.
191
- - La habilidad de aceptación indica al agente que construya la página con el comando, nunca a mano, que recorra el paso esencial en un navegador real antes de entregarla y que diga qué comprobaciones se ejecutaron. Los textos de la página ganan veintidós frases en diez idiomas.
184
+ - bmad-plus review scope enumera los archivos modificados desde la rama base o en tu árbol de trabajo, indica por qué se descarta cada otro archivo modificado (secreto, binario, eliminado, generado), divide el trabajo en lotes ordenados y lo sella con una huella. Los archivos secretos se descartan antes de cualquier patrón de inclusión y nunca vuelven.
185
+ - Los hallazgos van en findings.json y citan literalmente el código al que se refieren. bmad-plus review anchor encuentra ese código en el archivo e informa de una cita que no encuentra, o que encuentra dos veces, en lugar de fiarse de un número de línea. Una categoría o gravedad desconocida se rechaza, nunca se adivina.
186
+ - bmad-plus review gate responde incomplete, findings o clean. Una revisión sigue incompleta mientras falte un archivo seleccionado en coverage.json o un hallazgo abierto no esté anclado; clean solo cubre el alcance revisado.
187
+ - Sentinel y el flujo code-review de Dev Studio conservan un hallazgo por defecto y solo lo refutan con un motivo citado: el código no está, el código lo contradice o duplica otro hallazgo. Un hallazgo de seguridad, pérdida de datos o dinero sin esa cita queda sin resolver.
188
+ - El README de mantenimiento indica ahora lo que BMAD+ no hace a propósito: sin motor de modelos, sin procesos en segundo plano, sin revisión que reubique los hallazgos, sin correcciones automáticas a partir de una revisión.
189
+ - Las herramientas de publicación aceptan la matriz de un elemento que npm devuelve ahora en las comprobaciones de integridad. La integración continua comprueba que cada acción de GitHub de terceros esté fijada a un commit completo.
192
190
 
193
191
  ## Historial de versiones
194
192
 
@@ -196,9 +194,9 @@ Estas fechas identifican las notas revisadas del CHANGELOG, no las fechas de pub
196
194
 
197
195
  | Versión | Fecha de las notas | Resumen revisado |
198
196
  | --- | --- | --- |
197
+ | 0.19.0 | 2026-09-27 | bmad-plus review sella lo que una revisión debe cubrir, localiza cada hallazgo por el código que cita y deduce el veredicto de la cobertura en lugar del número de hallazgos. La clasificación de las revisiones conserva ahora un hallazgo mientras ningún motivo citado lo refute. Nada cambia en la instalación ni en la actualización. |
199
198
  | 0.18.0 | 2026-09-25 | La página de aceptación ahora verifica cada guardado, restaura tus respuestas al recargar antes que nada, mantiene separadas las pruebas de otras versiones y otras pestañas, y nombra dieciséis garantías que la construcción se niega a perder. Nada cambia en la instalación ni en la actualización; reconstruye tus páginas de receta para obtener la nueva página. |
200
199
  | 0.17.1 | 2026-09-24 | Instalar sobre otra versión ahora se detiene y remite a update, una reinstalación conserva tus ajustes y las actualizaciones eliminan los archivos que una versión ya no incluye, con una copia restaurable. Algunos comportamientos de instalación cambian: revisa tus scripts antes de volver a ejecutarlos. |
201
- | 0.17.0 | 2026-09-22 | Las páginas de aceptación evitan la pérdida silenciosa de respuestas y las exportaciones vacías. Las recetas se generan por su identificador y los controles de publicación usan Node directamente en Windows. Las guías añaden prompts para revisar resultados y conservar el contexto del proyecto. |
202
200
 
203
201
  [Historial de versiones y guía de actualización](https://bmad-plus.rochetta.fr/es/docs/#changelog) · [Todas las versiones publicadas en npm](https://www.npmjs.com/package/bmad-plus?activeTab=versions)
204
202
 
@@ -4,9 +4,9 @@
4
4
  <a href="../README.md">English</a> | 🌐 <b>Français</b> | <a href="README.es.md">Español</a> | <a href="README.de.md">Deutsch</a>
5
5
  </div>
6
6
 
7
- [![Version](https://img.shields.io/badge/version-0.18.0-blue)](https://www.npmjs.com/package/bmad-plus)
7
+ [![Version](https://img.shields.io/badge/version-0.19.0-blue)](https://www.npmjs.com/package/bmad-plus)
8
8
 
9
- **Version 0.18.0** · Node.js `>=20.0.0` · MIT
9
+ **Version 0.19.0** · Node.js `>=20.0.0` · MIT
10
10
 
11
11
  Des workflows de développement IA propres à chaque projet, avec des rôles clairs, un contexte partagé, des mises à jour sûres et des adaptateurs pour vos outils de code.
12
12
 
@@ -39,7 +39,7 @@ Lancez l’installateur. Sélectionnez les adaptateurs de vos outils IA et les p
39
39
  **Dans le terminal du projet :**
40
40
 
41
41
  ```sh
42
- npx bmad-plus@0.18.0 install
42
+ npx bmad-plus@0.19.0 install
43
43
  ```
44
44
 
45
45
  L’installateur crée les instructions des agents, le socle partagé et les adaptateurs sélectionnés. Certains packs facultatifs nécessitent d’autres environnements ou des accès API.
@@ -75,7 +75,7 @@ Demandez à Sentinel de contrôler le changement selon les critères d’accepta
75
75
  **Dans votre assistant IA :**
76
76
 
77
77
  ```text
78
- Sentinel, relis ce changement selon les critères d’acceptation. Vérifie les principaux cas de réussite et d’échec, signale les problèmes restants et résume ce que la prochaine session doit savoir.
78
+ Sentinel, relis ce changement selon les critères d’acceptation. Vérifie les principaux cas de réussite et d’échec, signale les problèmes restants et résume ce que la prochaine session doit savoir. Si le CLI BMAD+ est disponible, scelle le périmètre avec bmad-plus review scope, cite le code de chaque remarque, rends compte de chaque fichier retenu, puis rapporte ce que répond bmad-plus review gate.
79
79
  ```
80
80
 
81
81
  Relisez les changements et les résultats des tests. L’assistant suit les autorisations et les capacités de son outil hôte.
@@ -175,20 +175,18 @@ Lis AGENTS.md et la mémoire de projet disponible. Résume le dernier état vér
175
175
 
176
176
  Résultat attendu : une reprise courte et fondée sur des éléments vérifiés. Les notes apportent du contexte, à confronter au travail actuel.
177
177
 
178
- ## Nouveautés de la 0.18.0
178
+ ## Nouveautés de la 0.19.0
179
179
 
180
- Des pages de recette qui gardent chaque réponse
180
+ Des revues de code vérifiables
181
181
 
182
- La page de recette vérifie désormais chaque enregistrement, restaure vos réponses au rechargement avant toute autre chose, distingue les passages d’autres révisions et d’autres onglets, et nomme seize garanties que la construction refuse de perdre. Rien ne change dans l’installation ou la mise à jour ; reconstruisez vos pages de recette pour obtenir la nouvelle page.
182
+ bmad-plus review scelle ce qu’une revue doit couvrir, retrouve chaque remarque par le code qu’elle cite, et déduit le verdict de la couverture plutôt que du nombre de remarques. Le tri des revues garde désormais une remarque tant qu’un motif cité ne la réfute pas. Rien ne change dans l’installation ou la mise à jour.
183
183
 
184
- - Une écriture dans le navigateur ne compte qu’une fois relue. Un enregistrement refusé (quota, fenêtre privée, stockage bloqué) est nommé sur la page, quitter est questionné et l’export reste possible — la page ne dit jamais « enregistré » sur parole.
185
- - Vos réponses reviennent au rechargement avant que le serveur local ou la base de l’artefact ne répondent, et la copie la plus récente gagne toujours : une copie distante plus ancienne n’écrase jamais ce que vous venez de cocher.
186
- - Un passage qui a répondu à une autre révision de la recette est proposé, pas réinjecté : les lignes inchangées gardent leurs réponses, les lignes modifiées sont redemandées, le passage précédent reste intact et exportable, et le nouveau passage note d’où viennent ses réponses.
187
- - Deux onglets ou deux appareils sur le même passage convergent vers la dernière modification et le disent. Un passage enregistré illisible est signalé et exportable, jamais supprimé.
188
- - La barre de progression expose la part de lignes renseignées — renseignées, pas réussies — avec les comptes vu, pas vu et bloqué ; la page dit où vivent les réponses et ce qui peut les faire disparaître.
189
- - Terminer avec des lignes non renseignées est questionné sur la page même et confirmé par un second clic ; un passage terminé dit qu’il n’est pas une validation, et une modification faite après la fin est datée et affichée.
190
- - uat build liste les seize garanties que porte la page et refuse un gabarit qui en a perdu une ; uat serve refuse une copie plus ancienne d’un passage et tout nom d’hôte autre que le sien. Un nom sans lettre latine signe son passage par une empreinte stable.
191
- - Le skill de recette demande à l’agent de construire la page avec la commande, jamais à la main, de jouer le passage essentiel dans un vrai navigateur avant de la remettre, et de dire quels contrôles ont tourné. Les textes de la page gagnent vingt-deux phrases en dix langues.
184
+ - bmad-plus review scope liste les fichiers modifiés depuis la branche de base ou dans votre arbre de travail, dit pourquoi chaque autre fichier modifié est écarté (secret, binaire, supprimé, généré), découpe le travail en lots ordonnés et scelle le tout par une empreinte. Les fichiers secrets sont écartés avant tout motif d’inclusion et ne reviennent jamais.
185
+ - Les remarques vont dans findings.json et citent mot pour mot le code qu’elles visent. bmad-plus review anchor retrouve ce code dans le fichier et signale une citation introuvable, ou trouvée deux fois, au lieu de se fier à un numéro de ligne. Une catégorie ou une sévérité inconnue est refusée, jamais devinée.
186
+ - bmad-plus review gate répond incomplete, findings ou clean. Une revue reste incomplète tant qu’un fichier retenu manque dans coverage.json ou qu’une remarque ouverte n’est pas ancrée ; clean ne couvre que le périmètre revu.
187
+ - Sentinel et le workflow code-review de Dev Studio gardent une remarque par défaut et ne la réfutent que sur un motif cité : le code est absent, le code la contredit, ou elle en double une autre. Une remarque sur la sécurité, la perte de données ou l’argent sans cette citation reste non résolue.
188
+ - Le README mainteneur dit désormais ce que BMAD+ ne fait pas, volontairement : pas de moteur de modèle, pas de processus d’arrière-plan, pas de revue qui repositionne les remarques, pas de correctif automatique issu d’une revue.
189
+ - Les outils de publication acceptent le tableau à un élément que npm renvoie désormais pour les contrôles d’intégrité. L’intégration continue vérifie que chaque action GitHub tierce est épinglée sur un commit complet.
192
190
 
193
191
  ## Historique des versions
194
192
 
@@ -196,9 +194,9 @@ Ces dates identifient les notes relues du CHANGELOG, pas les dates de publicatio
196
194
 
197
195
  | Version | Date des notes | Résumé relu |
198
196
  | --- | --- | --- |
197
+ | 0.19.0 | 2026-09-27 | bmad-plus review scelle ce qu’une revue doit couvrir, retrouve chaque remarque par le code qu’elle cite, et déduit le verdict de la couverture plutôt que du nombre de remarques. Le tri des revues garde désormais une remarque tant qu’un motif cité ne la réfute pas. Rien ne change dans l’installation ou la mise à jour. |
199
198
  | 0.18.0 | 2026-09-25 | La page de recette vérifie désormais chaque enregistrement, restaure vos réponses au rechargement avant toute autre chose, distingue les passages d’autres révisions et d’autres onglets, et nomme seize garanties que la construction refuse de perdre. Rien ne change dans l’installation ou la mise à jour ; reconstruisez vos pages de recette pour obtenir la nouvelle page. |
200
199
  | 0.17.1 | 2026-09-24 | Installer par-dessus une autre version s’arrête désormais et renvoie vers update, une réinstallation conserve vos réglages, et les mises à jour retirent les fichiers qu’une version ne livre plus en gardant une sauvegarde restaurable. Certains comportements d’installation changent : vérifiez vos scripts avant de les relancer. |
201
- | 0.17.0 | 2026-09-22 | Les pages de recette empêchent désormais la perte silencieuse des réponses et les exports vides. Une recette se construit avec son identifiant, et les contrôles de publication utilisent directement Node sous Windows. Les guides ajoutent des prompts pour relire les résultats et conserver le contexte du projet. |
202
200
 
203
201
  [Historique des versions et guide de mise à jour](https://bmad-plus.rochetta.fr/fr/docs/#changelog) · [Toutes les versions publiées sur npm](https://www.npmjs.com/package/bmad-plus?activeTab=versions)
204
202
 
@@ -60,9 +60,10 @@ When auto-activating a role, **announce it**: "💡 I'm switching to [Role] mode
60
60
  1. Establish the intended behavior, review scope and current artifact set. Read relevant project instructions, callers, tests and requirements. Treat the author's summary and worker completion messages as claims to check, not acceptance evidence. Inspect removed behavior and reachable failure paths where the change makes them relevant.
61
61
  2. Select only the review perspectives the task needs. When independent reviewers are authorized and available, give each the same baseline and a bounded question; launch the selected reviewers before using their findings to steer another review. Preserve their identities and returned evidence. Otherwise identify sequential perspectives as one assistant's work. An absent, failed or empty required response is incomplete; distinguish it from an explicit no-findings result after inspection.
62
62
  3. Verify each suspected issue before grouping findings. Trace its trigger through the actual caller and guards, inspect counterevidence, and reproduce it when practical. A failure in an unreachable state is not an established defect. Check every source, including a reviewer that describes its own findings as pre-verified.
63
- 4. Keep a compact finding record with identity, source, location, claimed consequence, evidence or refutation, disposition, relevant input identity and next action. Use confirmed, refuted or unresolved; rank confirmed defects by impact and likelihood. Preserve previous entries when their disposition changes. Group confirmed findings only when the same cause explains them, retaining each source record. Do not merge away a refutation or impose a finding quota.
63
+ 4. Keep a compact finding record with identity, source, location, claimed consequence, evidence or refutation, disposition, relevant input identity and next action. The default disposition is keep: refute only on a quoted ground — the described construct is absent from the file, the code contradicts the claim (guard, caller or test quoted), or another entry already carries the same cause — and write the analysis before the disposition. A security, data-loss or money finding without such a quote stays unresolved. Use confirmed, refuted or unresolved; rank confirmed defects by impact and likelihood. Preserve previous entries when their disposition changes. Group confirmed findings only when the same cause explains them, retaining each source record. Do not merge away a refutation or impose a finding quota.
64
64
  5. Inspect verification at the actual consumer. Read assertions and check how tests are selected before judging their coverage. Search relevant symbols and imports before asserting that coverage is absent. For a verification gap, name the consumer and a concrete regression or missed adoption that the current checks would fail to catch. Skipped tests, mocks that bypass the changed path and helper-only success do not prove that consumer works.
65
- 6. Reconcile acceptance using executed commands or direct observations against the current artifact set. Record passed, failed, skipped and unavailable checks separately. A missing required reviewer or untested criterion prevents a completed-acceptance claim, even when the remaining checks pass. A same-author review must not be described as independent verification.
65
+ 6. When the BMAD+ CLI is available, back a code review with its evidence: `bmad-plus review scope <id>` before reading, then `findings.json` with every finding quoting its code verbatim and `coverage.json` accounting for every selected file, then `bmad-plus review anchor <id>` and `bmad-plus review gate <id>`. Report the gate's disposition (incomplete, findings or clean) without upgrading it.
66
+ 7. Reconcile acceptance using executed commands or direct observations against the current artifact set. Record passed, failed, skipped and unavailable checks separately. A missing required reviewer or untested criterion prevents a completed-acceptance claim, even when the remaining checks pass. A same-author review must not be described as independent verification.
66
67
 
67
68
  ## Repair and continuation
68
69
 
@@ -8,6 +8,21 @@ For each suspected issue, inspect the supporting code and counterevidence. Remov
8
8
  duplicates and distinguish a behavioral defect from a style preference. Retain
9
9
  the reason a suspicion was refuted so continuation does not repeat it.
10
10
 
11
+ Refutation protocol. The default disposition is keep: removing a correct finding
12
+ costs more than keeping a wrong one. A finding moves to refuted only on one of
13
+ three citable grounds, and the citation goes into the record:
14
+
15
+ - **A — absent**: the construct the finding describes is not in the subject file;
16
+ quote what the file contains at that place.
17
+ - **B — contradicted**: the code refutes the claim; quote the guard, caller or
18
+ test that makes the failure unreachable.
19
+ - **C — duplicate**: another retained entry describes the same cause; name it.
20
+
21
+ Write the analysis before the disposition. Findings on security, data loss,
22
+ money or access control need ground A or B quoted; without it they stay
23
+ unresolved, never refuted. "Unlikely", "probably handled" and style disagreement
24
+ are not grounds.
25
+
11
26
  A retained finding needs a file/line, trigger, consequence, evidence, confidence
12
27
  and proportionate fix direction. Set severity from actual impact and likelihood.
13
28
  Unsupported speculation belongs in open questions.
@@ -17,9 +17,14 @@ arbitrary branch. Read the intended behavior and applicable project constraints.
17
17
 
18
18
  ## Procedure
19
19
 
20
- 1. Define the review scope and baseline. Inventory changed paths, related callers,
21
- tests and requirements. Preserve existing edits and record unavailable context.
22
- A supplied diff may omit the surrounding behavior needed to evaluate it.
20
+ 1. Define the review scope and baseline. When the BMAD+ CLI is available, seal it:
21
+ `bmad-plus review scope <id> --base <ref>` (or `--workspace` for uncommitted
22
+ work) writes `_bmad-output/review/<id>/scope.json` with every selected file,
23
+ every excluded file and its reason (secret, binary, deleted, generated), and
24
+ ordered review units. Never read or quote an excluded secret file. Inventory
25
+ related callers, tests and requirements. Preserve existing edits and record
26
+ unavailable context. A supplied diff may omit the surrounding behavior needed
27
+ to evaluate it.
23
28
  2. Review the intended behavior, invariants and relevant failure boundaries.
24
29
  Check control flow, data changes, interface compatibility and test assertions.
25
30
  Add concurrency, permissions, input validation, recovery or UX perspectives
@@ -34,7 +39,14 @@ arbitrary branch. Read the intended behavior and applicable project constraints.
34
39
  practical and within the task. Do not alter product code during a review
35
40
  unless fixes are also authorized.
36
41
  5. Triage the results. Remove duplicates, distinguish defects from preferences,
37
- and retain the reason when a suspicion is refuted. For a retained issue,
42
+ and retain the reason when a suspicion is refuted. Keep a finding by default:
43
+ deleting a real defect costs more than keeping a doubtful one. Refute it only
44
+ on a citable ground — (A) the construct it describes is absent from the file,
45
+ quoted from the file; (B) the claim contradicts the code, the named guard or
46
+ caller quoted; (C) it is a duplicate, the surviving entry named. Write the
47
+ analysis before the disposition. A finding on security, data loss or money
48
+ that meets none of these grounds stays unresolved, never refuted by judgment.
49
+ Doubt and taste are not grounds. For a retained issue,
38
50
  provide file/line, trigger, consequence, supporting evidence, confidence and a
39
51
  proportionate fix direction. Severity follows impact and likelihood, not tone.
40
52
  6. Reconcile acceptance coverage and review limitations. Record checks actually
@@ -43,6 +55,15 @@ arbitrary branch. Read the intended behavior and applicable project constraints.
43
55
  7. Deliver findings in impact order, followed by the scoped conclusion and
44
56
  evidence limits. Zero findings is valid after inspection; it means no
45
57
  actionable defect was found within that reviewed scope.
58
+ 8. With a sealed scope, record the evidence the CLI can check, in the same folder:
59
+ `findings.json` (`bmad-plus/review-findings/1`: every finding, refuted ones
60
+ included, each quoting `existing_code` verbatim from the file — never a line
61
+ number) and `coverage.json` (`bmad-plus/review-coverage/1`: every selected file
62
+ as completed, failed or waived with a reason). Run `bmad-plus review anchor <id>`
63
+ and requote any finding it reports ambiguous or unlocated; then
64
+ `bmad-plus review gate <id>`. Report its disposition as it is: `incomplete`
65
+ when a selected file is unaccounted for or a finding is not anchored, `findings`
66
+ or `clean` otherwise. `clean` covers the reviewed scope only.
46
67
 
47
68
  ## Output and acceptance
48
69
 
@@ -119,6 +119,7 @@ for (const modulePath of [
119
119
  './commands/studio',
120
120
  './commands/nexus',
121
121
  './commands/uat',
122
+ './commands/review',
122
123
  ]) {
123
124
  const command = require(modulePath);
124
125
  const configured = program.command(command.command).description(command.description);
@@ -0,0 +1,152 @@
1
+ /** Code review evidence: seal the scope, anchor the findings, derive the verdict from coverage. */
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const review = require('../lib/review');
7
+
8
+ const ID = /^[a-z0-9][a-z0-9.-]{0,80}$/;
9
+
10
+ function fail(message) {
11
+ throw new Error(message);
12
+ }
13
+
14
+ function readJson(file, what) {
15
+ if (!fs.existsSync(file)) return null;
16
+ try {
17
+ // PowerShell 5.1 writes UTF-8 with a byte-order mark; JSON.parse refuses it.
18
+ return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
19
+ } catch (error) {
20
+ fail(`${what} is not valid JSON (${error.message})`);
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function print(json, payload, lines) {
26
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, ...payload }, null, 2));
27
+ else for (const line of lines) console.log(line);
28
+ }
29
+
30
+ module.exports = {
31
+ command: 'review <action> [id]',
32
+ description: 'Code review evidence: scope, anchor, gate',
33
+ options: [
34
+ ['-d, --directory <path>', 'Project directory'],
35
+ ['--dir <path>', 'Review folder inside the project', review.DEFAULT_DIR],
36
+ ['--base <ref>', 'Base commit or branch (default HEAD~1)'],
37
+ ['--head <ref>', 'Head commit or branch (default HEAD)'],
38
+ ['--workspace', 'Review the working tree, untracked files included, against the base'],
39
+ ['--include <glob>', 'Only review matching paths (repeatable)', (v, all) => [...all, v], []],
40
+ ['--exclude <glob>', 'Never review matching paths (repeatable)', (v, all) => [...all, v], []],
41
+ ['--json', 'Machine-readable output'],
42
+ ],
43
+ action: (action, id, options = {}) => {
44
+ const projectDir = path.resolve(options.directory || process.cwd());
45
+ const json = Boolean(options.json);
46
+ try {
47
+ if (!id || !ID.test(id))
48
+ fail('a review id is required: lowercase letters, digits, dots and dashes');
49
+ if (!['scope', 'anchor', 'gate'].includes(action))
50
+ fail(`unknown action "${action}" (scope, anchor, gate)`);
51
+ const paths = review.layout(projectDir, options.dir || review.DEFAULT_DIR, id);
52
+
53
+ if (action === 'scope') {
54
+ const scope = review.buildScope(projectDir, {
55
+ id,
56
+ base: options.base,
57
+ head: options.head,
58
+ workspace: Boolean(options.workspace),
59
+ include: options.include,
60
+ exclude: options.exclude,
61
+ });
62
+ fs.mkdirSync(paths.root, { recursive: true });
63
+ fs.writeFileSync(paths.scope, `${JSON.stringify(scope, null, 2)}\n`);
64
+ const reasons = {};
65
+ for (const item of scope.excluded) reasons[item.reason] = (reasons[item.reason] || 0) + 1;
66
+ print(
67
+ json,
68
+ {
69
+ action,
70
+ id,
71
+ file: paths.scope,
72
+ totals: scope.totals,
73
+ units: scope.units.length,
74
+ excluded: reasons,
75
+ sha256: scope.sha256,
76
+ },
77
+ [
78
+ `${paths.scope}`,
79
+ `${scope.totals.selected} file(s) to review, ${scope.totals.lines} changed lines, ${scope.units.length} unit(s) — scope ${scope.sha256.slice(0, 12)}`,
80
+ ...Object.entries(reasons).map(([reason, n]) => ` excluded ${n} × ${reason}`),
81
+ 'Every selected file must end in coverage.json as completed, failed or waived (with a reason).',
82
+ ]
83
+ );
84
+ return;
85
+ }
86
+
87
+ const scope = readJson(paths.scope, 'scope.json');
88
+ if (!scope) fail(`no scope at ${paths.scope} — run "bmad-plus review scope ${id}" first`);
89
+
90
+ if (action === 'anchor') {
91
+ const findings = readJson(paths.findings, 'findings.json');
92
+ if (!findings) fail(`no findings at ${paths.findings}`);
93
+ const errors = review.validateFindings(findings, scope);
94
+ if (errors.length) {
95
+ print(
96
+ json,
97
+ { action, id, status: 'error', errors },
98
+ errors.map((e) => ` error ${e}`)
99
+ );
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ const anchored = review.anchorFindings(projectDir, scope, findings);
104
+ fs.writeFileSync(
105
+ paths.anchored,
106
+ `${JSON.stringify({ schema: review.FINDINGS_SCHEMA, scopeSha256: scope.sha256, ...anchored }, null, 2)}\n`
107
+ );
108
+ const loose = anchored.findings.filter((f) => f.location.status !== 'located');
109
+ print(json, { action, id, file: paths.anchored, counts: anchored.counts }, [
110
+ `${paths.anchored} — ${anchored.counts.located} located, ${anchored.counts.ambiguous} ambiguous, ${anchored.counts.unlocated} unlocated`,
111
+ ...loose.map(
112
+ (f) =>
113
+ ` ${f.location.status.padEnd(9)} ${f.id} ${f.path}: quote a longer, exact excerpt`
114
+ ),
115
+ ]);
116
+ process.exitCode = loose.length ? 1 : 0;
117
+ return;
118
+ }
119
+
120
+ if (action === 'gate') {
121
+ const findings = readJson(paths.findings, 'findings.json');
122
+ const coverage = readJson(paths.coverage, 'coverage.json');
123
+ let anchored = null;
124
+ if (findings && !review.validateFindings(findings, scope).length)
125
+ anchored = review.anchorFindings(projectDir, scope, findings);
126
+ const verdict = review.reviewGate({ scope, findings, coverage, anchored });
127
+ print(
128
+ json,
129
+ { action, id, ...verdict },
130
+ [
131
+ `${id}: ${verdict.status} — coverage ${verdict.coverage.completed}/${verdict.coverage.selected} completed (${verdict.coverage.waived} waived), ${verdict.findings.open} open finding(s), ${verdict.findings.refuted} refuted`,
132
+ ...verdict.reasons.map((reason) => ` - ${reason}`),
133
+ verdict.status === 'clean'
134
+ ? ' clean: no open finding within a fully covered scope. It does not prove the code correct.'
135
+ : '',
136
+ ].filter(Boolean)
137
+ );
138
+ process.exitCode = { clean: 0, findings: 1, incomplete: 2 }[verdict.status];
139
+ return;
140
+ }
141
+
142
+ fail(`unknown action "${action}" (scope, anchor, gate)`);
143
+ } catch (error) {
144
+ if (json)
145
+ console.log(
146
+ JSON.stringify({ schemaVersion: 1, status: 'error', message: error.message }, null, 2)
147
+ );
148
+ else console.error(`review: ${error.message}`);
149
+ process.exitCode = 3;
150
+ }
151
+ },
152
+ };
@@ -191,7 +191,7 @@ const DERIVED = {
191
191
  "product": {
192
192
  "code": "bmad-plus",
193
193
  "displayName": "BMAD+",
194
- "version": "0.18.0",
194
+ "version": "0.19.0",
195
195
  "derivedFrom": "BMAD-METHOD v6.6.0"
196
196
  },
197
197
  "packOrder": [
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Code review evidence: a sealed scope, findings anchored on quoted code, and a gate that
3
+ * derives the verdict from coverage — never from how many findings were written.
4
+ *
5
+ * The host agent reviews; this module only establishes what must be reviewed, where each
6
+ * finding really is, and whether every selected file was accounted for. Design taken from
7
+ * the open-code-review study (docs/research/open-code-review-2026-09-25): deterministic
8
+ * selection with named exclusion reasons, verbatim-snippet anchoring, coverage-derived
9
+ * disposition. No model call, no network, no write outside the review folder.
10
+ */
11
+ 'use strict';
12
+
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+ const crypto = require('node:crypto');
16
+ const { spawnSync } = require('node:child_process');
17
+
18
+ const SCOPE_SCHEMA = 'bmad-plus/review-scope/1';
19
+ const FINDINGS_SCHEMA = 'bmad-plus/review-findings/1';
20
+ const COVERAGE_SCHEMA = 'bmad-plus/review-coverage/1';
21
+ const DEFAULT_DIR = '_bmad-output/review';
22
+
23
+ const SEVERITIES = ['critical', 'high', 'medium', 'low'];
24
+ const CATEGORIES = [
25
+ 'correctness',
26
+ 'security',
27
+ 'data-loss',
28
+ 'concurrency',
29
+ 'performance',
30
+ 'error-handling',
31
+ 'compatibility',
32
+ 'test-gap',
33
+ 'maintainability',
34
+ 'documentation',
35
+ ];
36
+ const DISPOSITIONS = ['confirmed', 'refuted', 'unresolved'];
37
+ const CONFIDENCE = ['high', 'medium', 'low'];
38
+ const OUTCOMES = ['completed', 'failed', 'waived'];
39
+
40
+ /** Files whose content must never reach a review packet, whatever an include pattern says. */
41
+ const SECRET_PATTERNS = [
42
+ '**/.env',
43
+ '**/.env.*',
44
+ '**/*.pem',
45
+ '**/*.key',
46
+ '**/*.p12',
47
+ '**/*.pfx',
48
+ '**/id_rsa*',
49
+ '**/id_ed25519*',
50
+ '**/.credentials/**',
51
+ '**/credentials/**',
52
+ '**/secrets/**',
53
+ '**/*.keystore',
54
+ '**/.npmrc',
55
+ '**/.netrc',
56
+ ];
57
+ const GENERATED_PATTERNS = [
58
+ '**/node_modules/**',
59
+ '**/dist/**',
60
+ '**/build/**',
61
+ '**/coverage/**',
62
+ '**/vendor/**',
63
+ '**/*.min.js',
64
+ '**/*.min.css',
65
+ '**/*.map',
66
+ '**/package-lock.json',
67
+ '**/yarn.lock',
68
+ '**/pnpm-lock.yaml',
69
+ '**/Cargo.lock',
70
+ '**/go.sum',
71
+ '**/poetry.lock',
72
+ ];
73
+ const UNIT_LIMITS = { files: 8, lines: 400 };
74
+
75
+ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
76
+
77
+ /** A small glob: `**` spans directories, `*` stays within one, `?` is one character. Case-sensitive. */
78
+ function globToRegExp(glob) {
79
+ let source = '';
80
+ for (let i = 0; i < glob.length; i++) {
81
+ const c = glob[i];
82
+ if (c === '*' && glob[i + 1] === '*') {
83
+ const slash = glob[i + 2] === '/';
84
+ source += slash ? '(?:.*/)?' : '.*';
85
+ i += slash ? 2 : 1;
86
+ } else if (c === '*') source += '[^/]*';
87
+ else if (c === '?') source += '[^/]';
88
+ else source += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
89
+ }
90
+ return new RegExp(`^${source}$`);
91
+ }
92
+ const matchesAny = (file, patterns) => patterns.some((glob) => globToRegExp(glob).test(file));
93
+
94
+ function git(projectDir, args) {
95
+ const result = spawnSync('git', args, {
96
+ cwd: projectDir,
97
+ encoding: 'utf8',
98
+ maxBuffer: 64 * 1024 * 1024,
99
+ windowsHide: true,
100
+ shell: false,
101
+ });
102
+ if (result.error) throw new Error(`git is unavailable: ${result.error.message}`);
103
+ if (result.status !== 0)
104
+ throw new Error(`git ${args[0]} failed: ${(result.stderr || '').trim().split('\n')[0]}`);
105
+ return result.stdout;
106
+ }
107
+
108
+ /** A ref given on the command line is resolved to a commit or refused; it never reaches git as an option. */
109
+ function resolveCommit(projectDir, ref) {
110
+ if (typeof ref !== 'string' || !ref || ref.startsWith('-'))
111
+ throw new Error(`invalid ref "${ref}"`);
112
+ return git(projectDir, ['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`]).trim();
113
+ }
114
+
115
+ /** `git diff --numstat -z` entries: {path, oldPath?, added, removed, binary}. */
116
+ function parseNumstat(output) {
117
+ const parts = output.split('\0');
118
+ const entries = [];
119
+ for (let i = 0; i < parts.length; i++) {
120
+ const head = parts[i];
121
+ if (!head) continue;
122
+ const match = /^(-|\d+)\t(-|\d+)\t(.*)$/.exec(head);
123
+ if (!match) continue;
124
+ const [, added, removed, rest] = match;
125
+ const binary = added === '-' || removed === '-';
126
+ if (rest === '') {
127
+ // Rename or copy: old path and new path follow as separate fields.
128
+ const oldPath = parts[++i];
129
+ const newPath = parts[++i];
130
+ entries.push({ path: newPath, oldPath, added: +added || 0, removed: +removed || 0, binary });
131
+ } else {
132
+ entries.push({ path: rest, added: +added || 0, removed: +removed || 0, binary });
133
+ }
134
+ }
135
+ return entries;
136
+ }
137
+
138
+ function nameStatus(output) {
139
+ const parts = output.split('\0');
140
+ const status = new Map();
141
+ for (let i = 0; i < parts.length; i++) {
142
+ const code = parts[i];
143
+ if (!code) continue;
144
+ const kind = code[0];
145
+ if (kind === 'R' || kind === 'C') {
146
+ i += 2;
147
+ status.set(parts[i], kind === 'R' ? 'renamed' : 'copied');
148
+ } else {
149
+ i += 1;
150
+ status.set(
151
+ parts[i],
152
+ { A: 'added', D: 'deleted', M: 'modified', T: 'modified' }[kind] || 'modified'
153
+ );
154
+ }
155
+ }
156
+ return status;
157
+ }
158
+
159
+ /** Deterministic review units: files grouped by directory, cut at the file and line limits, in path order. */
160
+ function planUnits(selected, limits = UNIT_LIMITS) {
161
+ const units = [];
162
+ let current = null;
163
+ const flush = () => {
164
+ if (current && current.paths.length) units.push(current);
165
+ current = null;
166
+ };
167
+ for (const item of selected) {
168
+ const dir = path.posix.dirname(item.path);
169
+ const lines = item.added + item.removed;
170
+ if (
171
+ !current ||
172
+ current.dir !== dir ||
173
+ current.paths.length >= limits.files ||
174
+ (current.lines + lines > limits.lines && current.paths.length > 0)
175
+ ) {
176
+ flush();
177
+ current = { dir, paths: [], lines: 0 };
178
+ }
179
+ current.paths.push(item.path);
180
+ current.lines += lines;
181
+ }
182
+ flush();
183
+ return units.map((unit, index) => ({
184
+ id: `u${index + 1}`,
185
+ paths: unit.paths,
186
+ lines: unit.lines,
187
+ }));
188
+ }
189
+
190
+ /**
191
+ * The review scope: what changed between base and head (or the working tree), which files
192
+ * are selected, and why every other changed file is not. Secrets are excluded before any
193
+ * include pattern is considered.
194
+ */
195
+ function buildScope(projectDir, options = {}) {
196
+ const head = options.workspace ? null : resolveCommit(projectDir, options.head || 'HEAD');
197
+ const base = resolveCommit(projectDir, options.base || 'HEAD~1');
198
+ const mergeBase = git(projectDir, ['merge-base', base, head || 'HEAD']).trim();
199
+ const range = options.workspace ? [mergeBase] : [mergeBase, head];
200
+ const numstat = parseNumstat(
201
+ git(projectDir, ['diff', '--numstat', '-z', '--find-renames', ...range, '--'])
202
+ );
203
+ const statuses = nameStatus(
204
+ git(projectDir, ['diff', '--name-status', '-z', '--find-renames', ...range, '--'])
205
+ );
206
+ const changed = numstat.map((entry) => ({
207
+ ...entry,
208
+ status: statuses.get(entry.path) || 'modified',
209
+ }));
210
+ if (options.workspace) {
211
+ const untracked = git(projectDir, ['ls-files', '--others', '--exclude-standard', '-z'])
212
+ .split('\0')
213
+ .filter(Boolean);
214
+ for (const file of untracked) {
215
+ if (changed.some((entry) => entry.path === file)) continue;
216
+ let lines = 0;
217
+ let binary;
218
+ try {
219
+ const bytes = fs.readFileSync(path.join(projectDir, file));
220
+ binary = bytes.includes(0);
221
+ lines = binary ? 0 : bytes.toString('utf8').split('\n').length;
222
+ } catch {
223
+ binary = true;
224
+ }
225
+ changed.push({ path: file, added: lines, removed: 0, binary, status: 'added' });
226
+ }
227
+ }
228
+ changed.sort((a, b) => a.path.localeCompare(b.path));
229
+
230
+ const include = options.include || [];
231
+ const exclude = options.exclude || [];
232
+ const selected = [];
233
+ const excluded = [];
234
+ for (const entry of changed) {
235
+ const file = entry.path;
236
+ let reason = null;
237
+ if (matchesAny(file, SECRET_PATTERNS)) reason = 'secret';
238
+ else if (entry.binary) reason = 'binary';
239
+ else if (entry.status === 'deleted') reason = 'deleted';
240
+ else if (matchesAny(file, exclude)) reason = 'excluded-by-pattern';
241
+ else if (matchesAny(file, GENERATED_PATTERNS) && !matchesAny(file, include))
242
+ reason = 'generated-or-vendored';
243
+ else if (include.length && !matchesAny(file, include)) reason = 'not-included';
244
+ if (reason) excluded.push({ path: file, reason, status: entry.status });
245
+ else
246
+ selected.push({
247
+ path: file,
248
+ status: entry.status,
249
+ added: entry.added,
250
+ removed: entry.removed,
251
+ ...(entry.oldPath ? { oldPath: entry.oldPath } : {}),
252
+ });
253
+ }
254
+ const identity = {
255
+ base,
256
+ head: head || 'WORKTREE',
257
+ mergeBase,
258
+ workspace: Boolean(options.workspace),
259
+ include,
260
+ exclude,
261
+ };
262
+ const units = planUnits(selected, options.unitLimits);
263
+ const lines = selected.reduce((n, item) => n + item.added + item.removed, 0);
264
+ const body = { identity, selected, excluded, units };
265
+ return {
266
+ schema: SCOPE_SCHEMA,
267
+ id: options.id,
268
+ ...body,
269
+ totals: {
270
+ changed: changed.length,
271
+ selected: selected.length,
272
+ excluded: excluded.length,
273
+ lines,
274
+ },
275
+ sha256: sha256(JSON.stringify(body)),
276
+ };
277
+ }
278
+
279
+ // ── Findings ──────────────────────────────────────────────────────────────────
280
+
281
+ const FINDING_KEYS = [
282
+ 'id',
283
+ 'path',
284
+ 'existing_code',
285
+ 'content',
286
+ 'category',
287
+ 'severity',
288
+ 'confidence',
289
+ 'disposition',
290
+ 'trigger',
291
+ 'consequence',
292
+ 'evidence',
293
+ 'refutation',
294
+ 'fix',
295
+ ];
296
+
297
+ /** Unknown values are errors, never coerced: a wrong enum is a wrong finding. */
298
+ function validateFindings(doc, scope) {
299
+ const errors = [];
300
+ if (!doc || doc.schema !== FINDINGS_SCHEMA) errors.push(`schema must be "${FINDINGS_SCHEMA}"`);
301
+ if (scope && doc && doc.scopeSha256 !== scope.sha256)
302
+ errors.push(
303
+ 'scopeSha256 does not match the current scope — the findings answer another review'
304
+ );
305
+ const ids = new Set();
306
+ for (const [index, finding] of ((doc && doc.findings) || []).entries()) {
307
+ const at = `finding ${finding?.id ?? index}`;
308
+ for (const key of Object.keys(finding || {}))
309
+ if (!FINDING_KEYS.includes(key)) errors.push(`${at}: unknown key "${key}"`);
310
+ if (!finding.id || ids.has(finding.id)) errors.push(`${at}: id missing or duplicated`);
311
+ ids.add(finding.id);
312
+ if (typeof finding.path !== 'string' || !finding.path) errors.push(`${at}: path is required`);
313
+ if (typeof finding.existing_code !== 'string' || !finding.existing_code.trim())
314
+ errors.push(
315
+ `${at}: existing_code is required — quote the code the finding is about, verbatim`
316
+ );
317
+ if (typeof finding.content !== 'string' || !finding.content.trim())
318
+ errors.push(`${at}: content is required`);
319
+ if (!CATEGORIES.includes(finding.category))
320
+ errors.push(`${at}: category "${finding.category}" is not one of ${CATEGORIES.join('|')}`);
321
+ if (!SEVERITIES.includes(finding.severity))
322
+ errors.push(`${at}: severity "${finding.severity}" is not one of ${SEVERITIES.join('|')}`);
323
+ if (!CONFIDENCE.includes(finding.confidence))
324
+ errors.push(
325
+ `${at}: confidence "${finding.confidence}" is not one of ${CONFIDENCE.join('|')}`
326
+ );
327
+ if (!DISPOSITIONS.includes(finding.disposition))
328
+ errors.push(
329
+ `${at}: disposition "${finding.disposition}" is not one of ${DISPOSITIONS.join('|')}`
330
+ );
331
+ if (finding.disposition === 'refuted' && !String(finding.refutation || '').trim())
332
+ errors.push(`${at}: a refuted finding keeps its refutation — the quoted ground`);
333
+ if (finding.disposition === 'confirmed' && !String(finding.evidence || '').trim())
334
+ errors.push(`${at}: a confirmed finding needs evidence`);
335
+ if (scope && finding.path && !scope.selected.some((item) => item.path === finding.path))
336
+ errors.push(`${at}: ${finding.path} is not in the review scope`);
337
+ }
338
+ return errors;
339
+ }
340
+
341
+ const normalizeLine = (line) => line.replace(/\r$/, '').replace(/\s+/g, ' ').trim();
342
+
343
+ /**
344
+ * Where a quoted snippet is in a file: every place its lines appear consecutively, compared
345
+ * after whitespace normalisation. The model's line numbers are never used.
346
+ */
347
+ function locate(fileText, snippet) {
348
+ const fileLines = fileText.split('\n').map(normalizeLine);
349
+ const wanted = snippet
350
+ .split('\n')
351
+ .map(normalizeLine)
352
+ .filter((line, index, all) => line || (index > 0 && index < all.length - 1));
353
+ while (wanted.length && !wanted[0]) wanted.shift();
354
+ while (wanted.length && !wanted[wanted.length - 1]) wanted.pop();
355
+ if (!wanted.length) return [];
356
+ const hits = [];
357
+ for (let i = 0; i + wanted.length <= fileLines.length; i++) {
358
+ let ok = true;
359
+ for (let j = 0; j < wanted.length; j++) {
360
+ if (fileLines[i + j] !== wanted[j]) {
361
+ ok = false;
362
+ break;
363
+ }
364
+ }
365
+ if (ok) hits.push({ lineStart: i + 1, lineEnd: i + wanted.length });
366
+ }
367
+ return hits;
368
+ }
369
+
370
+ function readAtHead(projectDir, scope, file) {
371
+ if (scope.identity.workspace) return fs.readFileSync(path.join(projectDir, file), 'utf8');
372
+ return git(projectDir, ['show', `${scope.identity.head}:${file}`]);
373
+ }
374
+
375
+ /** Every finding gets a location status: located (unique), ambiguous (several), unlocated (none). */
376
+ function anchorFindings(projectDir, scope, doc) {
377
+ const cache = new Map();
378
+ const anchored = doc.findings.map((finding) => {
379
+ let text = cache.get(finding.path);
380
+ if (text === undefined) {
381
+ try {
382
+ text = readAtHead(projectDir, scope, finding.path);
383
+ } catch {
384
+ text = null;
385
+ }
386
+ cache.set(finding.path, text);
387
+ }
388
+ if (text === null)
389
+ return { ...finding, location: { status: 'unlocated', reason: 'file unreadable at head' } };
390
+ const hits = locate(text, finding.existing_code);
391
+ if (hits.length === 1) return { ...finding, location: { status: 'located', ...hits[0] } };
392
+ if (hits.length > 1)
393
+ return { ...finding, location: { status: 'ambiguous', candidates: hits.slice(0, 10) } };
394
+ return {
395
+ ...finding,
396
+ location: { status: 'unlocated', reason: 'the quoted code is not in the file' },
397
+ };
398
+ });
399
+ const counts = { located: 0, ambiguous: 0, unlocated: 0 };
400
+ for (const finding of anchored) counts[finding.location.status] += 1;
401
+ return { findings: anchored, counts };
402
+ }
403
+
404
+ // ── Coverage and gate ─────────────────────────────────────────────────────────
405
+
406
+ function validateCoverage(doc, scope) {
407
+ const errors = [];
408
+ if (!doc || doc.schema !== COVERAGE_SCHEMA) errors.push(`schema must be "${COVERAGE_SCHEMA}"`);
409
+ if (doc && doc.scopeSha256 !== scope.sha256)
410
+ errors.push('scopeSha256 does not match the current scope');
411
+ const seen = new Set();
412
+ for (const item of (doc && doc.items) || []) {
413
+ if (!scope.selected.some((entry) => entry.path === item.path))
414
+ errors.push(`${item.path}: not in the review scope`);
415
+ if (seen.has(item.path)) errors.push(`${item.path}: listed twice`);
416
+ seen.add(item.path);
417
+ if (!OUTCOMES.includes(item.outcome))
418
+ errors.push(`${item.path}: outcome "${item.outcome}" is not one of ${OUTCOMES.join('|')}`);
419
+ if (
420
+ (item.outcome === 'waived' || item.outcome === 'failed') &&
421
+ !String(item.reason || '').trim()
422
+ )
423
+ errors.push(`${item.path}: a ${item.outcome} file needs a reason`);
424
+ }
425
+ return errors;
426
+ }
427
+
428
+ /**
429
+ * The review verdict. `incomplete` whenever a selected file was not accounted for, failed,
430
+ * or the evidence is invalid; otherwise `findings` or `clean` — the latter only means no
431
+ * confirmed or unresolved finding within a fully covered scope.
432
+ */
433
+ function reviewGate({ scope, findings, coverage, anchored }) {
434
+ const reasons = [];
435
+ if (!coverage) reasons.push('no coverage.json: which files were reviewed is not established');
436
+ else reasons.push(...validateCoverage(coverage, scope));
437
+ if (!findings) reasons.push('no findings.json: write it even when there is nothing to report');
438
+ else reasons.push(...validateFindings(findings, scope));
439
+ const covered = new Map(((coverage && coverage.items) || []).map((item) => [item.path, item]));
440
+ const missing = scope.selected.filter((item) => !covered.has(item.path)).map((item) => item.path);
441
+ const failed = [...covered.values()]
442
+ .filter((item) => item.outcome === 'failed')
443
+ .map((item) => item.path);
444
+ const waived = [...covered.values()]
445
+ .filter((item) => item.outcome === 'waived')
446
+ .map((item) => item.path);
447
+ if (missing.length)
448
+ reasons.push(
449
+ `${missing.length} selected file(s) never accounted for: ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? '…' : ''}`
450
+ );
451
+ if (failed.length)
452
+ reasons.push(`${failed.length} file(s) whose review failed: ${failed.slice(0, 5).join(', ')}`);
453
+ const open = ((anchored && anchored.findings) || []).filter((f) => f.disposition !== 'refuted');
454
+ const unlocated = open.filter((f) => f.location.status !== 'located');
455
+ if (unlocated.length)
456
+ reasons.push(
457
+ `${unlocated.length} open finding(s) not anchored on the code at head: ${unlocated
458
+ .map((f) => f.id)
459
+ .slice(0, 5)
460
+ .join(', ')}`
461
+ );
462
+ const total = scope.selected.length;
463
+ const done = [...covered.values()].filter((item) => item.outcome === 'completed').length;
464
+ const coverageRate = total ? Math.round((done / total) * 1000) / 10 : 100;
465
+ const status = reasons.length ? 'incomplete' : open.length ? 'findings' : 'clean';
466
+ return {
467
+ status,
468
+ reasons,
469
+ coverage: {
470
+ selected: total,
471
+ completed: done,
472
+ waived: waived.length,
473
+ failed: failed.length,
474
+ missing: missing.length,
475
+ rate: coverageRate,
476
+ },
477
+ findings: {
478
+ open: open.length,
479
+ confirmed: open.filter((f) => f.disposition === 'confirmed').length,
480
+ unresolved: open.filter((f) => f.disposition === 'unresolved').length,
481
+ refuted: ((anchored && anchored.findings) || []).length - open.length,
482
+ bySeverity: Object.fromEntries(
483
+ SEVERITIES.map((s) => [s, open.filter((f) => f.severity === s).length])
484
+ ),
485
+ },
486
+ };
487
+ }
488
+
489
+ function layout(projectDir, dir = DEFAULT_DIR, id) {
490
+ const root = path.resolve(projectDir, dir, id);
491
+ return {
492
+ root,
493
+ scope: path.join(root, 'scope.json'),
494
+ findings: path.join(root, 'findings.json'),
495
+ anchored: path.join(root, 'findings.anchored.json'),
496
+ coverage: path.join(root, 'coverage.json'),
497
+ };
498
+ }
499
+
500
+ module.exports = {
501
+ SCOPE_SCHEMA,
502
+ FINDINGS_SCHEMA,
503
+ COVERAGE_SCHEMA,
504
+ DEFAULT_DIR,
505
+ SEVERITIES,
506
+ CATEGORIES,
507
+ DISPOSITIONS,
508
+ CONFIDENCE,
509
+ OUTCOMES,
510
+ SECRET_PATTERNS,
511
+ GENERATED_PATTERNS,
512
+ globToRegExp,
513
+ parseNumstat,
514
+ planUnits,
515
+ buildScope,
516
+ validateFindings,
517
+ locate,
518
+ anchorFindings,
519
+ validateCoverage,
520
+ reviewGate,
521
+ layout,
522
+ };
@@ -90,7 +90,7 @@ function readJson(file) {
90
90
  const stat = fs.lstatSync(file);
91
91
  if (stat.isSymbolicLink()) throw new Error(`${file} is a symbolic link; refused.`);
92
92
  if (stat.size > MAX_FILE) throw new Error(`${file} exceeds ${MAX_FILE} bytes.`);
93
- return JSON.parse(fs.readFileSync(file, 'utf8'));
93
+ return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); // PowerShell 5.1 writes a BOM
94
94
  }
95
95
 
96
96
  // ── Spec: legacy adapter, validation, lint ────────────────────────────────────