agent-sdlc 0.4.0__tar.gz → 0.4.2__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 (42) hide show
  1. {agent_sdlc-0.4.0/agent_sdlc.egg-info → agent_sdlc-0.4.2}/PKG-INFO +1 -1
  2. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2/agent_sdlc.egg-info}/PKG-INFO +1 -1
  3. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/__init__.py +1 -1
  4. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/cli.py +19 -4
  5. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/coverage_map.py +37 -18
  6. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/rules.py +7 -1
  7. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/diff-discipline.md +1 -0
  8. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/testing.md +2 -0
  9. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/tickets.md +2 -1
  10. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/voice.md +5 -0
  11. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/LICENSE +0 -0
  12. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/NOTICE.md +0 -0
  13. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/README.md +0 -0
  14. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/SOURCES.txt +0 -0
  15. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/dependency_links.txt +0 -0
  16. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/entry_points.txt +0 -0
  17. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/requires.txt +0 -0
  18. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/top_level.txt +0 -0
  19. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/adversary.py +0 -0
  20. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/commit_msg.py +0 -0
  21. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/diff_discipline.py +0 -0
  22. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/model_vv.py +0 -0
  23. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/mutants.py +0 -0
  24. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/no_comments.py +0 -0
  25. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/no_leaks.py +0 -0
  26. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/no_new_docs.py +0 -0
  27. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/repo.py +0 -0
  28. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/runner.py +0 -0
  29. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/token.py +0 -0
  30. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary.py +0 -0
  31. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_check.py +0 -0
  32. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_core.toml +0 -0
  33. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_molds.py +0 -0
  34. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_path.py +0 -0
  35. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_wordnet.py +0 -0
  36. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/waivers.py +0 -0
  37. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/pyproject.toml +0 -0
  38. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/code-clarity.md +0 -0
  39. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/model-vv.md +0 -0
  40. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/naming.md +0 -0
  41. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/setup.cfg +0 -0
  42. {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/tests/test_say_narrate_bin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-sdlc
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Diff-scoped mutation gate for pre-commit and agent Stop hooks
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/PavelGuzenfeld/agent-sdlc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-sdlc
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Diff-scoped mutation gate for pre-commit and agent Stop hooks
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/PavelGuzenfeld/agent-sdlc
@@ -1,3 +1,3 @@
1
1
  """Diff-scoped mutation gate. Spec: PavelGuzenfeld/dotfiles#9."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.2"
@@ -117,9 +117,11 @@ def _gate_file(
117
117
  token.write(repo, rel, fp, "no-mutants")
118
118
  return False, [], cands
119
119
 
120
- cover = coverage_map.covering_tests(repo, rel, cands, fp)
121
- if not cover and rel.endswith(".py"):
122
- _emit(f" {rel}: coverage came back empty — every mutant runs against all candidate tests")
120
+ try:
121
+ cover = coverage_map.covering_tests(repo, rel, cands, fp)
122
+ except coverage_map.EmptyCoverageError as empty:
123
+ cover = {}
124
+ _emit(f" {rel}: coverage came back empty ({empty}) — every mutant runs against all candidate tests")
123
125
  all_tests = [str(p.relative_to(repo.root)) for p in cands]
124
126
 
125
127
  baseline = runner.baseline_green(repo, all_tests, lang_cfg.test_command)
@@ -240,6 +242,14 @@ def _user_turn_text(content) -> str | None:
240
242
  return "\n".join(kept) if kept else None
241
243
 
242
244
 
245
+ def _line_numbers(text: str) -> set[int]:
246
+ parts = text.split(",")
247
+ bad = next((part for part in parts if not part.strip().isdigit()), None)
248
+ if bad is not None:
249
+ raise argparse.ArgumentTypeError(f"invalid line number: {bad!r}")
250
+ return {int(part) for part in parts}
251
+
252
+
243
253
  def main(argv: list[str] | None = None) -> int:
244
254
  argv = sys.argv[1:] if argv is None else argv
245
255
  if argv[:1] == ["rules"]:
@@ -261,10 +271,14 @@ def main(argv: list[str] | None = None) -> int:
261
271
  parser.add_argument("--no-adversary", action="store_true")
262
272
  parser.add_argument("--file", default=None,
263
273
  help="gate every line of one file (acceptance / on-demand)")
274
+ parser.add_argument("--lines", type=_line_numbers, default=None,
275
+ help="with --file, gate only these comma-separated line numbers")
264
276
  parser.add_argument("--dry-run", action="store_true",
265
277
  help="print candidates and mutants; run no tests")
266
278
  parser.add_argument("files", nargs="*", help="ignored; pre-commit passes filenames")
267
279
  args = parser.parse_args(argv)
280
+ if args.lines and not args.file:
281
+ parser.error("--lines requires --file")
268
282
  staged = args.staged or not args.worktree
269
283
 
270
284
  payload = _stop_hook_payload() if args.worktree else {}
@@ -309,7 +323,8 @@ def _run(repo, args, staged: bool, transcript_path: str | None = None) -> int:
309
323
 
310
324
  if args.file:
311
325
  target = (repo.root / args.file)
312
- all_changed = {args.file: set(range(1, len(target.read_text().splitlines()) + 1))}
326
+ every_line = set(range(1, len(target.read_text().splitlines()) + 1))
327
+ all_changed = {args.file: args.lines or every_line}
313
328
  else:
314
329
  try:
315
330
  all_changed = mutants.changed_lines(repo.root, staged)
@@ -10,7 +10,8 @@ from __future__ import annotations
10
10
  import ast
11
11
  import json
12
12
  import sqlite3
13
- from pathlib import Path
13
+ import time
14
+ from pathlib import Path, PurePosixPath
14
15
 
15
16
  from . import mutants, runner
16
17
  from .repo import CACHE_ROOT, Repo, git
@@ -221,6 +222,7 @@ def blob_hashes(repo: Repo, paths: list[Path]) -> list[str]:
221
222
 
222
223
 
223
224
  _CACHE_VERSION = "2"
225
+ _COARSEST_MTIME_STEP_S = 2.0
224
226
 
225
227
 
226
228
  def _cache_path(repo: Repo, target: str, fingerprint: str) -> Path:
@@ -235,6 +237,10 @@ def _cov_scope(target: str) -> str:
235
237
  return str(Path(target).parent)
236
238
 
237
239
 
240
+ class EmptyCoverageError(Exception):
241
+ pass
242
+
243
+
238
244
  def covering_tests(
239
245
  repo: Repo, target: str, cands: list[Path], fingerprint: str
240
246
  ) -> dict[int, list[str]]:
@@ -251,13 +257,19 @@ def covering_tests(
251
257
  rels = " ".join(str(p.relative_to(repo.root)) for p in cands)
252
258
  cmd = lang_cfg.coverage_command.format(file=_cov_scope(target), tests=rels)
253
259
  # Same suite plus instrumentation, so the baseline cap is the honest bound.
254
- # Overrunning leaves no contexts, which widens selection to the whole suite.
260
+ run_start = time.time()
255
261
  verdict = runner.run_capped(repo, cmd, repo.config.baseline_timeout)
262
+ if verdict in (runner.FAILED, runner.TIMED_OUT):
263
+ raise EmptyCoverageError(f"the coverage run {verdict}")
264
+ data_file = repo.root / lang_cfg.coverage_data_file
265
+ if data_file.exists() and data_file.stat().st_mtime < run_start - _COARSEST_MTIME_STEP_S:
266
+ raise EmptyCoverageError(f"{lang_cfg.coverage_data_file} predates this coverage run")
256
267
 
257
268
  mapping = _read_contexts(repo, target, lang_cfg.coverage_data_file)
258
- if mapping and verdict != runner.TIMED_OUT:
259
- cache.parent.mkdir(parents=True, exist_ok=True)
260
- cache.write_text(json.dumps({str(k): v for k, v in mapping.items()}))
269
+ if not mapping:
270
+ raise EmptyCoverageError(f"no test recorded a line of {target}")
271
+ cache.parent.mkdir(parents=True, exist_ok=True)
272
+ cache.write_text(json.dumps({str(k): v for k, v in mapping.items()}))
261
273
  return mapping
262
274
 
263
275
 
@@ -273,23 +285,30 @@ def _numbits_to_lines(numbits: bytes) -> list[int]:
273
285
  ]
274
286
 
275
287
 
288
+ def _measured_file_id(repo: Repo, target: str, files: list[tuple[int, str]]) -> int:
289
+ """A test_command run in docker records the container's mount path, not the host's
290
+ (#358), so a lone row ending in `target`'s components stands in for an exact match.
291
+ Two such rows are ambiguous: no mapping is safer than narrowing to the wrong file's tests."""
292
+ target_path = (repo.root / target).resolve()
293
+ exact = [fid for fid, path in files if Path(path).resolve() == target_path]
294
+ if exact:
295
+ return exact[0]
296
+ parts = Path(target).parts
297
+ tails = [fid for fid, path in files if PurePosixPath(path).parts[-len(parts):] == parts]
298
+ if not tails:
299
+ raise EmptyCoverageError(f"no row in the data file matches {target}")
300
+ if len(tails) > 1:
301
+ raise EmptyCoverageError(f"{len(tails)} rows in the data file end in {target}")
302
+ return tails[0]
303
+
304
+
276
305
  def _read_contexts(repo: Repo, target: str, coverage_data_file: str) -> dict[int, list[str]]:
277
306
  data_file = repo.root / coverage_data_file
278
307
  if not data_file.exists():
279
- return {}
280
- target_path = (repo.root / target).resolve()
308
+ raise EmptyCoverageError(f"no data file at {coverage_data_file}")
281
309
  con = sqlite3.connect(f"file:{data_file}?mode=ro", uri=True)
282
310
  try:
283
- file_id = next(
284
- (
285
- fid
286
- for fid, path in con.execute("SELECT id, path FROM file")
287
- if Path(path).resolve() == target_path
288
- ),
289
- None,
290
- )
291
- if file_id is None:
292
- return {}
311
+ file_id = _measured_file_id(repo, target, con.execute("SELECT id, path FROM file").fetchall())
293
312
  rows = con.execute(
294
313
  "SELECT line_bits.numbits, context.context FROM line_bits "
295
314
  "JOIN context ON context.id = line_bits.context_id "
@@ -297,7 +316,7 @@ def _read_contexts(repo: Repo, target: str, coverage_data_file: str) -> dict[int
297
316
  (file_id,),
298
317
  ).fetchall()
299
318
  except sqlite3.DatabaseError:
300
- return {}
319
+ raise EmptyCoverageError(f"{coverage_data_file} is not a readable coverage database") from None
301
320
  finally:
302
321
  con.close()
303
322
  result: dict[int, list[str]] = {}
@@ -20,6 +20,12 @@ _INSTALLED = Path(__file__).resolve().parent / "bundled_rules"
20
20
  RULES_DIR = _INSTALLED if _INSTALLED.is_dir() else Path(__file__).resolve().parent.parent / "rules"
21
21
 
22
22
 
23
+ def source_dir(repo: Repo) -> Path:
24
+ """The pack's own checkout is its own rule source; the bundle would downgrade it."""
25
+ is_pack_checkout = (repo.root / "mutation_gate" / "rules.py").is_file()
26
+ return repo.root / "rules" if is_pack_checkout else RULES_DIR
27
+
28
+
23
29
  def _emit(line: str) -> None:
24
30
  print(line, file=sys.stderr)
25
31
 
@@ -42,7 +48,7 @@ def frontmatter(model_paths: list[str]) -> bytes:
42
48
 
43
49
  def expected(repo: Repo) -> dict[str, bytes]:
44
50
  out: dict[str, bytes] = {}
45
- for path in sorted(RULES_DIR.glob("*.md")):
51
+ for path in sorted(source_dir(repo).glob("*.md")):
46
52
  if path.name == SCOPED_RULE:
47
53
  if not repo.config.model_paths:
48
54
  continue
@@ -28,3 +28,4 @@ structure right and the **layout wrong**. `sed`/`perl` only for prose, config an
28
28
  single-line literals — a regex alternation matches across lines and eats them.
29
29
  `sg` is ambiguous: `/usr/bin/sg` is newgrp's alias, and ast-grep-cli's shim can shadow it. Always `ast-grep`.
30
30
  Use it to audit too: a structural question deserves a pattern, not a grep heuristic.
31
+ Run the formatter through the repo's own hook, `pre-commit run <id> --files <changed files>`, never a bare whole-file run: it reformats lines you did not touch.
@@ -65,6 +65,8 @@ test that only re-exercises what the slice already covers earns nothing.
65
65
  watch it fail for the right reason. A test that was never red proves nothing —
66
66
  it could pass against the unmodified tree.
67
67
 
68
+ **Read the launch path first.** Before validating on hardware, read every script the launch path execs. A fake image can hide a startup step that changes the outcome, such as one that wipes a log directory.
69
+
68
70
  ## Adversary protocol
69
71
 
70
72
  When the gate goes green, the pack spawns an isolated review that sees the
@@ -34,7 +34,8 @@ A PR waits for CI in one foreground call — `gh pr checks <PR> --watch
34
34
  --fail-fast` or `gh run watch <run> --exit-status` — with the Bash timeout at
35
35
  its 600000 ms maximum and the output to a file, re-issued on timeout. The call
36
36
  blocks until CI settles, so no turn is spent on a status check: every turn
37
- re-reads the whole context.
37
+ re-reads the whole context. Unless the session is ending with the PR open: skip
38
+ the wait and record CI as unwatched in the handoff.
38
39
 
39
40
  ## Follow-ups
40
41
 
@@ -19,3 +19,8 @@ Private docs, issues and PR bodies: no preamble, no restating the brief, no Over
19
19
  section, no "This PR introduces…", no closing summary. Defect in the title, then
20
20
  repro, expected/actual, evidence. Tighten the section you edit; design docs and
21
21
  ICD notes keep their rationale.
22
+
23
+ Text posted or edited under the user's name starts from their own words: same
24
+ length and register, grammar fixed, nothing added. Session evidence goes in only
25
+ when they ask. Before any public post or edit: is this longer than what the user
26
+ wrote? Then cut back to it.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes