mapify-cli 3.34.2__py3-none-any.whl → 3.35.1__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.
- mapify_cli/__init__.py +6 -5
- mapify_cli/delivery/file_copier.py +48 -0
- mapify_cli/memory/capture.py +2 -1
- mapify_cli/ralph_state.py +2 -1
- mapify_cli/schemas.py +8 -0
- mapify_cli/templates/agents/actor.md +27 -0
- mapify_cli/templates/agents/evaluator.md +26 -0
- mapify_cli/templates/agents/monitor.md +51 -0
- mapify_cli/templates/agents/review-reader.md +10 -1
- mapify_cli/templates/codex/agents/actor.toml +26 -0
- mapify_cli/templates/codex/agents/evaluator.toml +25 -0
- mapify_cli/templates/codex/agents/monitor.toml +49 -0
- mapify_cli/templates/codex/agents/review-reader.toml +13 -1
- mapify_cli/templates/codex/hooks/context-meter.py +20 -9
- mapify_cli/templates/codex/hooks/map-token-meter.py +20 -9
- mapify_cli/templates/codex/hooks/post-compact-context.py +19 -9
- mapify_cli/templates/codex/hooks/pre-compact-save-transcript.py +19 -9
- mapify_cli/templates/codex/hooks/ralph-context-pruner.py +19 -9
- mapify_cli/templates/codex/hooks/ralph-iteration-logger.py +22 -11
- mapify_cli/templates/codex/hooks/scrub-internal-ids.py +18 -7
- mapify_cli/templates/codex/hooks/workflow-context-injector.py +19 -9
- mapify_cli/templates/codex/hooks/workflow-gate.py +19 -9
- mapify_cli/templates/codex/skills/map-architecture/SKILL.md +2 -2
- mapify_cli/templates/codex/skills/map-check/SKILL.md +2 -2
- mapify_cli/templates/codex/skills/map-efficient/SKILL.md +15 -5
- mapify_cli/templates/codex/skills/map-efficient/efficient-reference.md +41 -11
- mapify_cli/templates/codex/skills/map-explain/SKILL.md +1 -1
- mapify_cli/templates/codex/skills/map-plan/SKILL.md +7 -7
- mapify_cli/templates/codex/skills/map-release/release-reference.md +16 -7
- mapify_cli/templates/codex/skills/map-resume/SKILL.md +3 -3
- mapify_cli/templates/codex/skills/map-review/SKILL.md +45 -131
- mapify_cli/templates/codex/skills/map-review/adversarial-reference.md +14 -90
- mapify_cli/templates/codex/skills/map-review/review-reference.md +117 -5
- mapify_cli/templates/codex/skills/map-state/scripts/get-plan-path.sh +5 -17
- mapify_cli/templates/codex/skills/map-state/scripts/init-session.sh +3 -12
- mapify_cli/templates/codex/skills/map-task/SKILL.md +2 -2
- mapify_cli/templates/codex/skills/map-tdd/SKILL.md +5 -4
- mapify_cli/templates/codex/skills/map-tokenreport/SKILL.md +1 -1
- mapify_cli/templates/codex/skills/map-wayfind/wayfind-reference.md +9 -2
- mapify_cli/templates/hooks/context-meter.py +20 -9
- mapify_cli/templates/hooks/map-statusline.py +2 -1
- mapify_cli/templates/hooks/map-token-meter.py +20 -9
- mapify_cli/templates/hooks/post-compact-context.py +19 -9
- mapify_cli/templates/hooks/pre-compact-save-transcript.py +19 -9
- mapify_cli/templates/hooks/ralph-context-pruner.py +19 -9
- mapify_cli/templates/hooks/ralph-iteration-logger.py +22 -11
- mapify_cli/templates/hooks/scrub-internal-ids.py +18 -7
- mapify_cli/templates/hooks/workflow-context-injector.py +19 -9
- mapify_cli/templates/hooks/workflow-gate.py +19 -9
- mapify_cli/templates/map/scripts/codex_review_host.py +381 -0
- mapify_cli/templates/map/scripts/codex_review_runner.py +335 -0
- mapify_cli/templates/map/scripts/diagnostics.py +20 -8
- mapify_cli/templates/map/scripts/map_step_runner.py +1602 -243
- mapify_cli/templates/map/scripts/map_utils.py +37 -15
- mapify_cli/templates/map/scripts/scrub_internal_ids.py +110 -74
- mapify_cli/templates/map/scripts/sofa_client.py +46 -0
- mapify_cli/templates/map/scripts/validate_spec_citations.py +17 -6
- mapify_cli/templates/map/scripts/wayfind_runner.py +6 -3
- mapify_cli/templates/skills/map-architecture/SKILL.md +2 -2
- mapify_cli/templates/skills/map-check/SKILL.md +2 -2
- mapify_cli/templates/skills/map-efficient/SKILL.md +5 -4
- mapify_cli/templates/skills/map-efficient/efficient-reference.md +51 -10
- mapify_cli/templates/skills/map-explain/SKILL.md +1 -1
- mapify_cli/templates/skills/map-plan/SKILL.md +1 -1
- mapify_cli/templates/skills/map-release/release-reference.md +16 -7
- mapify_cli/templates/skills/map-resume/SKILL.md +3 -3
- mapify_cli/templates/skills/map-review/SKILL.md +16 -16
- mapify_cli/templates/skills/map-review/adversarial-reference.md +10 -4
- mapify_cli/templates/skills/map-review/review-reference.md +105 -5
- mapify_cli/templates/skills/map-state/scripts/get-plan-path.sh +5 -17
- mapify_cli/templates/skills/map-state/scripts/init-session.sh +3 -12
- mapify_cli/templates/skills/map-task/SKILL.md +2 -2
- mapify_cli/templates/skills/map-tdd/SKILL.md +5 -4
- mapify_cli/templates/skills/map-tokenreport/SKILL.md +1 -1
- mapify_cli/templates/skills/map-wayfind/wayfind-reference.md +9 -2
- mapify_cli/templates/skills/skill-rules.json +1 -1
- mapify_cli/templates_src/_partials/monitor-review-context.md.jinja +20 -0
- mapify_cli/templates_src/_partials/vacuous-test-check.md.jinja +21 -0
- mapify_cli/templates_src/agents/actor.md.jinja +6 -0
- mapify_cli/templates_src/agents/evaluator.md.jinja +5 -0
- mapify_cli/templates_src/agents/monitor.md.jinja +10 -0
- mapify_cli/templates_src/agents/review-reader.md.jinja +10 -1
- mapify_cli/templates_src/codex/agents/actor.toml.jinja +5 -0
- mapify_cli/templates_src/codex/agents/evaluator.toml.jinja +4 -0
- mapify_cli/templates_src/codex/agents/monitor.toml.jinja +8 -0
- mapify_cli/templates_src/codex/agents/review-reader.toml.jinja +13 -1
- mapify_cli/templates_src/codex/hooks/workflow-gate.py.jinja +19 -9
- mapify_cli/templates_src/hooks/context-meter.py.jinja +20 -9
- mapify_cli/templates_src/hooks/map-statusline.py.jinja +2 -1
- mapify_cli/templates_src/hooks/map-token-meter.py.jinja +20 -9
- mapify_cli/templates_src/hooks/post-compact-context.py.jinja +19 -9
- mapify_cli/templates_src/hooks/pre-compact-save-transcript.py.jinja +19 -9
- mapify_cli/templates_src/hooks/ralph-context-pruner.py.jinja +19 -9
- mapify_cli/templates_src/hooks/ralph-iteration-logger.py.jinja +22 -11
- mapify_cli/templates_src/hooks/scrub-internal-ids.py.jinja +18 -7
- mapify_cli/templates_src/hooks/workflow-context-injector.py.jinja +19 -9
- mapify_cli/templates_src/hooks/workflow-gate.py.jinja +19 -9
- mapify_cli/templates_src/map/scripts/codex_review_host.py.jinja +381 -0
- mapify_cli/templates_src/map/scripts/codex_review_runner.py.jinja +313 -0
- mapify_cli/templates_src/map/scripts/diagnostics.py.jinja +20 -8
- mapify_cli/templates_src/map/scripts/map_step_runner.py.jinja +1602 -243
- mapify_cli/templates_src/map/scripts/map_utils.py.jinja +37 -15
- mapify_cli/templates_src/map/scripts/scrub_internal_ids.py.jinja +110 -74
- mapify_cli/templates_src/map/scripts/sofa_client.py.jinja +46 -0
- mapify_cli/templates_src/map/scripts/validate_spec_citations.py.jinja +17 -6
- mapify_cli/templates_src/map/scripts/wayfind_runner.py.jinja +6 -3
- mapify_cli/templates_src/skills/map-architecture/SKILL.md.jinja +2 -2
- mapify_cli/templates_src/skills/map-check/SKILL.md.jinja +2 -2
- mapify_cli/templates_src/skills/map-efficient/SKILL.md.jinja +19 -8
- mapify_cli/templates_src/skills/map-efficient/efficient-reference.md.jinja +65 -21
- mapify_cli/templates_src/skills/map-explain/SKILL.md.jinja +2 -2
- mapify_cli/templates_src/skills/map-plan/SKILL.md.jinja +8 -8
- mapify_cli/templates_src/skills/map-release/release-reference.md.jinja +16 -7
- mapify_cli/templates_src/skills/map-resume/SKILL.md.jinja +3 -3
- mapify_cli/templates_src/skills/map-review/SKILL.md.jinja +76 -26
- mapify_cli/templates_src/skills/map-review/adversarial-reference.md.jinja +35 -5
- mapify_cli/templates_src/skills/map-review/review-reference.md.jinja +125 -5
- mapify_cli/templates_src/skills/map-state/scripts/get-plan-path.sh.jinja +5 -18
- mapify_cli/templates_src/skills/map-state/scripts/init-session.sh.jinja +3 -13
- mapify_cli/templates_src/skills/map-task/SKILL.md.jinja +2 -2
- mapify_cli/templates_src/skills/map-tdd/SKILL.md.jinja +5 -4
- mapify_cli/templates_src/skills/map-tokenreport/SKILL.md.jinja +1 -1
- mapify_cli/templates_src/skills/map-wayfind/wayfind-reference.md.jinja +9 -2
- mapify_cli/templates_src/skills/skill-rules.json.jinja +1 -1
- mapify_cli/verification_recorder.py +2 -1
- {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.1.dist-info}/METADATA +23 -4
- {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.1.dist-info}/RECORD +130 -125
- {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.1.dist-info}/WHEEL +1 -1
- {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.1.dist-info}/entry_points.txt +0 -0
- {mapify_cli-3.34.2.dist-info → mapify_cli-3.35.1.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.
|
|
26
|
+
__version__ = "3.35.1"
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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"/")
|
mapify_cli/memory/capture.py
CHANGED
|
@@ -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
|
|
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
|
mapify_cli/schemas.py
CHANGED
|
@@ -1426,6 +1426,14 @@ REVIEW_BUNDLE_SCHEMA = {
|
|
|
1426
1426
|
"files": {"type": "array", "items": {"type": "object"}},
|
|
1427
1427
|
}, "required": ["status", "files"], "additionalProperties": True,
|
|
1428
1428
|
},
|
|
1429
|
+
"context_artifacts": {
|
|
1430
|
+
"type": "object", "additionalProperties": {
|
|
1431
|
+
"type": "array", "items": {
|
|
1432
|
+
"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}},
|
|
1433
|
+
"required": ["path", "content"], "additionalProperties": False,
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
},
|
|
1429
1437
|
"generated_at": {"type": "string"},
|
|
1430
1438
|
"artifacts": {
|
|
1431
1439
|
"type": "object",
|
|
@@ -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
|
-
|
|
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
|
|
@@ -3,6 +3,9 @@ description = "Shell-free MAP reviewer using runner-supplied source evidence"
|
|
|
3
3
|
sandbox_mode = "read-only"
|
|
4
4
|
|
|
5
5
|
developer_instructions = """
|
|
6
|
+
Dispatch status: optional legacy native reader; unverified and unable to approve.
|
|
7
|
+
The supported /map-review path uses codex_review_runner.py environment-free threads.
|
|
8
|
+
|
|
6
9
|
You are an independent reviewer, never an executor or editor. Follow the role,
|
|
7
10
|
quality rubric, evidence requirements and output schema in the generated prompt
|
|
8
11
|
exactly: Monitor correctness/security/testing, Predictor integration/reachability,
|
|
@@ -16,7 +19,16 @@ changed-hunk attribution. Native shell-free read/search has not been established
|
|
|
16
19
|
Request missing content from the parent; never restore shell or guess attribution.
|
|
17
20
|
Existing base lines alone do not prove that a behavioral defect is pre-existing.
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
When a generated prompt provides parent-served context, first return its exact
|
|
23
|
+
context_request JSON and consume context_chunk continuations in this same session.
|
|
24
|
+
Follow next_offset through eof; check offsets, total size and hash metadata against the manifest before using it. Request
|
|
25
|
+
missing permitted resources from the parent, never object paths or extra tools.
|
|
26
|
+
These are intermediate responses, not findings or final review envelopes.
|
|
27
|
+
Wait for the parent's post-delivery finalization request before returning a final
|
|
28
|
+
review. In structured final envelopes, echo its session_id and finalization_token
|
|
29
|
+
in a context_receipt object.
|
|
30
|
+
Delivery is parent_acknowledged; it does not prove inspection or capability.
|
|
31
|
+
After receiving needed evidence, return the role envelope plus
|
|
20
32
|
receipt={covered_files:[],skipped_files:[],denied_access:[],check_evidence:[check_id]}.
|
|
21
33
|
covered_files lists only paths actually inspected, including supplied source text.
|
|
22
34
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|