ssmforge 0.2.2__tar.gz → 0.2.3__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.
Files changed (24) hide show
  1. {ssmforge-0.2.2/src/ssmforge.egg-info → ssmforge-0.2.3}/PKG-INFO +72 -2
  2. {ssmforge-0.2.2 → ssmforge-0.2.3}/README.md +71 -1
  3. {ssmforge-0.2.2 → ssmforge-0.2.3}/pyproject.toml +1 -1
  4. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/__init__.py +1 -1
  5. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/cli.py +177 -9
  6. {ssmforge-0.2.2 → ssmforge-0.2.3/src/ssmforge.egg-info}/PKG-INFO +72 -2
  7. {ssmforge-0.2.2 → ssmforge-0.2.3}/LICENSE +0 -0
  8. {ssmforge-0.2.2 → ssmforge-0.2.3}/MANIFEST.in +0 -0
  9. {ssmforge-0.2.2 → ssmforge-0.2.3}/docs/DEPLOY.md +0 -0
  10. {ssmforge-0.2.2 → ssmforge-0.2.3}/docs/INSTALL.md +0 -0
  11. {ssmforge-0.2.2 → ssmforge-0.2.3}/docs/quickstart.md +0 -0
  12. {ssmforge-0.2.2 → ssmforge-0.2.3}/setup.cfg +0 -0
  13. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/analyze/__init__.py +0 -0
  14. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/analyze/diff.py +0 -0
  15. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/analyze/graph.py +0 -0
  16. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/analyze/markdown_render.py +0 -0
  17. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/analyze/report.py +0 -0
  18. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/analyze/state_dict_scan.py +0 -0
  19. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge/analyze/summary.py +0 -0
  20. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge.egg-info/SOURCES.txt +0 -0
  21. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge.egg-info/dependency_links.txt +0 -0
  22. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge.egg-info/entry_points.txt +0 -0
  23. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge.egg-info/requires.txt +0 -0
  24. {ssmforge-0.2.2 → ssmforge-0.2.3}/src/ssmforge.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssmforge
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Standalone HF architecture analyzer (`ssmforge arch`). Inspects any HuggingFace model and reports architectural quirks — attention biases, fused QKV/gate-up, tied embeddings, GQA, MQA, MoE, sliding window, LayerScale, soft-capping, partial RoPE, MLP type, norm type. Outputs structured JSON + human-readable summary.
5
5
  Author: SSMForge Contributors
6
6
  License-Expression: Apache-2.0
@@ -82,6 +82,28 @@ visible *before* you spend the next hour finding out the hard way.
82
82
  It does **not** modify the model. It does **not** run inference. It only
83
83
  inspects.
84
84
 
85
+ ### What's new in 0.2.3
86
+
87
+ | Fix | What was broken |
88
+ |-----|-----------------|
89
+ | `--output` on Git Bash + Windows with unquoted backslash paths | Git Bash strips backslashes from unquoted args, so `--output C:\Users\foo\out.md` reached Python as `C:Usersfooout.md` — silently writing a single literal-named file in the cwd. Pre-parse guard now catches this BEFORE argparse runs and exits 1 with a clear "wrap in double quotes or use forward slashes" hint. |
90
+ | `C:Usersnetge...` (bash-mangled, no separators) detected | The post-write `_check_output_path` was too late. The pre-parse guard catches the mangled pattern early and tells you exactly what bash did to your argument. |
91
+
92
+ **Quote your paths from now on.** Or use forward slashes — they work on both Windows and POSIX.
93
+
94
+ ```bash
95
+ # ✅ Recommended — works everywhere
96
+ python -m ssmforge.cli arch X --graph --format markdown --output ~/Desktop/report.md
97
+ python -m ssmforge.cli arch X --graph --format markdown --output ./report.md
98
+ python -m ssmforge.cli arch X --graph --format markdown --output "C:/Users/me/Desktop/report.md"
99
+
100
+ # ❌ These will now fail with a clear error:
101
+ python -m ssmforge.cli arch X --graph --format markdown --output C:\Users\me\Desktop\report.md
102
+ python -m ssmforge.cli arch X --graph --format markdown --output C:UsersmeDesktopreport.md
103
+ ```
104
+
105
+ Plus 9 new tests (174 total).
106
+
85
107
  ### What's new in 0.2.2
86
108
 
87
109
  | Fix | What was broken |
@@ -1400,7 +1422,55 @@ install.
1400
1422
 
1401
1423
  ## 16. Update log
1402
1424
 
1403
- ### v0.2.2 (current) — 2026-09-23
1425
+ ### v0.2.3 (current) — 2026-09-23
1426
+
1427
+ **Bug fix: Git Bash strips backslashes from unquoted `--output` args**
1428
+
1429
+ When you run on Git Bash + Windows and pass an unquoted Windows path:
1430
+ ```bash
1431
+ python -m ssmforge.cli arch X --graph --format markdown \
1432
+ --output C:\Users\netge\Desktop\report.md
1433
+ ```
1434
+
1435
+ Git Bash sees the backslashes as path separators and strips them
1436
+ **before** passing the argument to Python. Python receives:
1437
+ ```
1438
+ --output C:UsersnetgeDesktopreport.md
1439
+ ```
1440
+ which is interpreted as a single literal filename containing no
1441
+ separators. The CLI then writes a file named
1442
+ `./C:UsersnetgeDesktopreport.md` in the current directory — totally
1443
+ wrong, and v0.2.2 didn't catch it.
1444
+
1445
+ **v0.2.3 fixes this with a pre-parse guard** that runs before argparse
1446
+ and checks for:
1447
+
1448
+ 1. **Literal backslash paths** (`C:\\file.md`) — caught with hint to
1449
+ use forward slashes or wrap in quotes.
1450
+ 2. **Bash-mangled paths** (`C:UsersnetgeDesktop.md`) — caught with
1451
+ hint explaining what bash did to the argument.
1452
+
1453
+ The error fires immediately, before any model is loaded:
1454
+
1455
+ ```
1456
+ Error: --output value looks like a bash-mangled Windows path.
1457
+ Got: 'C:UsersnetgeDesktopreport.md'
1458
+ Git Bash strips backslashes from unquoted arguments.
1459
+ Wrap the value in double quotes, e.g.:
1460
+ --output "C:\Users\netge\Desktop\report.md"
1461
+ Or use forward slashes (works on both Windows and POSIX):
1462
+ --output C:/Users/netge/Desktop/report.md
1463
+ ```
1464
+
1465
+ **Recommended from v0.2.3 onward:** quote all `--output` paths, or
1466
+ use forward slashes.
1467
+
1468
+ Tests: 174 passed (was 165). 9 new tests for the pre-parse guard
1469
+ covering bash-mangled separate-args, bash-mangled equals-form,
1470
+ backslash-only paths, plain POSIX paths, forward-slash Windows paths,
1471
+ and bare drive-letter patterns.
1472
+
1473
+ ### v0.2.2 — 2026-09-23
1404
1474
 
1405
1475
  **Bug fix: `--output PATH` directory validation**
1406
1476
 
@@ -59,6 +59,28 @@ visible *before* you spend the next hour finding out the hard way.
59
59
  It does **not** modify the model. It does **not** run inference. It only
60
60
  inspects.
61
61
 
62
+ ### What's new in 0.2.3
63
+
64
+ | Fix | What was broken |
65
+ |-----|-----------------|
66
+ | `--output` on Git Bash + Windows with unquoted backslash paths | Git Bash strips backslashes from unquoted args, so `--output C:\Users\foo\out.md` reached Python as `C:Usersfooout.md` — silently writing a single literal-named file in the cwd. Pre-parse guard now catches this BEFORE argparse runs and exits 1 with a clear "wrap in double quotes or use forward slashes" hint. |
67
+ | `C:Usersnetge...` (bash-mangled, no separators) detected | The post-write `_check_output_path` was too late. The pre-parse guard catches the mangled pattern early and tells you exactly what bash did to your argument. |
68
+
69
+ **Quote your paths from now on.** Or use forward slashes — they work on both Windows and POSIX.
70
+
71
+ ```bash
72
+ # ✅ Recommended — works everywhere
73
+ python -m ssmforge.cli arch X --graph --format markdown --output ~/Desktop/report.md
74
+ python -m ssmforge.cli arch X --graph --format markdown --output ./report.md
75
+ python -m ssmforge.cli arch X --graph --format markdown --output "C:/Users/me/Desktop/report.md"
76
+
77
+ # ❌ These will now fail with a clear error:
78
+ python -m ssmforge.cli arch X --graph --format markdown --output C:\Users\me\Desktop\report.md
79
+ python -m ssmforge.cli arch X --graph --format markdown --output C:UsersmeDesktopreport.md
80
+ ```
81
+
82
+ Plus 9 new tests (174 total).
83
+
62
84
  ### What's new in 0.2.2
63
85
 
64
86
  | Fix | What was broken |
@@ -1377,7 +1399,55 @@ install.
1377
1399
 
1378
1400
  ## 16. Update log
1379
1401
 
1380
- ### v0.2.2 (current) — 2026-09-23
1402
+ ### v0.2.3 (current) — 2026-09-23
1403
+
1404
+ **Bug fix: Git Bash strips backslashes from unquoted `--output` args**
1405
+
1406
+ When you run on Git Bash + Windows and pass an unquoted Windows path:
1407
+ ```bash
1408
+ python -m ssmforge.cli arch X --graph --format markdown \
1409
+ --output C:\Users\netge\Desktop\report.md
1410
+ ```
1411
+
1412
+ Git Bash sees the backslashes as path separators and strips them
1413
+ **before** passing the argument to Python. Python receives:
1414
+ ```
1415
+ --output C:UsersnetgeDesktopreport.md
1416
+ ```
1417
+ which is interpreted as a single literal filename containing no
1418
+ separators. The CLI then writes a file named
1419
+ `./C:UsersnetgeDesktopreport.md` in the current directory — totally
1420
+ wrong, and v0.2.2 didn't catch it.
1421
+
1422
+ **v0.2.3 fixes this with a pre-parse guard** that runs before argparse
1423
+ and checks for:
1424
+
1425
+ 1. **Literal backslash paths** (`C:\\file.md`) — caught with hint to
1426
+ use forward slashes or wrap in quotes.
1427
+ 2. **Bash-mangled paths** (`C:UsersnetgeDesktop.md`) — caught with
1428
+ hint explaining what bash did to the argument.
1429
+
1430
+ The error fires immediately, before any model is loaded:
1431
+
1432
+ ```
1433
+ Error: --output value looks like a bash-mangled Windows path.
1434
+ Got: 'C:UsersnetgeDesktopreport.md'
1435
+ Git Bash strips backslashes from unquoted arguments.
1436
+ Wrap the value in double quotes, e.g.:
1437
+ --output "C:\Users\netge\Desktop\report.md"
1438
+ Or use forward slashes (works on both Windows and POSIX):
1439
+ --output C:/Users/netge/Desktop/report.md
1440
+ ```
1441
+
1442
+ **Recommended from v0.2.3 onward:** quote all `--output` paths, or
1443
+ use forward slashes.
1444
+
1445
+ Tests: 174 passed (was 165). 9 new tests for the pre-parse guard
1446
+ covering bash-mangled separate-args, bash-mangled equals-form,
1447
+ backslash-only paths, plain POSIX paths, forward-slash Windows paths,
1448
+ and bare drive-letter patterns.
1449
+
1450
+ ### v0.2.2 — 2026-09-23
1381
1451
 
1382
1452
  **Bug fix: `--output PATH` directory validation**
1383
1453
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ssmforge"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Standalone HF architecture analyzer (`ssmforge arch`). Inspects any HuggingFace model and reports architectural quirks — attention biases, fused QKV/gate-up, tied embeddings, GQA, MQA, MoE, sliding window, LayerScale, soft-capping, partial RoPE, MLP type, norm type. Outputs structured JSON + human-readable summary."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,3 +1,3 @@
1
1
  """SSMForge — architecture analyzer for HuggingFace models."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.3"
@@ -290,22 +290,67 @@ def _check_output_path(output_path: str | None) -> tuple[Path | None, str | None
290
290
  - ``(parent_dir, None)`` when the directory exists and is writable.
291
291
  - ``(parent_dir, error_message)`` when the path can't be written.
292
292
 
293
- The error_message is a user-friendly hint that names the actual problem
294
- (missing parent dir, not a directory, permission denied) and suggests
295
- writable alternatives when appropriate.
293
+ Detects two common Windows-MINGW64 traps:
294
+ 1. Literal backslash paths (`C:\\file.md` unquoted) — POSIX `Path`
295
+ treats them as single-component filenames. Hint: quote it.
296
+ 2. Bash-mangled paths (`C:UsersnetgeDesktop.md`) — when Git Bash
297
+ strips backslashes from an unquoted argument, the resulting
298
+ string looks like a Windows drive path with no separators. Hint:
299
+ quote the argument or use forward slashes.
296
300
  """
297
301
  if not output_path or output_path == "-":
298
302
  return None, None
299
- p = Path(output_path)
300
- parent = p.parent if str(p.parent) else Path(".")
301
- # Catch a bare filename like 'report.md' (parent is '.') which DOES work
302
- parent = parent.resolve()
303
+
304
+ raw = output_path
305
+ bs = chr(92)
306
+ fs = "/"
307
+
308
+ # Trap 1: Literal backslashes without forward slashes — POSIX `Path`
309
+ # will treat this as a single filename, not a path.
310
+ if bs in raw and fs not in raw:
311
+ return None, (
312
+ f"Error: output path uses backslashes without forward slashes.\n"
313
+ f" Got: {raw!r}\n"
314
+ f" On Git Bash + Windows, backslash paths must be quoted:\n"
315
+ f" --output \"{raw}\" # wrap in double quotes\n"
316
+ f" Or use forward slashes (work on both Windows and POSIX):\n"
317
+ f" --output {raw.replace(bs, fs)}"
318
+ )
319
+
320
+ # Trap 2: Bash-mangled Windows path — `C:` followed by a sequence of
321
+ # identifiers concatenated without separators. Real Windows paths
322
+ # always contain separators between components.
323
+ if (
324
+ len(raw) >= 3
325
+ and raw[1] == ":"
326
+ and raw[0].isalpha()
327
+ and fs not in raw
328
+ and bs not in raw
329
+ and len(raw) > 2
330
+ ):
331
+ # Looks like `C:UsersnetgeDesktop` (bash mangled) instead of
332
+ # `C:/Users/netge/Desktop` (real path).
333
+ return None, (
334
+ f"Error: output path looks like a Windows path with all separators stripped.\n"
335
+ f" Got: {raw!r}\n"
336
+ f" Git Bash strips backslashes from unquoted arguments. Wrap in quotes:\n"
337
+ f" --output \"C:{bs}Users{bs}netge{bs}Desktop{bs}report.md\"\n"
338
+ f" Or use forward slashes (POSIX-style, works on Windows):\n"
339
+ f" --output C:/Users/netge/Desktop/report.md"
340
+ )
341
+
342
+ # Use os.path for parent resolution to match what write_text will do.
343
+ abs_target = os.path.abspath(raw)
344
+ parent_str = os.path.dirname(abs_target) or "."
345
+ parent = Path(parent_str)
346
+
303
347
  # Case 1: parent doesn't exist
304
348
  if not parent.exists():
305
349
  return parent, (
306
350
  f"Error: output directory does not exist: {parent}\n"
307
351
  f" Hint: create the directory first, or use a writable location.\n"
308
- f" Try: --output ./report.md (current working directory)"
352
+ f" Try: --output ./report.md (current working directory)\n"
353
+ f" --output ~/report.md (your home directory)"
309
354
  )
310
355
  # Case 2: parent exists but is a file (not a dir)
311
356
  if parent.is_file():
@@ -325,12 +370,105 @@ def _check_output_path(output_path: str | None) -> tuple[Path | None, str | None
325
370
  return parent, None
326
371
 
327
372
 
373
+ def _looks_like_bash_mangled_path(value: str) -> bool:
374
+ """Detect a string that's likely been mangled by Git Bash.
375
+
376
+ Patterns detected:
377
+ - Drive letter + no separators: 'C:UsersnetgeDesktop.md'
378
+ - Drive letter + backslashes only: 'C:\\\\Users\\\\...'
379
+
380
+ Returns True if the value looks like a Windows path whose separators
381
+ have been stripped. Returns False for plain POSIX paths or quoted
382
+ Windows paths (which contain forward slashes or backslashes that
383
+ survived quoting).
384
+ """
385
+ if len(value) < 3:
386
+ return False
387
+ # Must start with drive letter + colon
388
+ if not (value[0].isalpha() and value[1] == ":"):
389
+ return False
390
+ # Must have no separators (the mangling signature)
391
+ if "/" in value or "\\" in value:
392
+ return False
393
+ # Drive + filename only (`C:foo.md`) is ambiguous on Windows and
394
+ # almost certainly unintended — flag it.
395
+ return True
396
+
397
+
398
+ def _pre_parse_path_guard(argv: list[str]) -> None:
399
+ """Pre-parse guard: detect bash-mangled --output values BEFORE argparse.
400
+
401
+ Runs before argparse consumes the args. If `--output VALUE` has a
402
+ mangled VALUE, prints a clear error and exits 1 — so the user sees
403
+ the issue immediately rather than after the model loads.
404
+
405
+ Also detects bare backslash paths (with backslashes intact) which
406
+ POSIX Python interprets as literal filenames.
407
+ """
408
+ bs = chr(92)
409
+ fs = "/"
410
+ i = 0
411
+ while i < len(argv):
412
+ arg = argv[i]
413
+ # --output VALUE (separate args)
414
+ if arg == "--output" or arg == "-o":
415
+ if i + 1 < len(argv):
416
+ value = argv[i + 1]
417
+ if _looks_like_bash_mangled_path(value):
418
+ print(
419
+ f"Error: --output value looks like a bash-mangled Windows path.\n"
420
+ f" Got: {value!r}\n"
421
+ f" Git Bash strips backslashes from unquoted arguments.\n"
422
+ f" Wrap the value in double quotes, e.g.:\n"
423
+ f" --output \"C:{bs}Users{bs}netge{bs}Desktop{bs}report.md\"\n"
424
+ f" Or use forward slashes (works on both Windows and POSIX):\n"
425
+ f" --output C:/Users/netge/Desktop/report.md",
426
+ file=sys.stderr,
427
+ )
428
+ sys.exit(1)
429
+ if bs in value and fs not in value:
430
+ print(
431
+ f"Error: --output uses backslashes without forward slashes.\n"
432
+ f" Got: {value!r}\n"
433
+ f" Wrap in double quotes:\n"
434
+ f" --output \"{value}\"\n"
435
+ f" Or use forward slashes:\n"
436
+ f" --output {value.replace(bs, fs)}",
437
+ file=sys.stderr,
438
+ )
439
+ sys.exit(1)
440
+ i += 2
441
+ continue
442
+ # --output=VALUE (combined) or --output=anything
443
+ if arg.startswith("--output=") or arg.startswith("-o="):
444
+ value = arg.split("=", 1)[1]
445
+ if _looks_like_bash_mangled_path(value):
446
+ print(
447
+ f"Error: --output value looks like a bash-mangled Windows path.\n"
448
+ f" Got: {value!r}\n"
449
+ f" Wrap the value in double quotes or use forward slashes.",
450
+ file=sys.stderr,
451
+ )
452
+ sys.exit(1)
453
+ if bs in value and fs not in value:
454
+ print(
455
+ f"Error: --output uses backslashes without forward slashes.\n"
456
+ f" Got: {value!r}",
457
+ file=sys.stderr,
458
+ )
459
+ sys.exit(1)
460
+ i += 1
461
+
462
+
328
463
  def _write_or_print(text: str, output_path: str | None) -> None:
329
464
  """Write text to a file if path given, else print to stdout.
330
465
 
331
466
  Special-case: if output_path is '-', print to stdout (Unix convention).
332
467
  Pre-flight checks the parent directory exists and is writable; on
333
468
  failure prints a friendly hint and exits 1.
469
+ After writing, verifies the file exists on disk (defends against
470
+ silently-successful-to-nowhere writes on Windows MINGW64 where
471
+ backslash paths can be misinterpreted as relative filenames).
334
472
  """
335
473
  if output_path and output_path != "-":
336
474
  parent, err = _check_output_path(output_path)
@@ -347,7 +485,30 @@ def _write_or_print(text: str, output_path: str | None) -> None:
347
485
  file=sys.stderr,
348
486
  )
349
487
  sys.exit(1)
350
- print(f"Report written to {output_path}", file=sys.stderr)
488
+ # Post-write verification: catch the case where Python writes to a
489
+ # path on disk that the user didn't intend (e.g., a literal
490
+ # 'C:\nope.md' filename in the current directory on POSIX).
491
+ resolved = p.resolve()
492
+ if not resolved.exists():
493
+ print(
494
+ f"Error: write appeared to succeed but file not found.\n"
495
+ f" Target: {resolved}\n"
496
+ f" Hint: try a relative path like --output ./report.md",
497
+ file=sys.stderr,
498
+ )
499
+ sys.exit(1)
500
+ # If the user gave a Windows-style absolute path with backslashes
501
+ # but it landed in the current directory as a literal filename,
502
+ # warn them.
503
+ bs = chr(92)
504
+ if bs in str(output_path) and "/" not in str(output_path) and resolved.parent == Path(".").resolve():
505
+ print(
506
+ f"Warning: wrote to a literal filename {resolved!r}\n"
507
+ f" Did you mean a Windows-style path? On Git Bash + Windows,\n"
508
+ f" use forward slashes: --output {str(output_path).replace(bs, '/')}",
509
+ file=sys.stderr,
510
+ )
511
+ print(f"Report written to {resolved}", file=sys.stderr)
351
512
  else:
352
513
  print(text)
353
514
 
@@ -656,6 +817,13 @@ def main(argv: list[str] | None = None) -> None:
656
817
  "can use it in scripts.",
657
818
  )
658
819
 
820
+ # Pre-parse guard: catch common path-mangling issues from Git Bash on
821
+ # Windows BEFORE argparse consumes the args. Without this, an unquoted
822
+ # `--output C:\Users\foo\out.md` becomes `C:Usersfooout.md` after bash
823
+ # strips the backslashes, then argparse accepts the garbage silently.
824
+ if argv:
825
+ _pre_parse_path_guard(argv)
826
+
659
827
  args = parser.parse_args(argv)
660
828
 
661
829
  if args.command == "arch":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssmforge
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Standalone HF architecture analyzer (`ssmforge arch`). Inspects any HuggingFace model and reports architectural quirks — attention biases, fused QKV/gate-up, tied embeddings, GQA, MQA, MoE, sliding window, LayerScale, soft-capping, partial RoPE, MLP type, norm type. Outputs structured JSON + human-readable summary.
5
5
  Author: SSMForge Contributors
6
6
  License-Expression: Apache-2.0
@@ -82,6 +82,28 @@ visible *before* you spend the next hour finding out the hard way.
82
82
  It does **not** modify the model. It does **not** run inference. It only
83
83
  inspects.
84
84
 
85
+ ### What's new in 0.2.3
86
+
87
+ | Fix | What was broken |
88
+ |-----|-----------------|
89
+ | `--output` on Git Bash + Windows with unquoted backslash paths | Git Bash strips backslashes from unquoted args, so `--output C:\Users\foo\out.md` reached Python as `C:Usersfooout.md` — silently writing a single literal-named file in the cwd. Pre-parse guard now catches this BEFORE argparse runs and exits 1 with a clear "wrap in double quotes or use forward slashes" hint. |
90
+ | `C:Usersnetge...` (bash-mangled, no separators) detected | The post-write `_check_output_path` was too late. The pre-parse guard catches the mangled pattern early and tells you exactly what bash did to your argument. |
91
+
92
+ **Quote your paths from now on.** Or use forward slashes — they work on both Windows and POSIX.
93
+
94
+ ```bash
95
+ # ✅ Recommended — works everywhere
96
+ python -m ssmforge.cli arch X --graph --format markdown --output ~/Desktop/report.md
97
+ python -m ssmforge.cli arch X --graph --format markdown --output ./report.md
98
+ python -m ssmforge.cli arch X --graph --format markdown --output "C:/Users/me/Desktop/report.md"
99
+
100
+ # ❌ These will now fail with a clear error:
101
+ python -m ssmforge.cli arch X --graph --format markdown --output C:\Users\me\Desktop\report.md
102
+ python -m ssmforge.cli arch X --graph --format markdown --output C:UsersmeDesktopreport.md
103
+ ```
104
+
105
+ Plus 9 new tests (174 total).
106
+
85
107
  ### What's new in 0.2.2
86
108
 
87
109
  | Fix | What was broken |
@@ -1400,7 +1422,55 @@ install.
1400
1422
 
1401
1423
  ## 16. Update log
1402
1424
 
1403
- ### v0.2.2 (current) — 2026-09-23
1425
+ ### v0.2.3 (current) — 2026-09-23
1426
+
1427
+ **Bug fix: Git Bash strips backslashes from unquoted `--output` args**
1428
+
1429
+ When you run on Git Bash + Windows and pass an unquoted Windows path:
1430
+ ```bash
1431
+ python -m ssmforge.cli arch X --graph --format markdown \
1432
+ --output C:\Users\netge\Desktop\report.md
1433
+ ```
1434
+
1435
+ Git Bash sees the backslashes as path separators and strips them
1436
+ **before** passing the argument to Python. Python receives:
1437
+ ```
1438
+ --output C:UsersnetgeDesktopreport.md
1439
+ ```
1440
+ which is interpreted as a single literal filename containing no
1441
+ separators. The CLI then writes a file named
1442
+ `./C:UsersnetgeDesktopreport.md` in the current directory — totally
1443
+ wrong, and v0.2.2 didn't catch it.
1444
+
1445
+ **v0.2.3 fixes this with a pre-parse guard** that runs before argparse
1446
+ and checks for:
1447
+
1448
+ 1. **Literal backslash paths** (`C:\\file.md`) — caught with hint to
1449
+ use forward slashes or wrap in quotes.
1450
+ 2. **Bash-mangled paths** (`C:UsersnetgeDesktop.md`) — caught with
1451
+ hint explaining what bash did to the argument.
1452
+
1453
+ The error fires immediately, before any model is loaded:
1454
+
1455
+ ```
1456
+ Error: --output value looks like a bash-mangled Windows path.
1457
+ Got: 'C:UsersnetgeDesktopreport.md'
1458
+ Git Bash strips backslashes from unquoted arguments.
1459
+ Wrap the value in double quotes, e.g.:
1460
+ --output "C:\Users\netge\Desktop\report.md"
1461
+ Or use forward slashes (works on both Windows and POSIX):
1462
+ --output C:/Users/netge/Desktop/report.md
1463
+ ```
1464
+
1465
+ **Recommended from v0.2.3 onward:** quote all `--output` paths, or
1466
+ use forward slashes.
1467
+
1468
+ Tests: 174 passed (was 165). 9 new tests for the pre-parse guard
1469
+ covering bash-mangled separate-args, bash-mangled equals-form,
1470
+ backslash-only paths, plain POSIX paths, forward-slash Windows paths,
1471
+ and bare drive-letter patterns.
1472
+
1473
+ ### v0.2.2 — 2026-09-23
1404
1474
 
1405
1475
  **Bug fix: `--output PATH` directory validation**
1406
1476
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes