mapify-cli 3.34.2__py3-none-any.whl → 3.35.0__py3-none-any.whl

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 (125) hide show
  1. mapify_cli/__init__.py +6 -5
  2. mapify_cli/delivery/file_copier.py +48 -0
  3. mapify_cli/memory/capture.py +2 -1
  4. mapify_cli/ralph_state.py +2 -1
  5. mapify_cli/templates/agents/actor.md +27 -0
  6. mapify_cli/templates/agents/evaluator.md +26 -0
  7. mapify_cli/templates/agents/monitor.md +51 -0
  8. mapify_cli/templates/agents/review-reader.md +10 -1
  9. mapify_cli/templates/codex/agents/actor.toml +26 -0
  10. mapify_cli/templates/codex/agents/evaluator.toml +25 -0
  11. mapify_cli/templates/codex/agents/monitor.toml +49 -0
  12. mapify_cli/templates/codex/agents/review-reader.toml +10 -1
  13. mapify_cli/templates/codex/hooks/context-meter.py +20 -9
  14. mapify_cli/templates/codex/hooks/map-token-meter.py +20 -9
  15. mapify_cli/templates/codex/hooks/post-compact-context.py +19 -9
  16. mapify_cli/templates/codex/hooks/pre-compact-save-transcript.py +19 -9
  17. mapify_cli/templates/codex/hooks/ralph-context-pruner.py +19 -9
  18. mapify_cli/templates/codex/hooks/ralph-iteration-logger.py +22 -11
  19. mapify_cli/templates/codex/hooks/scrub-internal-ids.py +18 -7
  20. mapify_cli/templates/codex/hooks/workflow-context-injector.py +19 -9
  21. mapify_cli/templates/codex/hooks/workflow-gate.py +19 -9
  22. mapify_cli/templates/codex/skills/map-architecture/SKILL.md +2 -2
  23. mapify_cli/templates/codex/skills/map-check/SKILL.md +2 -2
  24. mapify_cli/templates/codex/skills/map-efficient/SKILL.md +15 -5
  25. mapify_cli/templates/codex/skills/map-efficient/efficient-reference.md +41 -11
  26. mapify_cli/templates/codex/skills/map-explain/SKILL.md +1 -1
  27. mapify_cli/templates/codex/skills/map-plan/SKILL.md +7 -7
  28. mapify_cli/templates/codex/skills/map-release/release-reference.md +16 -7
  29. mapify_cli/templates/codex/skills/map-resume/SKILL.md +3 -3
  30. mapify_cli/templates/codex/skills/map-review/SKILL.md +17 -17
  31. mapify_cli/templates/codex/skills/map-review/adversarial-reference.md +11 -5
  32. mapify_cli/templates/codex/skills/map-review/review-reference.md +96 -4
  33. mapify_cli/templates/codex/skills/map-state/scripts/get-plan-path.sh +5 -17
  34. mapify_cli/templates/codex/skills/map-state/scripts/init-session.sh +3 -12
  35. mapify_cli/templates/codex/skills/map-task/SKILL.md +2 -2
  36. mapify_cli/templates/codex/skills/map-tdd/SKILL.md +5 -4
  37. mapify_cli/templates/codex/skills/map-tokenreport/SKILL.md +1 -1
  38. mapify_cli/templates/codex/skills/map-wayfind/wayfind-reference.md +9 -2
  39. mapify_cli/templates/hooks/context-meter.py +20 -9
  40. mapify_cli/templates/hooks/map-statusline.py +2 -1
  41. mapify_cli/templates/hooks/map-token-meter.py +20 -9
  42. mapify_cli/templates/hooks/post-compact-context.py +19 -9
  43. mapify_cli/templates/hooks/pre-compact-save-transcript.py +19 -9
  44. mapify_cli/templates/hooks/ralph-context-pruner.py +19 -9
  45. mapify_cli/templates/hooks/ralph-iteration-logger.py +22 -11
  46. mapify_cli/templates/hooks/scrub-internal-ids.py +18 -7
  47. mapify_cli/templates/hooks/workflow-context-injector.py +19 -9
  48. mapify_cli/templates/hooks/workflow-gate.py +19 -9
  49. mapify_cli/templates/map/scripts/diagnostics.py +20 -8
  50. mapify_cli/templates/map/scripts/map_step_runner.py +1393 -210
  51. mapify_cli/templates/map/scripts/map_utils.py +37 -15
  52. mapify_cli/templates/map/scripts/scrub_internal_ids.py +110 -74
  53. mapify_cli/templates/map/scripts/sofa_client.py +46 -0
  54. mapify_cli/templates/map/scripts/validate_spec_citations.py +17 -6
  55. mapify_cli/templates/map/scripts/wayfind_runner.py +6 -3
  56. mapify_cli/templates/skills/map-architecture/SKILL.md +2 -2
  57. mapify_cli/templates/skills/map-check/SKILL.md +2 -2
  58. mapify_cli/templates/skills/map-efficient/SKILL.md +5 -4
  59. mapify_cli/templates/skills/map-efficient/efficient-reference.md +51 -10
  60. mapify_cli/templates/skills/map-explain/SKILL.md +1 -1
  61. mapify_cli/templates/skills/map-plan/SKILL.md +1 -1
  62. mapify_cli/templates/skills/map-release/release-reference.md +16 -7
  63. mapify_cli/templates/skills/map-resume/SKILL.md +3 -3
  64. mapify_cli/templates/skills/map-review/SKILL.md +16 -16
  65. mapify_cli/templates/skills/map-review/adversarial-reference.md +10 -4
  66. mapify_cli/templates/skills/map-review/review-reference.md +96 -4
  67. mapify_cli/templates/skills/map-state/scripts/get-plan-path.sh +5 -17
  68. mapify_cli/templates/skills/map-state/scripts/init-session.sh +3 -12
  69. mapify_cli/templates/skills/map-task/SKILL.md +2 -2
  70. mapify_cli/templates/skills/map-tdd/SKILL.md +5 -4
  71. mapify_cli/templates/skills/map-tokenreport/SKILL.md +1 -1
  72. mapify_cli/templates/skills/map-wayfind/wayfind-reference.md +9 -2
  73. mapify_cli/templates/skills/skill-rules.json +1 -1
  74. mapify_cli/templates_src/_partials/monitor-review-context.md.jinja +20 -0
  75. mapify_cli/templates_src/_partials/vacuous-test-check.md.jinja +21 -0
  76. mapify_cli/templates_src/agents/actor.md.jinja +6 -0
  77. mapify_cli/templates_src/agents/evaluator.md.jinja +5 -0
  78. mapify_cli/templates_src/agents/monitor.md.jinja +10 -0
  79. mapify_cli/templates_src/agents/review-reader.md.jinja +10 -1
  80. mapify_cli/templates_src/codex/agents/actor.toml.jinja +5 -0
  81. mapify_cli/templates_src/codex/agents/evaluator.toml.jinja +4 -0
  82. mapify_cli/templates_src/codex/agents/monitor.toml.jinja +8 -0
  83. mapify_cli/templates_src/codex/agents/review-reader.toml.jinja +10 -1
  84. mapify_cli/templates_src/codex/hooks/workflow-gate.py.jinja +19 -9
  85. mapify_cli/templates_src/hooks/context-meter.py.jinja +20 -9
  86. mapify_cli/templates_src/hooks/map-statusline.py.jinja +2 -1
  87. mapify_cli/templates_src/hooks/map-token-meter.py.jinja +20 -9
  88. mapify_cli/templates_src/hooks/post-compact-context.py.jinja +19 -9
  89. mapify_cli/templates_src/hooks/pre-compact-save-transcript.py.jinja +19 -9
  90. mapify_cli/templates_src/hooks/ralph-context-pruner.py.jinja +19 -9
  91. mapify_cli/templates_src/hooks/ralph-iteration-logger.py.jinja +22 -11
  92. mapify_cli/templates_src/hooks/scrub-internal-ids.py.jinja +18 -7
  93. mapify_cli/templates_src/hooks/workflow-context-injector.py.jinja +19 -9
  94. mapify_cli/templates_src/hooks/workflow-gate.py.jinja +19 -9
  95. mapify_cli/templates_src/map/scripts/diagnostics.py.jinja +20 -8
  96. mapify_cli/templates_src/map/scripts/map_step_runner.py.jinja +1393 -210
  97. mapify_cli/templates_src/map/scripts/map_utils.py.jinja +37 -15
  98. mapify_cli/templates_src/map/scripts/scrub_internal_ids.py.jinja +110 -74
  99. mapify_cli/templates_src/map/scripts/sofa_client.py.jinja +46 -0
  100. mapify_cli/templates_src/map/scripts/validate_spec_citations.py.jinja +17 -6
  101. mapify_cli/templates_src/map/scripts/wayfind_runner.py.jinja +6 -3
  102. mapify_cli/templates_src/skills/map-architecture/SKILL.md.jinja +2 -2
  103. mapify_cli/templates_src/skills/map-check/SKILL.md.jinja +2 -2
  104. mapify_cli/templates_src/skills/map-efficient/SKILL.md.jinja +19 -8
  105. mapify_cli/templates_src/skills/map-efficient/efficient-reference.md.jinja +65 -21
  106. mapify_cli/templates_src/skills/map-explain/SKILL.md.jinja +2 -2
  107. mapify_cli/templates_src/skills/map-plan/SKILL.md.jinja +8 -8
  108. mapify_cli/templates_src/skills/map-release/release-reference.md.jinja +16 -7
  109. mapify_cli/templates_src/skills/map-resume/SKILL.md.jinja +3 -3
  110. mapify_cli/templates_src/skills/map-review/SKILL.md.jinja +17 -17
  111. mapify_cli/templates_src/skills/map-review/adversarial-reference.md.jinja +11 -5
  112. mapify_cli/templates_src/skills/map-review/review-reference.md.jinja +100 -4
  113. mapify_cli/templates_src/skills/map-state/scripts/get-plan-path.sh.jinja +5 -18
  114. mapify_cli/templates_src/skills/map-state/scripts/init-session.sh.jinja +3 -13
  115. mapify_cli/templates_src/skills/map-task/SKILL.md.jinja +2 -2
  116. mapify_cli/templates_src/skills/map-tdd/SKILL.md.jinja +5 -4
  117. mapify_cli/templates_src/skills/map-tokenreport/SKILL.md.jinja +1 -1
  118. mapify_cli/templates_src/skills/map-wayfind/wayfind-reference.md.jinja +9 -2
  119. mapify_cli/templates_src/skills/skill-rules.json.jinja +1 -1
  120. mapify_cli/verification_recorder.py +2 -1
  121. {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.0.dist-info}/METADATA +23 -4
  122. {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.0.dist-info}/RECORD +125 -124
  123. {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.0.dist-info}/WHEEL +1 -1
  124. {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.0.dist-info}/entry_points.txt +0 -0
  125. {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.0.dist-info}/licenses/LICENSE +0 -0
mapify_cli/__init__.py CHANGED
@@ -23,7 +23,7 @@ Or install globally:
23
23
  mapify check
24
24
  """
25
25
 
26
- __version__ = "3.34.2"
26
+ __version__ = "3.35.0"
27
27
 
28
28
  # MUST stay the first import: it turns "this interpreter is older than 3.11" into
29
29
  # a message that says so, instead of the ImportError raised by `from datetime
@@ -144,6 +144,7 @@ from mapify_cli.provider_registry import (
144
144
  SUPPORTED_PROVIDERS,
145
145
  skill_dir,
146
146
  )
147
+ from mapify_cli.ralph_state import sanitize_branch_name
147
148
 
148
149
  if TYPE_CHECKING:
149
150
  from mapify_cli.install_manifest import InstallManifest
@@ -434,7 +435,7 @@ def _detect_provider(project_path: Path) -> str:
434
435
  def get_project_health(project_path: Path) -> dict[str, Any]:
435
436
  """Collect project health diagnostics for check/doctor commands."""
436
437
  agent_exclude = {"README.md", "CHANGELOG.md", "MCP-PATTERNS.md"}
437
- current_branch = sanitize_identifier(get_current_branch_name())
438
+ current_branch = sanitize_branch_name(get_current_branch_name())
438
439
  branch_dir = project_path / ".map" / current_branch
439
440
  detected = _detect_provider(project_path)
440
441
 
@@ -548,7 +549,7 @@ def get_current_branch_name() -> str:
548
549
 
549
550
  def get_branch_workspace_dir(project_path: Path, branch: str | None = None) -> Path:
550
551
  """Return the branch-scoped MAP workspace directory."""
551
- branch_name = sanitize_identifier(branch or get_current_branch_name())
552
+ branch_name = sanitize_branch_name(branch or get_current_branch_name())
552
553
  return project_path / ".map" / branch_name
553
554
 
554
555
 
@@ -563,7 +564,7 @@ def get_branch_artifact_templates() -> dict[str, str]:
563
564
 
564
565
  def initialize_branch_workspace(project_path: Path, branch: str | None = None) -> Path:
565
566
  """Create branch-scoped planning artifacts inside `.map/<branch>/`."""
566
- branch_name = sanitize_identifier(branch or get_current_branch_name())
567
+ branch_name = sanitize_branch_name(branch or get_current_branch_name())
567
568
  workspace_dir = get_branch_workspace_dir(project_path, branch_name)
568
569
  workspace_dir.mkdir(parents=True, exist_ok=True)
569
570
 
@@ -579,7 +580,7 @@ def get_branch_workspace_status(
579
580
  project_path: Path, branch: str | None = None
580
581
  ) -> dict[str, Any]:
581
582
  """Collect status information for branch-scoped planning artifacts."""
582
- branch_name = sanitize_identifier(branch or get_current_branch_name())
583
+ branch_name = sanitize_branch_name(branch or get_current_branch_name())
583
584
  workspace_dir = get_branch_workspace_dir(project_path, branch_name)
584
585
  expected_files = list(get_branch_artifact_templates().keys())
585
586
  existing_files = (
@@ -511,6 +511,8 @@ _UPDATE_RUNTIME_GITIGNORE_PATHS = (
511
511
  ".map/provider-refresh.lock",
512
512
  ".map/installer.lock",
513
513
  ".map/gitignore.lock",
514
+ ".map/**/sessions/",
515
+ "**/__pycache__/",
514
516
  ".claude/worktrees/",
515
517
  )
516
518
  _WINDOWS_GITIGNORE_MUTEX_TIMEOUT_MS = 30_000
@@ -1135,6 +1137,48 @@ def _gitignore_pattern_matches(pattern: bytes, path: bytes) -> bool:
1135
1137
  return match(0, 0)
1136
1138
 
1137
1139
 
1140
+ def _gitignore_patterns_may_overlap(left: bytes, right: bytes) -> bool:
1141
+ """Conservatively intersect patterns, including recursive directory rules.
1142
+
1143
+ Unlike matching a concrete path, either side can contain wildcards. Two
1144
+ wildcard-bearing segments may overlap; literal segments still constrain the
1145
+ result so unrelated negations do not cause repeated ignore-file growth.
1146
+ """
1147
+ def parts(pattern: bytes) -> list[bytes]:
1148
+ stripped = pattern.rstrip(b"/")
1149
+ segments = stripped.lstrip(b"/").split(b"/")
1150
+ return [b"**", *segments] if b"/" not in stripped else segments
1151
+
1152
+ a, b = parts(left), parts(right)
1153
+ pending = [(0, 0)]
1154
+ seen: set[tuple[int, int]] = set()
1155
+ while pending:
1156
+ i, j = pending.pop()
1157
+ if (i, j) in seen:
1158
+ continue
1159
+ seen.add((i, j))
1160
+ if i == len(a) and j == len(b):
1161
+ return True
1162
+ if i < len(a) and a[i] == b"**":
1163
+ pending.append((i + 1, j))
1164
+ if j < len(b):
1165
+ pending.append((i, j + 1))
1166
+ elif j < len(b) and b[j] == b"**":
1167
+ pending.append((i, j + 1))
1168
+ if i < len(a):
1169
+ pending.append((i + 1, j))
1170
+ elif i < len(a) and j < len(b):
1171
+ a_glob = any(char in a[i] for char in (b"*", b"?", b"["))
1172
+ b_glob = any(char in b[j] for char in (b"*", b"?", b"["))
1173
+ if (
1174
+ (a_glob and b_glob)
1175
+ or fnmatchcase(a[i], b[j])
1176
+ or fnmatchcase(b[j], a[i])
1177
+ ):
1178
+ pending.append((i + 1, j + 1))
1179
+ return False
1180
+
1181
+
1138
1182
  def _negation_targets_path(negation: bytes, required: bytes) -> bool:
1139
1183
  """Return whether a later ``!`` rule can plausibly re-include *required*.
1140
1184
 
@@ -1148,6 +1192,10 @@ def _negation_targets_path(negation: bytes, required: bytes) -> bool:
1148
1192
  if not pattern:
1149
1193
  return False
1150
1194
  base = required.rstrip(b"/")
1195
+ if b"*" in base and _gitignore_patterns_may_overlap(
1196
+ pattern, required
1197
+ ):
1198
+ return True
1151
1199
  if _gitignore_pattern_matches(pattern, base):
1152
1200
  return True
1153
1201
  pattern_base = pattern.rstrip(b"/")
@@ -55,7 +55,8 @@ def _sanitize_branch(name: str) -> str:
55
55
  rest of MAP (a path-traversal hardening applied there is inherited here).
56
56
  Behaviour: replaces every character not in [a-zA-Z0-9_.-] with '-',
57
57
  collapses consecutive '-', strips leading/trailing '-', and falls back to
58
- "default" on empty result or path-traversal indicators.
58
+ "default" on empty input or path-traversal indicators. Non-empty names
59
+ stripped entirely by normalization use a deterministic SHA-256 scope.
59
60
  """
60
61
  return sanitize_branch_name(name)
61
62
 
mapify_cli/ralph_state.py CHANGED
@@ -10,6 +10,7 @@ State persisted to .map/<branch>/ralph_state.json
10
10
  Circuit breaker config source: .claude/ralph-loop-config.json
11
11
  """
12
12
 
13
+ import hashlib
13
14
  import json
14
15
  import re
15
16
  from dataclasses import dataclass, field
@@ -299,7 +300,7 @@ def sanitize_branch_name(branch: str) -> str:
299
300
  # Prevent path traversal attempts
300
301
  if ".." in sanitized or sanitized.startswith("."):
301
302
  return "default"
302
- return sanitized or "default"
303
+ return sanitized or ("branch-" + hashlib.sha256(branch.encode("utf-8")).hexdigest() if branch else "default")
303
304
 
304
305
 
305
306
  @dataclass
@@ -289,6 +289,7 @@ Rules:
289
289
  8. Do NOT add temporal comments about test failure status (e.g., "currently FAILS",
290
290
  "expected to FAIL", "will PASS once fix is applied"). Write tests as permanent,
291
291
  clean code — the Red/Green state is transient and must not leak into comments.
292
+ 9. Every test must pass the vacuous-test check in section 5 (Testing Considerations).
292
293
 
293
294
  Output:
294
295
  - Test files created via Write tool
@@ -395,6 +396,31 @@ Document key decisions using this structure:
395
396
  - For each `VCn:` item in `validation_criteria`, implement or update at least one automated test that would fail without your change and pass with it.
396
397
  - Prefer naming tests with `vc<n>` (e.g., `test_vc1_*`, `TestVC1*`) so Monitor can deterministically confirm coverage.
397
398
 
399
+ **Vacuous-test check (do-nothing implementation).** For every new or changed
400
+ test, name the assertion that would FAIL if the code under test were replaced by
401
+ a do-nothing implementation: each function returns the empty value of its return
402
+ type (`None`, `[]`, `{}`, `""`, `0`, `False`) and performs no side effect (no
403
+ write, no call, no state change). No such assertion means the test is
404
+ **vacuous**: it stays green when the code does nothing, so it verifies no behavior.
405
+
406
+ Typical vacuous shapes:
407
+ - no assertion at all: `process(data)`
408
+ - only a type/shape check an empty value satisfies: `assert isinstance(parse("a,b"), list)`
409
+ - only `is not None` / truthiness of a value an empty implementation also returns: `assert load({"x": 1}) is not None`
410
+ - only checks values the test set up itself, never calling the code under test: `repo.get.return_value = item; assert repo.get(1) == item`
411
+ - expects an exception the test itself raises, not the code under test: `with pytest.raises(KeyError): load({"x": 1})["missing"]`
412
+
413
+ Not vacuous: `assert parse("a,b") == ["a", "b"]`; expected-exception tests
414
+ (`pytest.raises`, `assertRaises`, `toThrow`) whose exception is raised by the code
415
+ under test, because a do-nothing implementation does not raise it; and, for code
416
+ whose job is a side effect, assertions on that
417
+ effect (written content, persisted row, a call with the expected arguments).
418
+ This check catches tests that verify nothing; it does not prove a test checks the
419
+ right thing (that needs mutation testing).
420
+
421
+ Self-check before handoff: rewrite or drop any vacuous test. Monitor counts it as
422
+ no coverage.
423
+
398
424
  **Format**:
399
425
  ```text
400
426
  1. test_[function]_[scenario]_[expected]
@@ -484,6 +510,7 @@ exhaustive test matrices by default.
484
510
  - [ ] Trade-offs documented with alternatives
485
511
  - [ ] Test cases cover happy + edge + error paths
486
512
  - [ ] Each `validation_criteria` item has at least one automated test (or explicit N/A with reason)
513
+ - [ ] No vacuous tests: each new or changed test fails against a do-nothing implementation
487
514
  - [ ] Template variables `{{...}}` preserved in generated code
488
515
 
489
516
  ### Hallucination Guard
@@ -624,6 +624,32 @@ Untested code is broken code waiting to happen. Testability indicates design qua
624
624
  - [ ] Happy path + error cases tested?
625
625
  - [ ] Edge cases covered?
626
626
  - [ ] Tests are deterministic (not flaky)?
627
+ - [ ] Every new or changed test fails against a do-nothing implementation?
628
+
629
+ **Vacuous-test check (do-nothing implementation).** For every new or changed
630
+ test, name the assertion that would FAIL if the code under test were replaced by
631
+ a do-nothing implementation: each function returns the empty value of its return
632
+ type (`None`, `[]`, `{}`, `""`, `0`, `False`) and performs no side effect (no
633
+ write, no call, no state change). No such assertion means the test is
634
+ **vacuous**: it stays green when the code does nothing, so it verifies no behavior.
635
+
636
+ Typical vacuous shapes:
637
+ - no assertion at all: `process(data)`
638
+ - only a type/shape check an empty value satisfies: `assert isinstance(parse("a,b"), list)`
639
+ - only `is not None` / truthiness of a value an empty implementation also returns: `assert load({"x": 1}) is not None`
640
+ - only checks values the test set up itself, never calling the code under test: `repo.get.return_value = item; assert repo.get(1) == item`
641
+ - expects an exception the test itself raises, not the code under test: `with pytest.raises(KeyError): load({"x": 1})["missing"]`
642
+
643
+ Not vacuous: `assert parse("a,b") == ["a", "b"]`; expected-exception tests
644
+ (`pytest.raises`, `assertRaises`, `toThrow`) whose exception is raised by the code
645
+ under test, because a do-nothing implementation does not raise it; and, for code
646
+ whose job is a side effect, assertions on that
647
+ effect (written content, persisted row, a call with the expected arguments).
648
+ This check catches tests that verify nothing; it does not prove a test checks the
649
+ right thing (that needs mutation testing).
650
+
651
+ Score vacuous tests as absent: they add nothing to test coverage or test
652
+ quality, and code covered only by vacuous tests is scored as untested.
627
653
 
628
654
  <example type="score_9">
629
655
  **Code**: Dependency injection, 95% coverage, tests for happy path + 5 error cases + 3 edge cases, mocked external APIs, isolated tests.
@@ -62,6 +62,26 @@ subtask metadata) apply only to Actor subtask validation, not standalone review.
62
62
 
63
63
  ### Actor runtime preflight in linked worktrees
64
64
 
65
+ **Managed Actor worktree:** use the `monitor_instruction` returned by
66
+ `create_subtask_worktree` (also included by `build_context_block` after creation).
67
+ It names the parent metadata root, candidate checkout, and two absolute runner
68
+ commands with **both** `--metadata-root <parent>` and `--worktree <candidate>`.
69
+ Run those commands instead of the checkout-local commands below. The runner
70
+ verifies the registered subtask/path and shared Git repository, reads the
71
+ blueprint and prior results from the parent, and computes the candidate diff
72
+ against its registered creation SHA. Parent dirty baselines never filter this
73
+ diff. Read AAG/research/state from the supplied parent branch directory; run
74
+ source inspection, builds, and tests in the candidate. Do not copy or symlink
75
+ mutable `.map/` artifacts into managed candidates, and do not globally redirect
76
+ `CLAUDE_PROJECT_DIR` to the parent. Explicit roots apply only to these gates.
77
+ Missing/mismatched registration, metadata, or runner is a blocking setup error:
78
+ report `valid: false`, `recommendation: "block"`; never fall back to the parent
79
+ diff or silently use the checkout-local gates. Unset inherited Git directory
80
+ and index variables for independent source/test commands as well.
81
+
82
+ **Unmanaged checkout without explicit managed context:** use the existing
83
+ checkout-local preflight below. A standalone `/map-review` skips both variants.
84
+
65
85
  Before invoking either Actor-specific `.map/scripts/map_step_runner.py` gate,
66
86
  clear inherited `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, and
67
87
  `GIT_INDEX_FILE` in a subshell before resolving the checkout being reviewed
@@ -1263,18 +1283,49 @@ for user in users:
1263
1283
  2. Check if external calls can be mocked
1264
1284
  3. Review included tests for coverage
1265
1285
  4. Validate test assertions are specific
1286
+ 5. Apply the vacuous-test check below to every new or changed test
1287
+
1288
+ **Vacuous-test check (do-nothing implementation).** For every new or changed
1289
+ test, name the assertion that would FAIL if the code under test were replaced by
1290
+ a do-nothing implementation: each function returns the empty value of its return
1291
+ type (`None`, `[]`, `{}`, `""`, `0`, `False`) and performs no side effect (no
1292
+ write, no call, no state change). No such assertion means the test is
1293
+ **vacuous**: it stays green when the code does nothing, so it verifies no behavior.
1294
+
1295
+ Typical vacuous shapes:
1296
+ - no assertion at all: `process(data)`
1297
+ - only a type/shape check an empty value satisfies: `assert isinstance(parse("a,b"), list)`
1298
+ - only `is not None` / truthiness of a value an empty implementation also returns: `assert load({"x": 1}) is not None`
1299
+ - only checks values the test set up itself, never calling the code under test: `repo.get.return_value = item; assert repo.get(1) == item`
1300
+ - expects an exception the test itself raises, not the code under test: `with pytest.raises(KeyError): load({"x": 1})["missing"]`
1301
+
1302
+ Not vacuous: `assert parse("a,b") == ["a", "b"]`; expected-exception tests
1303
+ (`pytest.raises`, `assertRaises`, `toThrow`) whose exception is raised by the code
1304
+ under test, because a do-nothing implementation does not raise it; and, for code
1305
+ whose job is a side effect, assertions on that
1306
+ effect (written content, persisted row, a call with the expected arguments).
1307
+ This check catches tests that verify nothing; it does not prove a test checks the
1308
+ right thing (that needs mutation testing).
1309
+
1310
+ A vacuous test does not count as coverage: a `VCn` or requirement covered only
1311
+ by vacuous tests has `test_coverage != PASS` and falls under the contract
1312
+ Decision Rule. Report each one as a `testability` issue titled
1313
+ `Vacuous test: <test id>` that names the missing assertion; severity HIGH when it
1314
+ is the only test for a requirement, otherwise MEDIUM.
1266
1315
 
1267
1316
  #### Pass Criteria
1268
1317
  - Dependencies injected, not hardcoded
1269
1318
  - Tests cover happy path and errors
1270
1319
  - Tests are deterministic and isolated
1271
1320
  - Assertions validate specific behaviors
1321
+ - Every new or changed test fails against a do-nothing implementation
1272
1322
 
1273
1323
  #### Common Failures
1274
1324
  - Hardcoded external dependencies
1275
1325
  - Missing tests for error cases
1276
1326
  - Flaky tests (time-dependent, order-dependent)
1277
1327
  - Generic assertions (assertTrue without specifics)
1328
+ - Vacuous tests that stay green against a do-nothing implementation
1278
1329
 
1279
1330
  #### Severity Mapping
1280
1331
  - **Critical**: Untestable design blocking all testing
@@ -28,7 +28,16 @@ changed-hunk attribution instead of shell commands. A line existing at base does
28
28
  not prove a behavioral defect is pre-existing; require supporting context or mark
29
29
  attribution unknown. Request missing bundle content from the parent, never shell.
30
30
 
31
- Return the role's envelope with an additional receipt object:
31
+ When the prompt provides parent-served context, return its exact context_request
32
+ JSON and consume context_chunk continuations in this same session. Follow
33
+ next_offset through eof and check offsets, total size and hash metadata against the manifest before using it. Request
34
+ permitted resources from the parent, never read context object paths directly.
35
+ These intermediate responses are not findings or final review envelopes.
36
+ Wait for the parent's post-delivery finalization request before returning a final
37
+ review. In structured final envelopes, echo its session_id and finalization_token
38
+ in a context_receipt object.
39
+ Delivery is parent_acknowledged; it does not prove inspection or capability.
40
+ After receiving needed evidence, return the role's envelope with an additional receipt object:
32
41
  `receipt={covered_files:[],skipped_files:[],denied_access:[],check_evidence:[check_id]}`.
33
42
  List only paths actually inspected in covered_files; report skipped scope and denied
34
43
  required access honestly. Reading claims are self_reported, not tool_witnessed.
@@ -22,6 +22,32 @@ changes, never revert unrelated work, and adapt to concurrent edits.
22
22
  6. Never edit generated MAP trees directly when repository instructions designate a
23
23
  template source; edit the source and run the documented renderer.
24
24
  7. Do not commit, publish, delete broad paths, or expand scope without explicit authority.
25
+ 8. Before reporting, apply the vacuous-test check below to every test you wrote or
26
+ changed; rewrite or drop any vacuous test. Monitor counts it as no coverage.
27
+
28
+ # Vacuous tests
29
+
30
+ **Vacuous-test check (do-nothing implementation).** For every new or changed
31
+ test, name the assertion that would FAIL if the code under test were replaced by
32
+ a do-nothing implementation: each function returns the empty value of its return
33
+ type (`None`, `[]`, `{}`, `""`, `0`, `False`) and performs no side effect (no
34
+ write, no call, no state change). No such assertion means the test is
35
+ **vacuous**: it stays green when the code does nothing, so it verifies no behavior.
36
+
37
+ Typical vacuous shapes:
38
+ - no assertion at all: `process(data)`
39
+ - only a type/shape check an empty value satisfies: `assert isinstance(parse("a,b"), list)`
40
+ - only `is not None` / truthiness of a value an empty implementation also returns: `assert load({"x": 1}) is not None`
41
+ - only checks values the test set up itself, never calling the code under test: `repo.get.return_value = item; assert repo.get(1) == item`
42
+ - expects an exception the test itself raises, not the code under test: `with pytest.raises(KeyError): load({"x": 1})["missing"]`
43
+
44
+ Not vacuous: `assert parse("a,b") == ["a", "b"]`; expected-exception tests
45
+ (`pytest.raises`, `assertRaises`, `toThrow`) whose exception is raised by the code
46
+ under test, because a do-nothing implementation does not raise it; and, for code
47
+ whose job is a side effect, assertions on that
48
+ effect (written content, persisted row, a call with the expected arguments).
49
+ This check catches tests that verify nothing; it does not prove a test checks the
50
+ right thing (that needs mutation testing).
25
51
 
26
52
  # Output
27
53
 
@@ -88,6 +88,31 @@ Weighting: readability 40%, maintainability 30%, idioms 30%.
88
88
  0-1: hardcoded dependencies, no test consideration.
89
89
  Weighting: test coverage 40%, test quality 30%, design for testability 30%.
90
90
 
91
+ **Vacuous-test check (do-nothing implementation).** For every new or changed
92
+ test, name the assertion that would FAIL if the code under test were replaced by
93
+ a do-nothing implementation: each function returns the empty value of its return
94
+ type (`None`, `[]`, `{}`, `""`, `0`, `False`) and performs no side effect (no
95
+ write, no call, no state change). No such assertion means the test is
96
+ **vacuous**: it stays green when the code does nothing, so it verifies no behavior.
97
+
98
+ Typical vacuous shapes:
99
+ - no assertion at all: `process(data)`
100
+ - only a type/shape check an empty value satisfies: `assert isinstance(parse("a,b"), list)`
101
+ - only `is not None` / truthiness of a value an empty implementation also returns: `assert load({"x": 1}) is not None`
102
+ - only checks values the test set up itself, never calling the code under test: `repo.get.return_value = item; assert repo.get(1) == item`
103
+ - expects an exception the test itself raises, not the code under test: `with pytest.raises(KeyError): load({"x": 1})["missing"]`
104
+
105
+ Not vacuous: `assert parse("a,b") == ["a", "b"]`; expected-exception tests
106
+ (`pytest.raises`, `assertRaises`, `toThrow`) whose exception is raised by the code
107
+ under test, because a do-nothing implementation does not raise it; and, for code
108
+ whose job is a side effect, assertions on that
109
+ effect (written content, persisted row, a call with the expected arguments).
110
+ This check catches tests that verify nothing; it does not prove a test checks the
111
+ right thing (that needs mutation testing).
112
+
113
+ Score vacuous tests as absent: they add nothing to test coverage or test
114
+ quality, and code covered only by vacuous tests is scored as untested.
115
+
91
116
  ## 6. Performance (0-10)
92
117
  10: efficient algorithms, appropriate data structures, handles scale.
93
118
  8-9: reasonable complexity, minor optimizations possible.
@@ -53,6 +53,26 @@ subtask metadata) apply only to Actor subtask validation, not standalone review.
53
53
 
54
54
  ### Actor runtime preflight in linked worktrees
55
55
 
56
+ **Managed Actor worktree:** use the `monitor_instruction` returned by
57
+ `create_subtask_worktree` (also included by `build_context_block` after creation).
58
+ It names the parent metadata root, candidate checkout, and two absolute runner
59
+ commands with **both** `--metadata-root <parent>` and `--worktree <candidate>`.
60
+ Run those commands instead of the checkout-local commands below. The runner
61
+ verifies the registered subtask/path and shared Git repository, reads the
62
+ blueprint and prior results from the parent, and computes the candidate diff
63
+ against its registered creation SHA. Parent dirty baselines never filter this
64
+ diff. Read AAG/research/state from the supplied parent branch directory; run
65
+ source inspection, builds, and tests in the candidate. Do not copy or symlink
66
+ mutable `.map/` artifacts into managed candidates, and do not globally redirect
67
+ `CLAUDE_PROJECT_DIR` to the parent. Explicit roots apply only to these gates.
68
+ Missing/mismatched registration, metadata, or runner is a blocking setup error:
69
+ report `valid: false`, `recommendation: "block"`; never fall back to the parent
70
+ diff or silently use the checkout-local gates. Unset inherited Git directory
71
+ and index variables for independent source/test commands as well.
72
+
73
+ **Unmanaged checkout without explicit managed context:** use the existing
74
+ checkout-local preflight below. A standalone `/map-review` skips both variants.
75
+
56
76
  Before invoking either Actor-specific `.map/scripts/map_step_runner.py` gate,
57
77
  clear inherited `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, and
58
78
  `GIT_INDEX_FILE` in a subshell before resolving the checkout being reviewed
@@ -458,11 +478,40 @@ What to Check:
458
478
  - Tests included (happy path, errors, edge cases)
459
479
  - Test quality (deterministic, isolated, specific assertions)
460
480
 
481
+ **Vacuous-test check (do-nothing implementation).** For every new or changed
482
+ test, name the assertion that would FAIL if the code under test were replaced by
483
+ a do-nothing implementation: each function returns the empty value of its return
484
+ type (`None`, `[]`, `{}`, `""`, `0`, `False`) and performs no side effect (no
485
+ write, no call, no state change). No such assertion means the test is
486
+ **vacuous**: it stays green when the code does nothing, so it verifies no behavior.
487
+
488
+ Typical vacuous shapes:
489
+ - no assertion at all: `process(data)`
490
+ - only a type/shape check an empty value satisfies: `assert isinstance(parse("a,b"), list)`
491
+ - only `is not None` / truthiness of a value an empty implementation also returns: `assert load({"x": 1}) is not None`
492
+ - only checks values the test set up itself, never calling the code under test: `repo.get.return_value = item; assert repo.get(1) == item`
493
+ - expects an exception the test itself raises, not the code under test: `with pytest.raises(KeyError): load({"x": 1})["missing"]`
494
+
495
+ Not vacuous: `assert parse("a,b") == ["a", "b"]`; expected-exception tests
496
+ (`pytest.raises`, `assertRaises`, `toThrow`) whose exception is raised by the code
497
+ under test, because a do-nothing implementation does not raise it; and, for code
498
+ whose job is a side effect, assertions on that
499
+ effect (written content, persisted row, a call with the expected arguments).
500
+ This check catches tests that verify nothing; it does not prove a test checks the
501
+ right thing (that needs mutation testing).
502
+
503
+ A vacuous test does not count as coverage: a `VCn` or requirement covered only
504
+ by vacuous tests has `test_coverage != PASS` and falls under the contract
505
+ Decision Rule. Report each one as a `testability` issue titled
506
+ `Vacuous test: <test id>` that names the missing assertion; severity HIGH when it
507
+ is the only test for a requirement, otherwise MEDIUM.
508
+
461
509
  Pass Criteria:
462
510
  - Dependencies injected, not hardcoded
463
511
  - Tests cover happy path and errors
464
512
  - Tests are deterministic and isolated
465
513
  - Assertions validate specific behaviors
514
+ - Every new or changed test fails against a do-nothing implementation
466
515
 
467
516
  Severity Mapping:
468
517
  - Critical: Untestable design blocking all testing
@@ -16,7 +16,16 @@ changed-hunk attribution. Native shell-free read/search has not been established
16
16
  Request missing content from the parent; never restore shell or guess attribution.
17
17
  Existing base lines alone do not prove that a behavioral defect is pre-existing.
18
18
 
19
- Return the role envelope plus
19
+ When a generated prompt provides parent-served context, first return its exact
20
+ context_request JSON and consume context_chunk continuations in this same session.
21
+ Follow next_offset through eof; check offsets, total size and hash metadata against the manifest before using it. Request
22
+ missing permitted resources from the parent, never object paths or extra tools.
23
+ These are intermediate responses, not findings or final review envelopes.
24
+ Wait for the parent's post-delivery finalization request before returning a final
25
+ review. In structured final envelopes, echo its session_id and finalization_token
26
+ in a context_receipt object.
27
+ Delivery is parent_acknowledged; it does not prove inspection or capability.
28
+ After receiving needed evidence, return the role envelope plus
20
29
  receipt={covered_files:[],skipped_files:[],denied_access:[],check_evidence:[check_id]}.
21
30
  covered_files lists only paths actually inspected, including supplied source text.
22
31
  Report missing scope and denied access honestly. Reading claims are self_reported,
@@ -53,6 +53,7 @@ if tuple(sys.version_info) < (3, 11):
53
53
  sys.stderr.write(_MAP_PYTHON_PROBLEM)
54
54
  sys.exit(1)
55
55
 
56
+ import hashlib
56
57
  import json
57
58
  import os
58
59
  import re
@@ -76,23 +77,33 @@ def _sanitize_branch(branch: str) -> str:
76
77
  sanitized = re.sub(r"-+", "-", sanitized).strip("-")
77
78
  if ".." in sanitized or sanitized.startswith("."):
78
79
  return "default"
79
- return sanitized or "default"
80
+ return sanitized or ("branch-" + hashlib.sha256(branch.encode("utf-8")).hexdigest() if branch else "default")
80
81
 
81
82
 
82
83
  def _get_branch() -> str:
84
+ """Resolve branch scope before the first commit; warn on unavailable Git."""
83
85
  try:
84
- result = subprocess.run(
85
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
86
- capture_output=True,
87
- text=True,
88
- cwd=PROJECT_DIR,
89
- timeout=2,
90
- check=False,
91
- )
86
+ # Symbolic HEAD exists even before the branch has its first commit.
87
+ # rev-parse retains the historical detached-HEAD fallback.
88
+ for command in (
89
+ ["symbolic-ref", "--quiet", "--short", "HEAD"],
90
+ ["rev-parse", "--abbrev-ref", "HEAD"],
91
+ ):
92
+ result = subprocess.run(
93
+ ["git", *command],
94
+ capture_output=True,
95
+ text=True,
96
+ cwd=PROJECT_DIR,
97
+ timeout=2,
98
+ check=False,
99
+ )
100
+ if result.returncode == 0 and result.stdout.strip():
101
+ break
92
102
  if result.returncode == 0:
93
103
  return _sanitize_branch(result.stdout.strip())
94
104
  except Exception: # noqa: BLE001, S110 -- deliberate fallback/resilience boundary, must not propagate
95
105
  pass
106
+ print("MAP warning: could not determine Git branch; using .map/default/ for workflow artifacts.", file=sys.stderr)
96
107
  return "default"
97
108
 
98
109
 
@@ -58,6 +58,7 @@ if tuple(sys.version_info) < (3, 11):
58
58
  sys.stderr.write(_MAP_PYTHON_PROBLEM)
59
59
  sys.exit(1)
60
60
 
61
+ import hashlib
61
62
  import json
62
63
  import os
63
64
  import re
@@ -75,23 +76,33 @@ def _sanitize_branch(branch: str) -> str:
75
76
  sanitized = re.sub(r"-+", "-", sanitized).strip("-")
76
77
  if ".." in sanitized or sanitized.startswith("."):
77
78
  return "default"
78
- return sanitized or "default"
79
+ return sanitized or ("branch-" + hashlib.sha256(branch.encode("utf-8")).hexdigest() if branch else "default")
79
80
 
80
81
 
81
82
  def _get_branch() -> str:
83
+ """Resolve branch scope before the first commit; warn on unavailable Git."""
82
84
  try:
83
- result = subprocess.run(
84
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
85
- capture_output=True,
86
- text=True,
87
- cwd=PROJECT_DIR,
88
- timeout=2,
89
- check=False,
90
- )
85
+ # Symbolic HEAD exists even before the branch has its first commit.
86
+ # rev-parse retains the historical detached-HEAD fallback.
87
+ for command in (
88
+ ["symbolic-ref", "--quiet", "--short", "HEAD"],
89
+ ["rev-parse", "--abbrev-ref", "HEAD"],
90
+ ):
91
+ result = subprocess.run(
92
+ ["git", *command],
93
+ capture_output=True,
94
+ text=True,
95
+ cwd=PROJECT_DIR,
96
+ timeout=2,
97
+ check=False,
98
+ )
99
+ if result.returncode == 0 and result.stdout.strip():
100
+ break
91
101
  if result.returncode == 0:
92
102
  return _sanitize_branch(result.stdout.strip())
93
103
  except Exception: # noqa: BLE001, S110 -- deliberate fallback/resilience boundary, must not propagate
94
104
  pass
105
+ print("MAP warning: could not determine Git branch; using .map/default/ for workflow artifacts.", file=sys.stderr)
95
106
  return "default"
96
107
 
97
108
 
@@ -37,6 +37,7 @@ if tuple(sys.version_info) < (3, 11):
37
37
  sys.stderr.write(_MAP_PYTHON_PROBLEM)
38
38
  sys.exit(1)
39
39
 
40
+ import hashlib
40
41
  import json
41
42
  import os
42
43
  import re
@@ -64,24 +65,33 @@ def sanitize_branch_name(branch: str) -> str:
64
65
  sanitized = re.sub(r"-+", "-", sanitized).strip("-")
65
66
  if ".." in sanitized or sanitized.startswith("."):
66
67
  return "default"
67
- return sanitized or "default"
68
+ return sanitized or ("branch-" + hashlib.sha256(branch.encode("utf-8")).hexdigest() if branch else "default")
68
69
 
69
70
 
70
71
  def get_branch_name() -> str:
71
72
  """Get current git branch name."""
72
73
  try:
73
- result = subprocess.run(
74
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
75
- capture_output=True,
76
- text=True,
77
- cwd=PROJECT_DIR,
78
- timeout=2,
79
- check=False,
80
- )
74
+ # Symbolic HEAD exists even before the branch has its first commit.
75
+ # rev-parse retains the historical detached-HEAD fallback.
76
+ for command in (
77
+ ["symbolic-ref", "--quiet", "--short", "HEAD"],
78
+ ["rev-parse", "--abbrev-ref", "HEAD"],
79
+ ):
80
+ result = subprocess.run(
81
+ ["git", *command],
82
+ capture_output=True,
83
+ text=True,
84
+ cwd=PROJECT_DIR,
85
+ timeout=2,
86
+ check=False,
87
+ )
88
+ if result.returncode == 0 and result.stdout.strip():
89
+ break
81
90
  if result.returncode == 0:
82
91
  return sanitize_branch_name(result.stdout.strip())
83
92
  except Exception: # noqa: BLE001, S110 -- deliberate fallback/resilience boundary, must not propagate
84
93
  pass
94
+ print("MAP warning: could not determine Git branch; using .map/default/ for workflow artifacts.", file=sys.stderr)
85
95
  return "default"
86
96
 
87
97