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.
- {agent_sdlc-0.4.0/agent_sdlc.egg-info → agent_sdlc-0.4.2}/PKG-INFO +1 -1
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2/agent_sdlc.egg-info}/PKG-INFO +1 -1
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/__init__.py +1 -1
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/cli.py +19 -4
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/coverage_map.py +37 -18
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/rules.py +7 -1
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/diff-discipline.md +1 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/testing.md +2 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/tickets.md +2 -1
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/voice.md +5 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/LICENSE +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/NOTICE.md +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/README.md +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/SOURCES.txt +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/dependency_links.txt +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/entry_points.txt +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/requires.txt +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/agent_sdlc.egg-info/top_level.txt +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/adversary.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/commit_msg.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/diff_discipline.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/model_vv.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/mutants.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/no_comments.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/no_leaks.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/no_new_docs.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/repo.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/runner.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/token.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_check.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_core.toml +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_molds.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_path.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/vocabulary_wordnet.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/mutation_gate/waivers.py +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/pyproject.toml +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/code-clarity.md +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/model-vv.md +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/rules/naming.md +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/setup.cfg +0 -0
- {agent_sdlc-0.4.0 → agent_sdlc-0.4.2}/tests/test_say_narrate_bin.py +0 -0
|
@@ -117,9 +117,11 @@ def _gate_file(
|
|
|
117
117
|
token.write(repo, rel, fp, "no-mutants")
|
|
118
118
|
return False, [], cands
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|