bmad-method 6.11.1-next.25 → 6.11.1-next.26

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.11.1-next.25",
4
+ "version": "6.11.1-next.26",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -692,7 +692,26 @@ def html_doc(rows: list[dict]) -> str:
692
692
  )
693
693
 
694
694
 
695
+ def pin_utf8(stream):
696
+ """Pin a console stream to UTF-8, keeping its own error handler.
697
+
698
+ `--extra` technique text and the technique names echoed to stderr are
699
+ arbitrary user input, so either stream can carry a character the platform
700
+ default cannot encode (cp1252 on Windows) and print() then raises.
701
+
702
+ errors= is passed through deliberately: reconfigure(encoding=...) alone
703
+ resets the handler to "strict", which would silently downgrade stderr's
704
+ POSIX default of "backslashreplace" and turn a diagnostic about an
705
+ undecodable path into a traceback.
706
+ """
707
+ reconfigure = getattr(stream, "reconfigure", None)
708
+ if reconfigure is not None:
709
+ reconfigure(encoding="utf-8", errors=getattr(stream, "errors", None) or "strict")
710
+
711
+
695
712
  def main(argv: list[str] | None = None) -> int:
713
+ pin_utf8(sys.stdout)
714
+ pin_utf8(sys.stderr)
696
715
  p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
697
716
  p.add_argument("--file", type=Path, default=DEFAULT_FILE, help="technique CSV (default: sibling assets/brain-methods.csv)")
698
717
  p.add_argument("--extra", type=Path, help="JSON overlay of additional techniques (customize.toml additional_techniques), merged into every command")
@@ -3,6 +3,7 @@
3
3
  # dependencies = ["pytest>=8.0"]
4
4
  # ///
5
5
  """Tests for brain.py. Run: uv run -m pytest scripts/tests/test_brain.py"""
6
+ import io
6
7
  import json
7
8
  import sys
8
9
  from pathlib import Path
@@ -227,6 +228,55 @@ def test_unknown_category_style_uses_fallback_glyph():
227
228
  assert glyph == brain._FALLBACK_GLYPH
228
229
 
229
230
 
231
+ # --- console encoding (Windows cp1252) ----------------------------------
232
+
233
+ def _cp1252_stream():
234
+ """A text stream that behaves like a Windows console: cp1252, strict."""
235
+ return io.TextIOWrapper(io.BytesIO(), encoding="cp1252", errors="strict", write_through=True)
236
+
237
+
238
+ def test_extra_technique_prints_when_stdout_encoding_is_cp1252(lib, tmp_path, monkeypatch):
239
+ # --extra text is arbitrary user input; the shipped catalog happens to be
240
+ # cp1252-safe, an overlay is not. Unpinned stdout raises UnicodeEncodeError
241
+ # and the command exits having printed nothing.
242
+ overlay = tmp_path / "extra.json"
243
+ overlay.write_text(
244
+ json.dumps([{"category": "wild", "technique_name": "Fikir Fırtınası 🌪", "description": "Beyin fırtınası — 日本語"}]),
245
+ encoding="utf-8",
246
+ )
247
+ fake = _cp1252_stream()
248
+ monkeypatch.setattr(sys, "stdout", fake)
249
+ assert brain.main(["--file", str(lib), "--extra", str(overlay), "list", "--all"]) == 0
250
+ out = fake.buffer.getvalue().decode("utf-8")
251
+ assert "Fikir Fırtınası 🌪" in out
252
+ assert "日本語" in out
253
+
254
+
255
+ def test_missing_technique_name_reports_when_stderr_encoding_is_cp1252(lib, monkeypatch):
256
+ # `show` echoes the name it could not find; that name came from argv.
257
+ fake = _cp1252_stream()
258
+ monkeypatch.setattr(sys, "stderr", fake)
259
+ assert brain.main(["--file", str(lib), "show", "日本語"]) == 1
260
+ assert "日本語" in fake.buffer.getvalue().decode("utf-8")
261
+
262
+
263
+ def test_pin_utf8_preserves_the_streams_error_handler():
264
+ # reconfigure(encoding=...) on its own resets errors to "strict"; stderr on
265
+ # POSIX defaults to "backslashreplace" and must keep it, or a diagnostic
266
+ # carrying a surrogate-escaped path becomes a traceback.
267
+ stream = io.TextIOWrapper(io.BytesIO(), encoding="ascii", errors="backslashreplace")
268
+ brain.pin_utf8(stream)
269
+ assert stream.encoding == "utf-8"
270
+ assert stream.errors == "backslashreplace"
271
+
272
+
273
+ def test_pin_utf8_ignores_a_stream_without_reconfigure():
274
+ class Captured: # e.g. pytest's capture object, or a StringIO stand-in
275
+ errors = None
276
+
277
+ brain.pin_utf8(Captured()) # must not raise
278
+
279
+
230
280
  def test_shipped_selector_is_in_sync_with_catalog():
231
281
  # foolproofing: if someone edits brain-methods.csv they must regenerate the page.
232
282
  # Regenerate with: uv run brain.py html --out assets/brain-selector.html