fromargs 0.2.0__tar.gz → 0.2.1__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 (29) hide show
  1. {fromargs-0.2.0 → fromargs-0.2.1}/.gitignore +6 -0
  2. {fromargs-0.2.0 → fromargs-0.2.1}/CHANGELOG.md +22 -3
  3. {fromargs-0.2.0 → fromargs-0.2.1}/PKG-INFO +8 -6
  4. {fromargs-0.2.0 → fromargs-0.2.1}/README.md +4 -2
  5. {fromargs-0.2.0 → fromargs-0.2.1}/examples/cheese_cave.py +1 -1
  6. {fromargs-0.2.0 → fromargs-0.2.1}/pyproject.toml +9 -6
  7. {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_app.py +14 -4
  8. {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_argv.py +1 -1
  9. {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_run.py +43 -4
  10. fromargs-0.2.1/tests/conftest.py +54 -0
  11. {fromargs-0.2.0 → fromargs-0.2.1}/tests/e2e/conftest.py +2 -2
  12. {fromargs-0.2.0 → fromargs-0.2.1}/tests/e2e/mutate.py +18 -18
  13. {fromargs-0.2.0 → fromargs-0.2.1}/tests/e2e/test_acceptance.py +31 -31
  14. fromargs-0.2.1/tests/test_ac_marker.py +40 -0
  15. {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_argv.py +56 -0
  16. fromargs-0.2.1/tests/test_mutate_selection.py +124 -0
  17. {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_output.py +3 -1
  18. {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_run.py +89 -12
  19. {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_surface.py +24 -0
  20. fromargs-0.2.0/tests/conftest.py +0 -25
  21. fromargs-0.2.0/uv.lock +0 -195
  22. {fromargs-0.2.0 → fromargs-0.2.1}/LICENSE +0 -0
  23. {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/__init__.py +0 -0
  24. {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_errors.py +0 -0
  25. {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/_output.py +0 -0
  26. {fromargs-0.2.0 → fromargs-0.2.1}/src/fromargs/py.typed +0 -0
  27. {fromargs-0.2.0 → fromargs-0.2.1}/tests/e2e/acceptance_cli.py +0 -0
  28. {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_adversarial.py +0 -0
  29. {fromargs-0.2.0 → fromargs-0.2.1}/tests/test_example_cheese_cave.py +0 -0
@@ -13,8 +13,14 @@ __pycache__/
13
13
  .cheese/
14
14
  .context/
15
15
 
16
+ # Skill archives: build output. `just build` writes them; CI publishes them on dist (ADR-012).
17
+ skills/*/scripts/*.pyz
18
+
16
19
  # Skill-creator iteration workspaces (eval runs, benchmarks, viewer HTML)
17
20
  skills/*-workspace/
18
21
 
22
+ # Live smoke run records (just live-smoke)
23
+ .live-smoke/
24
+
19
25
  # Serena MCP — machine-local; not tracked
20
26
  .serena/
@@ -8,12 +8,30 @@ A `0.x` minor release can contain breaking changes.
8
8
  The `publish fromargs` workflow reads the section for the tagged version.
9
9
  It uses that section as the GitHub release notes.
10
10
  To release, move the `Unreleased` entries into a new version section with the date.
11
- Then set the same version in `lib/fromargs/pyproject.toml`.
12
- When the minor version changes, also bump the `fromargs` range in `pyproject.toml` and `lib/pyproject.toml`.
11
+ Then set the same version in `packages/fromargs/pyproject.toml`.
12
+ When the minor version changes, also bump the `fromargs` range in the root `pyproject.toml` and `packages/skillz/pyproject.toml`.
13
13
  Then relock, and push a `fromargs-v<version>` tag after the release commit merges.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.2.1] - 2026-10-25
18
+
19
+ ### Changed
20
+
21
+ - An unexpected exception now writes its traceback file to a private per-user directory under the temp root.
22
+ On POSIX the directory is `fromargs-<uid>`; `fromargs` refuses a symlink there and removes group and other permission bits.
23
+ The directory keeps only the newest 20 traceback files, so an older path from the envelope can disappear.
24
+ Before, each failure left a new `fromargs-*.traceback` file in the shared temp root, without a limit.
25
+
26
+ ### Fixed
27
+
28
+ - Quote repair now splits an option merged with its value, such as `"--count 3"`.
29
+ Before, `run()` rejected that token as an unknown option at exit 2.
30
+ The repair still skips free-text `str` and path options.
31
+ - `App.command` and `App.group` raise `ValueError` for a name that is already registered.
32
+ Before, they raised `cyclopts.CommandCollisionError`, which is not a `ValueError`.
33
+ `App.default` already raised `ValueError` for a second default handler.
34
+
17
35
  ## [0.2.0] - 2026-10-04
18
36
 
19
37
  ### Changed
@@ -71,6 +89,7 @@ Then relock, and push a `fromargs-v<version>` tag after the release commit merge
71
89
  - `--version` resolution for the calling distribution, not for `fromargs`.
72
90
  - Async handlers through `asyncio.run` when the effective backend is asyncio.
73
91
 
74
- [Unreleased]: https://github.com/paulnsorensen/skillz-that-grillz/compare/fromargs-v0.2.0...HEAD
92
+ [Unreleased]: https://github.com/paulnsorensen/skillz-that-grillz/compare/fromargs-v0.2.1...HEAD
93
+ [0.2.1]: https://github.com/paulnsorensen/skillz-that-grillz/compare/fromargs-v0.2.0...fromargs-v0.2.1
75
94
  [0.2.0]: https://github.com/paulnsorensen/skillz-that-grillz/compare/fromargs-v0.1.0...fromargs-v0.2.0
76
95
  [0.1.0]: https://github.com/paulnsorensen/skillz-that-grillz/releases/tag/fromargs-v0.1.0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fromargs
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Self-healing Cyclopts CLI helpers for agent-friendly command lines.
5
- Project-URL: Homepage, https://github.com/paulnsorensen/skillz-that-grillz/tree/main/lib/fromargs
6
- Project-URL: Source, https://github.com/paulnsorensen/skillz-that-grillz/tree/main/lib/fromargs
5
+ Project-URL: Homepage, https://github.com/paulnsorensen/skillz-that-grillz/tree/main/packages/fromargs
6
+ Project-URL: Source, https://github.com/paulnsorensen/skillz-that-grillz/tree/main/packages/fromargs
7
7
  Project-URL: Issues, https://github.com/paulnsorensen/skillz-that-grillz/issues
8
- Project-URL: Changelog, https://github.com/paulnsorensen/skillz-that-grillz/blob/main/lib/fromargs/CHANGELOG.md
8
+ Project-URL: Changelog, https://github.com/paulnsorensen/skillz-that-grillz/blob/main/packages/fromargs/CHANGELOG.md
9
9
  Author-email: Paul Sorensen <paulnsorensen@gmail.com>
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
@@ -98,8 +98,10 @@ a truncated list result.
98
98
  `AssertionError` is a usage error at exit code `2`. Any other exception
99
99
  from a converter, a validator, the handler, or the result serialization
100
100
  reports at exit code `1`. Its envelope adds a `traceback` key with the
101
- path of a temporary file. `fromargs` keeps the file, so the caller can
102
- read it after the process exits.
101
+ path of a file in a per-user directory under the temp root
102
+ (`fromargs-<uid>` on POSIX). The directory has no group or other
103
+ permission bits. `fromargs` keeps the newest 20 traceback files there and
104
+ deletes older ones, so read the file soon after the process exits.
103
105
  - `sys.exit(n)` sets the exit status, and `run()` catches it while it
104
106
  parses, runs the handler, and prints the result. `0` or `None` exits `0`
105
107
  with no output. An `int` from `1` to `255` reports an error envelope at
@@ -75,8 +75,10 @@ a truncated list result.
75
75
  `AssertionError` is a usage error at exit code `2`. Any other exception
76
76
  from a converter, a validator, the handler, or the result serialization
77
77
  reports at exit code `1`. Its envelope adds a `traceback` key with the
78
- path of a temporary file. `fromargs` keeps the file, so the caller can
79
- read it after the process exits.
78
+ path of a file in a per-user directory under the temp root
79
+ (`fromargs-<uid>` on POSIX). The directory has no group or other
80
+ permission bits. `fromargs` keeps the newest 20 traceback files there and
81
+ deletes older ones, so read the file soon after the process exits.
80
82
  - `sys.exit(n)` sets the exit status, and `run()` catches it while it
81
83
  parses, runs the handler, and prints the result. `0` or `None` exits `0`
82
84
  with no output. An `int` from `1` to `255` reports an error envelope at
@@ -2,7 +2,7 @@
2
2
 
3
3
  The CLI tracks wheels of cheese as they ripen. Run it from the repository root:
4
4
 
5
- uv run --project lib/fromargs python lib/fromargs/examples/cheese_cave.py wheels list
5
+ uv run --project packages/fromargs python packages/fromargs/examples/cheese_cave.py wheels list
6
6
 
7
7
  Agents often send malformed argv. Each call below still runs the intended
8
8
  command. ``run`` prints one ``note:`` line on stderr only for quote-split repairs:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fromargs"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Self-healing Cyclopts CLI helpers for agent-friendly command lines."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -20,16 +20,16 @@ classifiers = [
20
20
  dependencies = ["cyclopts>=5,<6"]
21
21
 
22
22
  [project.urls]
23
- Homepage = "https://github.com/paulnsorensen/skillz-that-grillz/tree/main/lib/fromargs"
24
- Source = "https://github.com/paulnsorensen/skillz-that-grillz/tree/main/lib/fromargs"
23
+ Homepage = "https://github.com/paulnsorensen/skillz-that-grillz/tree/main/packages/fromargs"
24
+ Source = "https://github.com/paulnsorensen/skillz-that-grillz/tree/main/packages/fromargs"
25
25
  Issues = "https://github.com/paulnsorensen/skillz-that-grillz/issues"
26
- Changelog = "https://github.com/paulnsorensen/skillz-that-grillz/blob/main/lib/fromargs/CHANGELOG.md"
26
+ Changelog = "https://github.com/paulnsorensen/skillz-that-grillz/blob/main/packages/fromargs/CHANGELOG.md"
27
27
 
28
28
  [dependency-groups]
29
- dev = ["pytest>=8", "basedpyright==1.40.1"]
29
+ dev = ["pytest>=8", "pytest-xdist>=3", "basedpyright==1.40.2"]
30
30
 
31
31
  [build-system]
32
- requires = ["hatchling>=1.27.0"]
32
+ requires = ["hatchling>=1.32.4"]
33
33
  build-backend = "hatchling.build"
34
34
 
35
35
  [tool.hatch.build.targets.wheel]
@@ -50,5 +50,8 @@ addopts = "--strict-markers"
50
50
 
51
51
  [tool.basedpyright]
52
52
  pythonVersion = "3.11"
53
+ # Workspace members share the root .venv.
54
+ venvPath = "../.."
55
+ venv = ".venv"
53
56
  include = ["src", "tests", "examples"]
54
57
  extraPaths = ["examples"]
@@ -156,7 +156,8 @@ class App:
156
156
 
157
157
  ``limit`` truncates a sequence result to its first ``limit`` items
158
158
  unless ``--full`` is passed; it must not be negative. ``obj`` must
159
- not declare a CLI option named ``--json`` or ``--full``.
159
+ not declare a CLI option named ``--json`` or ``--full``. Raises
160
+ ``ValueError`` when the command name is already registered.
160
161
  """
161
162
  if obj is None:
162
163
 
@@ -167,7 +168,10 @@ class App:
167
168
  _reject_inert(kwargs)
168
169
  _check_limit(limit)
169
170
  before = set(self._cyclopts)
170
- _ = self._cyclopts.command(obj, name=name, help=help, **kwargs)
171
+ try:
172
+ _ = self._cyclopts.command(obj, name=name, help=help, **kwargs)
173
+ except cyclopts.CommandCollisionError as exc:
174
+ raise ValueError(str(exc)) from exc
171
175
  registered = sorted(set(self._cyclopts) - before)
172
176
  sub_app = self._cyclopts[registered[0]]
173
177
  try:
@@ -236,12 +240,18 @@ class App:
236
240
  return obj
237
241
 
238
242
  def group(self, name: str, *, help: str | None = None, **cyclopts_kwargs: Unpack[_AppKwargs]) -> App:
239
- """Return a nested command group registered under this app."""
243
+ """Return a nested command group registered under this app.
244
+
245
+ Raises ``ValueError`` when ``name`` is already registered.
246
+ """
240
247
  _reject_inert(cyclopts_kwargs)
241
248
  sub = cyclopts.App(name=name, help=help, **cyclopts_kwargs)
242
249
  if cyclopts_kwargs.get("default_command") is not None:
243
250
  _reject_reserved(sub, (*self._ancestors, self._cyclopts))
244
- _ = self._cyclopts.command(sub)
251
+ try:
252
+ _ = self._cyclopts.command(sub)
253
+ except cyclopts.CommandCollisionError as exc:
254
+ raise ValueError(str(exc)) from exc
245
255
  return App._wrap(sub, self._limits, (*self._ancestors, self._cyclopts))
246
256
 
247
257
  def run(self, argv: Sequence[str] | None = None, *, stdout: TextIO | None = None) -> int:
@@ -50,7 +50,7 @@ def repair_rejected(app: App, argv: Sequence[str]) -> list[str] | None:
50
50
  if pieces is None or control_flags.intersection(pieces):
51
51
  continue
52
52
  previous = original[index - 1] if index else ""
53
- option = token.partition("=")[0] if token.startswith("--") else previous
53
+ option = pieces[0].partition("=")[0] if token.startswith("--") else previous
54
54
  if option not in splittable:
55
55
  continue
56
56
  candidate = original[:index] + pieces + original[index + 1 :]
@@ -7,6 +7,7 @@ import contextlib
7
7
  import inspect
8
8
  import json
9
9
  import os
10
+ import stat
10
11
  import sys
11
12
  import tempfile
12
13
  import traceback
@@ -21,6 +22,9 @@ from fromargs._argv import command_chain, parse_once, repair_rejected, strip_glo
21
22
  from fromargs._errors import CliError
22
23
  from fromargs._output import write_result
23
24
 
25
+ _KEEP_TRACEBACKS = 20
26
+ _TRACEBACK_SUFFIX = ".traceback"
27
+
24
28
 
25
29
  class _AsyncContractError(TypeError):
26
30
  """A programmer-contract violation for an async handler; always re-raised."""
@@ -200,16 +204,50 @@ def _report_system_exit(exc: SystemExit) -> int:
200
204
  return _report(str(code), 1)
201
205
 
202
206
 
207
+ def _traceback_dir() -> str:
208
+ """Create and return this user's private traceback directory under the temp root."""
209
+ uid = os.getuid() if hasattr(os, "getuid") else None
210
+ # Without getuid (Windows), the temp root is already per-user.
211
+ name = "fromargs" if uid is None else f"fromargs-{uid}"
212
+ path = os.path.join(tempfile.gettempdir(), name)
213
+ os.makedirs(path, mode=0o700, exist_ok=True)
214
+ if uid is not None:
215
+ st = os.lstat(path)
216
+ if not stat.S_ISDIR(st.st_mode) or st.st_uid != uid:
217
+ raise PermissionError(f"{path} is not a directory that this user owns")
218
+ if st.st_mode & 0o077:
219
+ os.chmod(path, 0o700)
220
+ return path
221
+
222
+
223
+ def _prune_tracebacks(directory: str, keep_path: str) -> None:
224
+ """Delete all but the newest ``_KEEP_TRACEBACKS`` traceback files; never ``keep_path``."""
225
+ with contextlib.suppress(OSError):
226
+ dated: list[tuple[int, str]] = []
227
+ with os.scandir(directory) as entries:
228
+ for entry in entries:
229
+ if entry.path != keep_path and entry.name.endswith(_TRACEBACK_SUFFIX):
230
+ with contextlib.suppress(OSError):
231
+ dated.append((entry.stat().st_mtime_ns, entry.path))
232
+ dated.sort()
233
+ for _, stale in dated[: max(0, len(dated) - (_KEEP_TRACEBACKS - 1))]:
234
+ with contextlib.suppress(OSError):
235
+ os.unlink(stale)
236
+
237
+
203
238
  def _report_unexpected(exc: Exception) -> int:
204
239
  """Report an unexpected exception as a two- or three-key envelope; return 1.
205
240
 
206
241
  The envelope gets a ``traceback`` path when the traceback file writes
207
- successfully. An ``OSError`` while creating or writing that file omits
208
- the ``traceback`` key instead of escaping the ADR-001 envelope.
242
+ successfully. The file lives in a per-user directory under the temp root,
243
+ which keeps only the newest ``_KEEP_TRACEBACKS`` files. An ``OSError``
244
+ while creating or writing that file omits the ``traceback`` key instead
245
+ of escaping the ADR-001 envelope.
209
246
  """
210
247
  envelope: dict[str, object] = {"error": f"{type(exc).__name__}: {exc}", "exit_code": 1}
211
248
  try:
212
- descriptor, path = tempfile.mkstemp(prefix="fromargs-", suffix=".traceback")
249
+ directory = _traceback_dir()
250
+ descriptor, path = tempfile.mkstemp(prefix="fromargs-", suffix=_TRACEBACK_SUFFIX, dir=directory)
213
251
  except OSError:
214
252
  pass
215
253
  else:
@@ -221,5 +259,6 @@ def _report_unexpected(exc: Exception) -> int:
221
259
  os.unlink(path)
222
260
  else:
223
261
  envelope["traceback"] = path
262
+ _prune_tracebacks(directory, path)
224
263
  print(json.dumps(envelope), file=sys.stderr)
225
- return 1
264
+ return 1
@@ -0,0 +1,54 @@
1
+ """Shared fixtures for the fromargs suite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import tempfile
8
+ from collections.abc import Callable, Iterator
9
+ from pathlib import Path
10
+ from typing import cast
11
+
12
+ import pytest
13
+
14
+ JsonLine = Callable[[str], dict[str, object]]
15
+
16
+
17
+ def _single_json_line(err: str) -> dict[str, object]:
18
+ lines = err.splitlines()
19
+ assert len(lines) == 1, err
20
+ envelope = cast("dict[str, object]", json.loads(lines[0]))
21
+ assert set(envelope) == {"error", "exit_code"}
22
+ return envelope
23
+
24
+
25
+ @pytest.fixture
26
+ def single_json_line() -> JsonLine:
27
+ """Checker: ``err`` is exactly one ADR-001 JSON error envelope; returns it."""
28
+ return _single_json_line
29
+
30
+
31
+ @pytest.fixture(autouse=True)
32
+ def _isolated_temp_root(tmp_path: Path) -> Iterator[None]:
33
+ """Point the temp root at ``tmp_path`` so traceback files never reach the shared temp directory."""
34
+ saved = tempfile.tempdir
35
+ tempfile.tempdir = str(tmp_path)
36
+ try:
37
+ yield
38
+ finally:
39
+ tempfile.tempdir = saved
40
+
41
+
42
+ AC_MARKER = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*:[A-Z]+-[A-Z]*[0-9]+")
43
+
44
+
45
+ def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
46
+ """Fail collection when an ``ac`` marker does not name its source as ``<source>:<id>``."""
47
+ for item in items:
48
+ for marker in item.iter_markers(name="ac"):
49
+ argument = marker.args[0] if len(marker.args) == 1 and not marker.kwargs else None
50
+ if not isinstance(argument, str) or not AC_MARKER.fullmatch(argument):
51
+ form = "one string in the form <source>:<id>, for example gh-267:AC-1"
52
+ raise pytest.UsageError(
53
+ f"{item.nodeid}: ac marker {marker.args!r} must be {form} (see docs/agents/criteria/index.md)"
54
+ )
@@ -1,9 +1,9 @@
1
1
  """Collection-time helper for ``mutate.py``.
2
2
 
3
- ``pytest -m`` cannot match a marker's *argument* (``@pytest.mark.ac("AC-3")``),
3
+ ``pytest -m`` cannot match a marker's *argument* (``@pytest.mark.ac("fromargs-cli-library:AC-3")``),
4
4
  only its name. To let the mutation runner select, per acceptance criterion,
5
5
  exactly the tests that must catch a broken build, this hook prints one
6
- ``nodeid<TAB>AC-n`` line per collected test to stdout during a
6
+ ``nodeid<TAB><source>:AC-n`` line per collected test to stdout during a
7
7
  ``--collect-only`` pass -- but only when ``FROMARGS_AC_DUMP`` is set, so a
8
8
  normal test run stays silent.
9
9
  """
@@ -9,7 +9,7 @@ regression.
9
9
 
10
10
  Usage (from the repository root, or anywhere)::
11
11
 
12
- uv run --project lib/fromargs python lib/fromargs/tests/e2e/mutate.py
12
+ uv run --project packages/fromargs python packages/fromargs/tests/e2e/mutate.py
13
13
 
14
14
  Never writes to the real ``src/`` tree: every mutation is applied to a fresh
15
15
  ``tempfile.TemporaryDirectory()`` copy that is deleted once that mutation's
@@ -50,70 +50,70 @@ class Mutation:
50
50
 
51
51
  MUTATIONS: list[Mutation] = [
52
52
  Mutation(
53
- ac_id="AC-1",
53
+ ac_id="fromargs-cli-library:AC-1",
54
54
  label="command registration renames every command",
55
55
  rel_file="_app.py",
56
56
  old=' _ = self._cyclopts.command(obj, name=name, help=help, **kwargs)',
57
57
  new=' _ = self._cyclopts.command(obj, name=f"broken-{name or \'\'}", help=help, **kwargs)',
58
58
  ),
59
59
  Mutation(
60
- ac_id="AC-2",
60
+ ac_id="fromargs-cli-library:AC-2",
61
61
  label="JSON result payload is discarded",
62
62
  rel_file="_output.py",
63
63
  old=" print(serialized, file=stream)",
64
64
  new=' print(json.dumps({"broken": True}), file=stream)',
65
65
  ),
66
66
  Mutation(
67
- ac_id="AC-3",
67
+ ac_id="fromargs-cli-library:AC-3",
68
68
  label="None-result exit code",
69
69
  rel_file="_run.py",
70
70
  old=" if status is None:\n return 0",
71
71
  new=" if status is None:\n return 1",
72
72
  ),
73
73
  Mutation(
74
- ac_id="AC-4",
74
+ ac_id="fromargs-cli-library:AC-4",
75
75
  label="JSON error envelope key",
76
76
  rel_file="_run.py",
77
77
  old=' print(json.dumps({"error": message, "exit_code": exit_code}), file=sys.stderr)',
78
78
  new=' print(json.dumps({"error": message, "code": exit_code}), file=sys.stderr)',
79
79
  ),
80
80
  Mutation(
81
- ac_id="AC-5",
81
+ ac_id="fromargs-cli-library:AC-5",
82
82
  label="--json stops being a no-op",
83
83
  rel_file="_argv.py",
84
84
  old='GLOBAL_FLAGS = frozenset({"--json", "--full"})',
85
85
  new='GLOBAL_FLAGS = frozenset({"--full"})',
86
86
  ),
87
87
  Mutation(
88
- ac_id="AC-6",
88
+ ac_id="fromargs-cli-library:AC-6",
89
89
  label="--full never turns off truncation",
90
90
  rel_file="_argv.py",
91
91
  old=' return kept + after, "--full" in before',
92
92
  new=" return kept + after, False",
93
93
  ),
94
94
  Mutation(
95
- ac_id="AC-7",
95
+ ac_id="fromargs-cli-library:AC-7",
96
96
  label="limit truncation ignores --full",
97
97
  rel_file="_output.py",
98
98
  old=" if not full and total > limit:",
99
99
  new=" if total > limit:",
100
100
  ),
101
101
  Mutation(
102
- ac_id="AC-8",
102
+ ac_id="fromargs-cli-library:AC-8",
103
103
  label="full handler parameter no longer reserved",
104
104
  rel_file="_app.py",
105
105
  old=" reserved = sorted(GLOBAL_FLAGS.intersection(names))",
106
106
  new=' reserved = sorted(frozenset({"--json"}).intersection(names))',
107
107
  ),
108
108
  Mutation(
109
- ac_id="AC-9",
109
+ ac_id="fromargs-cli-library:AC-9",
110
110
  label="public surface grows an extra name",
111
111
  rel_file="__init__.py",
112
112
  old='__all__ = ["App", "CliError", "Group", "Parameter", "contract_error"]',
113
113
  new='__all__ = ["App", "CliError", "Group", "Parameter", "contract_error", "extra"]',
114
114
  ),
115
115
  Mutation(
116
- ac_id="AC-10",
116
+ ac_id="fromargs-cli-library:AC-10",
117
117
  label="no JSON-input flag or stdin reader",
118
118
  na_reason=(
119
119
  "fromargs never reads sys.stdin and defines no JSON-input flag "
@@ -122,14 +122,14 @@ MUTATIONS: list[Mutation] = [
122
122
  ),
123
123
  ),
124
124
  Mutation(
125
- ac_id="AC-11",
125
+ ac_id="fromargs-cli-library:AC-11",
126
126
  label="ambiguous quote split accepts the first candidate",
127
127
  rel_file="_argv.py",
128
128
  old=" if found is not None:\n return None",
129
129
  new=" if found is not None:\n return found[0]",
130
130
  ),
131
131
  Mutation(
132
- ac_id="AC-12",
132
+ ac_id="fromargs-cli-library:AC-12",
133
133
  label="probe parse invokes the handler",
134
134
  rel_file="_argv.py",
135
135
  old=(
@@ -143,7 +143,7 @@ MUTATIONS: list[Mutation] = [
143
143
  ),
144
144
  ),
145
145
  Mutation(
146
- ac_id="AC-13",
146
+ ac_id="fromargs-cli-library:AC-13",
147
147
  label='"Did you mean" suggestion wording',
148
148
  na_reason=(
149
149
  "Did-you-mean suggestions are native Cyclopts error-message "
@@ -154,28 +154,28 @@ MUTATIONS: list[Mutation] = [
154
154
  ),
155
155
  ),
156
156
  Mutation(
157
- ac_id="AC-15",
157
+ ac_id="fromargs-cli-library:AC-15",
158
158
  label="resolved version falls back before checking the caller module",
159
159
  rel_file="_app.py",
160
160
  old=" caller = frame.f_back if frame is not None else None",
161
161
  new=" caller = frame",
162
162
  ),
163
163
  Mutation(
164
- ac_id="AC-16",
164
+ ac_id="fromargs-cli-library:AC-16",
165
165
  label="default handler is never registered with Cyclopts",
166
166
  rel_file="_app.py",
167
167
  old=" _ = self._cyclopts.default(obj, validator=validator)",
168
168
  new=" pass",
169
169
  ),
170
170
  Mutation(
171
- ac_id="AC-17",
171
+ ac_id="fromargs-cli-library:AC-17",
172
172
  label="group kwargs are dropped from the nested Cyclopts app",
173
173
  rel_file="_app.py",
174
174
  old=" sub = cyclopts.App(name=name, help=help, **cyclopts_kwargs)",
175
175
  new=" sub = cyclopts.App(name=name, help=help)",
176
176
  ),
177
177
  Mutation(
178
- ac_id="AC-14",
178
+ ac_id="fromargs-cli-library:AC-14",
179
179
  label="underscore flag binding (--max_count -> max_count)",
180
180
  na_reason=(
181
181
  "Underscore/dash flag normalization is native Cyclopts behavior "