@hunter-harness/workflow-harness 0.2.4 → 0.2.5
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.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +15 -3
- package/harness/bundles/general/claude-code/harness-run/checklist.md +7 -3
- package/harness/bundles/general/claude-code/harness-run/reference.md +3 -9
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +5 -3
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +14 -2
- package/harness/bundles/general/claude-code/harness-test/checklist.md +4 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +4 -4
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +29 -1
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +15 -3
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +7 -3
- package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -9
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +5 -3
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +14 -2
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +4 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +4 -4
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +29 -1
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +15 -3
- package/harness/bundles/general/codex/harness-run/checklist.md +7 -3
- package/harness/bundles/general/codex/harness-run/reference.md +3 -9
- package/harness/bundles/general/codex/harness-submit/SKILL.md +5 -3
- package/harness/bundles/general/codex/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +14 -2
- package/harness/bundles/general/codex/harness-test/checklist.md +4 -0
- package/harness/bundles/general/codex/harness-test/reference.md +4 -4
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/general/codex/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/codex/scripts/harness_profile.py +29 -1
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +15 -3
- package/harness/bundles/general/cursor/harness-run/checklist.md +7 -3
- package/harness/bundles/general/cursor/harness-run/reference.md +3 -9
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +5 -3
- package/harness/bundles/general/cursor/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +14 -2
- package/harness/bundles/general/cursor/harness-test/checklist.md +4 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +4 -4
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/cursor/scripts/harness_profile.py +29 -1
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +18 -4
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +5 -3
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +16 -2
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +29 -1
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +18 -4
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +5 -3
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +16 -2
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +29 -1
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +18 -4
- package/harness/bundles/java/codex/harness-submit/SKILL.md +5 -3
- package/harness/bundles/java/codex/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +16 -2
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/java/codex/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/codex/scripts/harness_profile.py +29 -1
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +18 -4
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +5 -3
- package/harness/bundles/java/cursor/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +16 -2
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/cursor/scripts/harness_profile.py +29 -1
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +502 -0
- package/harness/manifests/general/claude-code.json +25 -21
- package/harness/manifests/general/codebuddy.json +25 -21
- package/harness/manifests/general/codex.json +25 -21
- package/harness/manifests/general/cursor.json +25 -21
- package/harness/manifests/java/claude-code.json +23 -19
- package/harness/manifests/java/codebuddy.json +23 -19
- package/harness/manifests/java/codex.json +23 -19
- package/harness/manifests/java/cursor.json +23 -19
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -324,6 +324,13 @@ def empty_profile_skeleton(excluded: tuple[str, ...] | list[str]) -> dict[str, A
|
|
|
324
324
|
"knownPreexistingErrors": [],
|
|
325
325
|
"shellQuirks": [],
|
|
326
326
|
"fingerprint": {"mvnVersion": "", "nodeVersion": "", "pomHash": ""},
|
|
327
|
+
"testTracking": {
|
|
328
|
+
"source": "detected",
|
|
329
|
+
"mode": "force-track-touched",
|
|
330
|
+
"paths": [],
|
|
331
|
+
"staleTestPolicy": "safe-repair",
|
|
332
|
+
"forbidTemporaryExclusion": True,
|
|
333
|
+
},
|
|
327
334
|
}
|
|
328
335
|
|
|
329
336
|
|
|
@@ -357,6 +364,9 @@ def _merge_user_overrides(
|
|
|
357
364
|
profile["knownPreexistingErrors"] = list(existing["knownPreexistingErrors"])
|
|
358
365
|
if isinstance(existing.get("shellQuirks"), list):
|
|
359
366
|
profile["shellQuirks"] = list(existing["shellQuirks"])
|
|
367
|
+
test_tracking = existing.get("testTracking")
|
|
368
|
+
if isinstance(test_tracking, dict) and test_tracking.get("source") == "user":
|
|
369
|
+
profile["testTracking"] = dict(test_tracking)
|
|
360
370
|
# 兼容字段:用户在 v1 配置的 verificationInputs 不保留(无 provenance),
|
|
361
371
|
# 由 commands 重新派生。knownPreexistingErrors/shellQuirks 是人工标注,保留。
|
|
362
372
|
|
|
@@ -392,10 +402,28 @@ def detect(project: Path) -> dict[str, Any]:
|
|
|
392
402
|
reactor_modules = find_reactor_modules(project, excluded)
|
|
393
403
|
if reactor_modules:
|
|
394
404
|
profile["commands"] = _java_commands(reactor_modules, pom_hash)
|
|
405
|
+
profile["testTracking"]["paths"] = sorted(
|
|
406
|
+
"src/test/**" if module == "." else f"{module}/src/test/**"
|
|
407
|
+
for module in reactor_modules
|
|
408
|
+
)
|
|
395
409
|
elif project_type == "node":
|
|
396
410
|
node_cmds = _node_commands(project)
|
|
397
411
|
if node_cmds:
|
|
398
412
|
profile["commands"] = node_cmds
|
|
413
|
+
profile["testTracking"]["paths"] = [
|
|
414
|
+
pattern
|
|
415
|
+
for extension in ("js", "jsx", "ts", "tsx", "mjs", "cjs")
|
|
416
|
+
for pattern in (
|
|
417
|
+
f"apps/*/test/**/*.{extension}",
|
|
418
|
+
f"apps/*/tests/**/*.{extension}",
|
|
419
|
+
f"packages/*/test/**/*.{extension}",
|
|
420
|
+
f"packages/*/tests/**/*.{extension}",
|
|
421
|
+
f"test/**/*.{extension}",
|
|
422
|
+
f"tests/**/*.{extension}",
|
|
423
|
+
f"**/*.test.{extension}",
|
|
424
|
+
f"**/*.spec.{extension}",
|
|
425
|
+
)
|
|
426
|
+
]
|
|
399
427
|
|
|
400
428
|
_merge_user_overrides(profile, existing)
|
|
401
429
|
_derive_verification_inputs(profile)
|
|
@@ -719,7 +747,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
719
747
|
if args.command == "check":
|
|
720
748
|
result = check(args.project)
|
|
721
749
|
sys.stdout.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
|
722
|
-
return 0 if not result.get("stale") else
|
|
750
|
+
return 0 if result.get("ok") and not result.get("stale") else 1
|
|
723
751
|
if args.command == "validate":
|
|
724
752
|
profile = load_profile(args.project.resolve())
|
|
725
753
|
if profile is None:
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Record and force-stage only explicitly touched test files.
|
|
3
|
+
|
|
4
|
+
Python 3.10+ stdlib only. Git is always invoked with an argv list.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import contextlib
|
|
11
|
+
import fnmatch
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
SCHEMA_VERSION = 1
|
|
25
|
+
MODE = "force-track-touched"
|
|
26
|
+
MANIFEST_REL = Path("evidence") / "test-tracking.json"
|
|
27
|
+
PROFILE_REL = Path(".harness") / "config" / "build-profile.json"
|
|
28
|
+
REASONS = ("tdd-created", "stale-test-repair", "test-updated")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LockUnavailable(RuntimeError):
|
|
32
|
+
"""A compatible lock file is already held by another process."""
|
|
33
|
+
|
|
34
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
35
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
36
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
37
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _result(ok: bool, action: str, code: str, files: list[str], **extra: Any) -> dict[str, Any]:
|
|
41
|
+
return {"ok": ok, "action": action, "code": code, "files": files, **extra}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _write_json(path: Path, data: Any) -> None:
|
|
45
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
|
47
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
48
|
+
try:
|
|
49
|
+
tmp.write_text(text, encoding="utf-8", newline="\n")
|
|
50
|
+
os.replace(tmp, path)
|
|
51
|
+
except BaseException:
|
|
52
|
+
tmp.unlink(missing_ok=True)
|
|
53
|
+
raise
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_json(path: Path) -> Any:
|
|
57
|
+
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sha256(path: Path) -> str:
|
|
61
|
+
digest = hashlib.sha256()
|
|
62
|
+
with path.open("rb") as handle:
|
|
63
|
+
for chunk in iter(lambda: handle.read(65536), b""):
|
|
64
|
+
digest.update(chunk)
|
|
65
|
+
return "sha256:" + digest.hexdigest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _git(
|
|
69
|
+
project: Path,
|
|
70
|
+
*args: str,
|
|
71
|
+
index_file: Path | None = None,
|
|
72
|
+
) -> subprocess.CompletedProcess[str]:
|
|
73
|
+
env = os.environ.copy()
|
|
74
|
+
if index_file is not None:
|
|
75
|
+
env["GIT_INDEX_FILE"] = str(index_file)
|
|
76
|
+
return subprocess.run(
|
|
77
|
+
[
|
|
78
|
+
"git",
|
|
79
|
+
"--literal-pathspecs",
|
|
80
|
+
"-c",
|
|
81
|
+
"core.quotepath=false",
|
|
82
|
+
"-C",
|
|
83
|
+
str(project),
|
|
84
|
+
*args,
|
|
85
|
+
],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
encoding="utf-8",
|
|
89
|
+
errors="replace",
|
|
90
|
+
timeout=30,
|
|
91
|
+
check=False,
|
|
92
|
+
env=env,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_ignored(project: Path, rel: str) -> bool:
|
|
97
|
+
status = _git(
|
|
98
|
+
project,
|
|
99
|
+
"status",
|
|
100
|
+
"--ignored=matching",
|
|
101
|
+
"--untracked-files=all",
|
|
102
|
+
"--porcelain=v1",
|
|
103
|
+
"--",
|
|
104
|
+
rel,
|
|
105
|
+
)
|
|
106
|
+
return status.returncode == 0 and any(
|
|
107
|
+
line.startswith("!! ") for line in status.stdout.splitlines()
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _inside(path: Path, root: Path) -> bool:
|
|
112
|
+
try:
|
|
113
|
+
normalized_path = os.path.normcase(str(path))
|
|
114
|
+
normalized_root = os.path.normcase(str(root))
|
|
115
|
+
return os.path.commonpath((normalized_path, normalized_root)) == normalized_root
|
|
116
|
+
except ValueError:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@contextlib.contextmanager
|
|
121
|
+
def _exclusive_lock(path: Path, *, wait_seconds: float) -> Any:
|
|
122
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
deadline = time.monotonic() + wait_seconds
|
|
124
|
+
descriptor: int | None = None
|
|
125
|
+
while descriptor is None:
|
|
126
|
+
try:
|
|
127
|
+
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
128
|
+
except FileExistsError as exc:
|
|
129
|
+
if time.monotonic() >= deadline:
|
|
130
|
+
raise LockUnavailable(str(path)) from exc
|
|
131
|
+
time.sleep(0.01)
|
|
132
|
+
try:
|
|
133
|
+
os.write(descriptor, f"{os.getpid()}\n".encode("ascii"))
|
|
134
|
+
yield
|
|
135
|
+
finally:
|
|
136
|
+
os.close(descriptor)
|
|
137
|
+
path.unlink(missing_ok=True)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _change_dir(project: Path, change_dir: Path | str) -> Path | None:
|
|
141
|
+
candidate = Path(change_dir)
|
|
142
|
+
if not candidate.is_absolute():
|
|
143
|
+
candidate = project / candidate
|
|
144
|
+
resolved = candidate.resolve()
|
|
145
|
+
return resolved if _inside(resolved, project) else None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _manifest_path(project: Path, change_root: Path) -> Path | None:
|
|
149
|
+
evidence = change_root / MANIFEST_REL.parent
|
|
150
|
+
manifest = change_root / MANIFEST_REL
|
|
151
|
+
expected_evidence = evidence.absolute()
|
|
152
|
+
resolved_evidence = evidence.resolve()
|
|
153
|
+
if (
|
|
154
|
+
not _inside(change_root.resolve(), project)
|
|
155
|
+
or not _inside(resolved_evidence, project)
|
|
156
|
+
or os.path.normcase(str(resolved_evidence))
|
|
157
|
+
!= os.path.normcase(str(expected_evidence))
|
|
158
|
+
):
|
|
159
|
+
return None
|
|
160
|
+
return manifest
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _manifest_target_inside(project: Path, manifest: Path) -> bool:
|
|
164
|
+
resolved = manifest.resolve()
|
|
165
|
+
return _inside(resolved, project) and _inside(resolved, manifest.parent.absolute())
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _profile_config(project: Path) -> tuple[list[str], list[str]] | None:
|
|
169
|
+
profile_path = project / PROFILE_REL
|
|
170
|
+
if not profile_path.is_file():
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
profile = _read_json(profile_path)
|
|
174
|
+
except (OSError, json.JSONDecodeError):
|
|
175
|
+
return [], []
|
|
176
|
+
tracking = profile.get("testTracking") if isinstance(profile, dict) else None
|
|
177
|
+
paths = tracking.get("paths") if isinstance(tracking, dict) else None
|
|
178
|
+
if not isinstance(paths, list):
|
|
179
|
+
return [], []
|
|
180
|
+
excluded = profile.get("excludedRoots") if isinstance(profile, dict) else None
|
|
181
|
+
excluded_roots = (
|
|
182
|
+
[item.replace("\\", "/").strip("/") for item in excluded if isinstance(item, str) and item]
|
|
183
|
+
if isinstance(excluded, list)
|
|
184
|
+
else []
|
|
185
|
+
)
|
|
186
|
+
patterns = [
|
|
187
|
+
item.replace("\\", "/") for item in paths if isinstance(item, str) and item
|
|
188
|
+
]
|
|
189
|
+
return patterns, excluded_roots
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _matches_pattern(rel: str, pattern: str) -> bool:
|
|
193
|
+
# fnmatch does not give **/ its usual zero-or-more-directory meaning, so
|
|
194
|
+
# also try the form with every **/ removed.
|
|
195
|
+
if fnmatch.fnmatchcase(rel, pattern):
|
|
196
|
+
return True
|
|
197
|
+
compact = pattern
|
|
198
|
+
while "**/" in compact:
|
|
199
|
+
compact = compact.replace("**/", "", 1)
|
|
200
|
+
if fnmatch.fnmatchcase(rel, compact):
|
|
201
|
+
return True
|
|
202
|
+
if pattern.endswith("/**"):
|
|
203
|
+
prefix = pattern[:-3].rstrip("/")
|
|
204
|
+
return rel == prefix or rel.startswith(prefix + "/")
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _standard_test_path(rel: str) -> bool:
|
|
209
|
+
parts = rel.split("/")
|
|
210
|
+
if len(parts) >= 2 and parts[0] in ("test", "tests"):
|
|
211
|
+
return True
|
|
212
|
+
if any(parts[index:index + 2] == ["src", "test"] for index in range(len(parts) - 1)):
|
|
213
|
+
return True
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _allowed_test_path(project: Path, rel: str) -> bool:
|
|
218
|
+
config = _profile_config(project)
|
|
219
|
+
if config is None:
|
|
220
|
+
return _standard_test_path(rel)
|
|
221
|
+
patterns, excluded_roots = config
|
|
222
|
+
rel_parts = tuple(os.path.normcase(part) for part in rel.split("/"))
|
|
223
|
+
for excluded in excluded_roots:
|
|
224
|
+
excluded_parts = tuple(os.path.normcase(part) for part in excluded.split("/"))
|
|
225
|
+
if rel_parts[: len(excluded_parts)] == excluded_parts:
|
|
226
|
+
return False
|
|
227
|
+
return any(_matches_pattern(rel, pattern) for pattern in patterns)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _validate_file(project: Path, raw: str) -> tuple[Path | None, str | None, str | None]:
|
|
231
|
+
candidate = Path(raw)
|
|
232
|
+
if not candidate.is_absolute():
|
|
233
|
+
candidate = project / candidate
|
|
234
|
+
resolved = candidate.resolve()
|
|
235
|
+
if not _inside(resolved, project):
|
|
236
|
+
return None, None, "PATH_OUTSIDE_PROJECT"
|
|
237
|
+
if not resolved.exists():
|
|
238
|
+
return None, None, "FILE_NOT_FOUND"
|
|
239
|
+
if not resolved.is_file():
|
|
240
|
+
return None, None, "NOT_REGULAR_FILE"
|
|
241
|
+
rel = resolved.relative_to(project).as_posix()
|
|
242
|
+
if not _allowed_test_path(project, rel):
|
|
243
|
+
return None, rel, "TEST_PATH_NOT_ALLOWED"
|
|
244
|
+
return resolved, rel, None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _entry_shape_valid(item: Any) -> bool:
|
|
248
|
+
return (
|
|
249
|
+
isinstance(item, dict)
|
|
250
|
+
and isinstance(item.get("path"), str)
|
|
251
|
+
and bool(item["path"])
|
|
252
|
+
and isinstance(item.get("sha256"), str)
|
|
253
|
+
and re.fullmatch(r"sha256:[0-9a-f]{64}", item["sha256"]) is not None
|
|
254
|
+
and item.get("reason") in REASONS
|
|
255
|
+
and type(item.get("ignored")) is bool
|
|
256
|
+
and type(item.get("trackedBefore")) is bool
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _validate_existing_manifest(
|
|
261
|
+
project: Path,
|
|
262
|
+
manifest: Any,
|
|
263
|
+
*,
|
|
264
|
+
allow_hash_drift: set[str] | None = None,
|
|
265
|
+
require_files: bool,
|
|
266
|
+
) -> tuple[str | None, dict[str, dict[str, Any]]]:
|
|
267
|
+
if (
|
|
268
|
+
not isinstance(manifest, dict)
|
|
269
|
+
or manifest.get("schemaVersion") != SCHEMA_VERSION
|
|
270
|
+
or manifest.get("mode") != MODE
|
|
271
|
+
):
|
|
272
|
+
return "MANIFEST_INVALID", {}
|
|
273
|
+
if manifest.get("projectRoot") != str(project):
|
|
274
|
+
return "MANIFEST_PROJECT_MISMATCH", {}
|
|
275
|
+
entries = manifest.get("files")
|
|
276
|
+
if not isinstance(entries, list):
|
|
277
|
+
return "MANIFEST_INVALID", {}
|
|
278
|
+
if require_files and not entries:
|
|
279
|
+
return "EMPTY_MANIFEST", {}
|
|
280
|
+
|
|
281
|
+
allowed_drift = allow_hash_drift or set()
|
|
282
|
+
validated: dict[str, dict[str, Any]] = {}
|
|
283
|
+
for item in entries:
|
|
284
|
+
if not _entry_shape_valid(item):
|
|
285
|
+
return "MANIFEST_INVALID", {}
|
|
286
|
+
rel = item["path"]
|
|
287
|
+
if rel in validated:
|
|
288
|
+
return "MANIFEST_INVALID", {}
|
|
289
|
+
path, normalized, error = _validate_file(project, rel)
|
|
290
|
+
if error or normalized != rel or path is None:
|
|
291
|
+
return error or "MANIFEST_INVALID", {}
|
|
292
|
+
if item["sha256"] != _sha256(path) and rel not in allowed_drift:
|
|
293
|
+
return "HASH_DRIFT" if require_files else "MANIFEST_INVALID", {}
|
|
294
|
+
ignored_now = _is_ignored(project, rel)
|
|
295
|
+
tracked_now = _git(
|
|
296
|
+
project, "ls-files", "--error-unmatch", "--", rel
|
|
297
|
+
).returncode == 0
|
|
298
|
+
if item["ignored"] != ignored_now or item["trackedBefore"] != tracked_now:
|
|
299
|
+
return "MANIFEST_INVALID", {}
|
|
300
|
+
validated[rel] = dict(item)
|
|
301
|
+
return None, validated
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def record(
|
|
305
|
+
project: Path | str,
|
|
306
|
+
change_dir: Path | str,
|
|
307
|
+
files: list[str],
|
|
308
|
+
reason: str,
|
|
309
|
+
) -> dict[str, Any]:
|
|
310
|
+
action = "record"
|
|
311
|
+
project_root = Path(project).resolve()
|
|
312
|
+
if not files:
|
|
313
|
+
return _result(False, action, "EMPTY_FILES", [])
|
|
314
|
+
if reason not in REASONS:
|
|
315
|
+
return _result(False, action, "INVALID_REASON", [])
|
|
316
|
+
change_root = _change_dir(project_root, change_dir)
|
|
317
|
+
if change_root is None:
|
|
318
|
+
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
319
|
+
|
|
320
|
+
validated: list[tuple[Path, str]] = []
|
|
321
|
+
for raw in files:
|
|
322
|
+
path, rel, error = _validate_file(project_root, raw)
|
|
323
|
+
if error:
|
|
324
|
+
return _result(False, action, error, [rel or str(raw)])
|
|
325
|
+
assert path is not None and rel is not None
|
|
326
|
+
validated.append((path, rel))
|
|
327
|
+
|
|
328
|
+
manifest_path = _manifest_path(project_root, change_root)
|
|
329
|
+
if manifest_path is None:
|
|
330
|
+
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
331
|
+
lock_path = manifest_path.with_name(manifest_path.name + ".lock")
|
|
332
|
+
try:
|
|
333
|
+
with _exclusive_lock(lock_path, wait_seconds=5.0):
|
|
334
|
+
if not _manifest_target_inside(project_root, manifest_path):
|
|
335
|
+
return _result(
|
|
336
|
+
False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", []
|
|
337
|
+
)
|
|
338
|
+
existing_files: dict[str, dict[str, Any]] = {}
|
|
339
|
+
if manifest_path.is_file():
|
|
340
|
+
try:
|
|
341
|
+
existing = _read_json(manifest_path)
|
|
342
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
343
|
+
return _result(
|
|
344
|
+
False, action, "MANIFEST_INVALID", [], error=str(exc)
|
|
345
|
+
)
|
|
346
|
+
error, existing_files = _validate_existing_manifest(
|
|
347
|
+
project_root,
|
|
348
|
+
existing,
|
|
349
|
+
allow_hash_drift={rel for _, rel in validated},
|
|
350
|
+
require_files=False,
|
|
351
|
+
)
|
|
352
|
+
if error:
|
|
353
|
+
return _result(False, action, error, [])
|
|
354
|
+
|
|
355
|
+
for path, rel in validated:
|
|
356
|
+
ignored = _is_ignored(project_root, rel)
|
|
357
|
+
tracked = _git(
|
|
358
|
+
project_root, "ls-files", "--error-unmatch", "--", rel
|
|
359
|
+
).returncode == 0
|
|
360
|
+
existing_files[rel] = {
|
|
361
|
+
"path": rel,
|
|
362
|
+
"sha256": _sha256(path),
|
|
363
|
+
"reason": reason,
|
|
364
|
+
"ignored": ignored,
|
|
365
|
+
"trackedBefore": tracked,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
manifest = {
|
|
369
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
370
|
+
"mode": MODE,
|
|
371
|
+
"projectRoot": str(project_root),
|
|
372
|
+
"files": [existing_files[key] for key in sorted(existing_files)],
|
|
373
|
+
}
|
|
374
|
+
_write_json(manifest_path, manifest)
|
|
375
|
+
except LockUnavailable:
|
|
376
|
+
return _result(False, action, "MANIFEST_LOCKED", [])
|
|
377
|
+
return _result(True, action, "RECORDED", [rel for _, rel in validated], manifestPath=str(manifest_path))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _stage_locked(
|
|
381
|
+
project_root: Path, manifest_path: Path, index_path: Path
|
|
382
|
+
) -> dict[str, Any]:
|
|
383
|
+
action = "stage"
|
|
384
|
+
if not _manifest_target_inside(project_root, manifest_path):
|
|
385
|
+
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
386
|
+
try:
|
|
387
|
+
manifest = _read_json(manifest_path)
|
|
388
|
+
except FileNotFoundError:
|
|
389
|
+
return _result(False, action, "MANIFEST_MISSING", [])
|
|
390
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
391
|
+
return _result(False, action, "MANIFEST_INVALID", [], error=str(exc))
|
|
392
|
+
error, entries = _validate_existing_manifest(
|
|
393
|
+
project_root, manifest, require_files=True
|
|
394
|
+
)
|
|
395
|
+
if error:
|
|
396
|
+
return _result(False, action, error, [])
|
|
397
|
+
rels = list(entries)
|
|
398
|
+
|
|
399
|
+
before_cached_result = _git(project_root, "diff", "--cached", "--name-only")
|
|
400
|
+
if before_cached_result.returncode != 0:
|
|
401
|
+
return _result(False, action, "CACHED_DIFF_FAILED", rels)
|
|
402
|
+
before_cached = set(before_cached_result.stdout.splitlines())
|
|
403
|
+
|
|
404
|
+
handle, temp_name = tempfile.mkstemp(
|
|
405
|
+
prefix=f".{index_path.name}.test-guard.", dir=index_path.parent
|
|
406
|
+
)
|
|
407
|
+
os.close(handle)
|
|
408
|
+
temp_index = Path(temp_name)
|
|
409
|
+
temp_index.unlink(missing_ok=True)
|
|
410
|
+
try:
|
|
411
|
+
if index_path.is_file():
|
|
412
|
+
shutil.copy2(index_path, temp_index)
|
|
413
|
+
added = _git(project_root, "add", "-f", "--", *rels, index_file=temp_index)
|
|
414
|
+
if added.returncode != 0:
|
|
415
|
+
return _result(
|
|
416
|
+
False,
|
|
417
|
+
action,
|
|
418
|
+
"GIT_ADD_FAILED",
|
|
419
|
+
rels,
|
|
420
|
+
error=added.stderr.strip(),
|
|
421
|
+
)
|
|
422
|
+
cached = _git(
|
|
423
|
+
project_root,
|
|
424
|
+
"diff",
|
|
425
|
+
"--cached",
|
|
426
|
+
"--name-only",
|
|
427
|
+
index_file=temp_index,
|
|
428
|
+
)
|
|
429
|
+
if cached.returncode != 0:
|
|
430
|
+
return _result(False, action, "CACHED_DIFF_FAILED", rels)
|
|
431
|
+
after_cached = set(cached.stdout.splitlines())
|
|
432
|
+
missing = [rel for rel in rels if rel not in after_cached]
|
|
433
|
+
unexpected = sorted((after_cached - before_cached) - set(rels))
|
|
434
|
+
if missing or unexpected:
|
|
435
|
+
return _result(
|
|
436
|
+
False,
|
|
437
|
+
action,
|
|
438
|
+
"CACHED_DIFF_MISMATCH",
|
|
439
|
+
missing or unexpected,
|
|
440
|
+
)
|
|
441
|
+
os.replace(temp_index, index_path)
|
|
442
|
+
return _result(True, action, "STAGED", rels)
|
|
443
|
+
finally:
|
|
444
|
+
temp_index.unlink(missing_ok=True)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def stage(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
448
|
+
action = "stage"
|
|
449
|
+
project_root = Path(project).resolve()
|
|
450
|
+
change_root = _change_dir(project_root, change_dir)
|
|
451
|
+
if change_root is None:
|
|
452
|
+
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
453
|
+
manifest_path = _manifest_path(project_root, change_root)
|
|
454
|
+
if manifest_path is None:
|
|
455
|
+
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
456
|
+
|
|
457
|
+
index_result = _git(project_root, "rev-parse", "--git-path", "index")
|
|
458
|
+
if index_result.returncode != 0 or not index_result.stdout.strip():
|
|
459
|
+
return _result(False, action, "GIT_INDEX_NOT_FOUND", [])
|
|
460
|
+
index_path = Path(index_result.stdout.strip())
|
|
461
|
+
if not index_path.is_absolute():
|
|
462
|
+
index_path = (project_root / index_path).resolve()
|
|
463
|
+
index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
464
|
+
|
|
465
|
+
index_lock = index_path.with_name(index_path.name + ".lock")
|
|
466
|
+
try:
|
|
467
|
+
with _exclusive_lock(index_lock, wait_seconds=0.0):
|
|
468
|
+
manifest_lock = manifest_path.with_name(manifest_path.name + ".lock")
|
|
469
|
+
try:
|
|
470
|
+
with _exclusive_lock(manifest_lock, wait_seconds=0.0):
|
|
471
|
+
return _stage_locked(project_root, manifest_path, index_path)
|
|
472
|
+
except LockUnavailable:
|
|
473
|
+
return _result(False, action, "MANIFEST_LOCKED", [])
|
|
474
|
+
except LockUnavailable:
|
|
475
|
+
return _result(False, action, "INDEX_LOCKED", [])
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def main(argv: list[str] | None = None) -> int:
|
|
479
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
480
|
+
sub = parser.add_subparsers(dest="action", required=True)
|
|
481
|
+
record_parser = sub.add_parser("record")
|
|
482
|
+
record_parser.add_argument("--project", required=True)
|
|
483
|
+
record_parser.add_argument("--change-dir", required=True)
|
|
484
|
+
record_parser.add_argument("--files", required=True)
|
|
485
|
+
record_parser.add_argument("--reason", required=True, choices=REASONS)
|
|
486
|
+
record_parser.add_argument("--json", action="store_true")
|
|
487
|
+
stage_parser = sub.add_parser("stage")
|
|
488
|
+
stage_parser.add_argument("--project", required=True)
|
|
489
|
+
stage_parser.add_argument("--change-dir", required=True)
|
|
490
|
+
stage_parser.add_argument("--json", action="store_true")
|
|
491
|
+
args = parser.parse_args(argv)
|
|
492
|
+
if args.action == "record":
|
|
493
|
+
files = [item.strip() for item in args.files.split(",") if item.strip()]
|
|
494
|
+
result = record(args.project, args.change_dir, files, args.reason)
|
|
495
|
+
else:
|
|
496
|
+
result = stage(args.project, args.change_dir)
|
|
497
|
+
print(json.dumps(result, ensure_ascii=False, indent=2 if args.json else None))
|
|
498
|
+
return 0 if result["ok"] else 1
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
raise SystemExit(main())
|
|
@@ -3,7 +3,7 @@ name: harness-archive
|
|
|
3
3
|
description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
|
|
4
4
|
.harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-codebase-map
|
|
|
3
3
|
description: 分析现有代码库并生成 .harness/codebase/map/
|
|
4
4
|
下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-codebase-map — 代码库地图
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-knowledge-ingest
|
|
|
3
3
|
description: 从 .harness/archive 归档整理、同步和维护项目知识索引。适用场景:ingest knowledge、sync
|
|
4
4
|
knowledge、rebuild knowledge index、promote knowledge、确认知识条目、检查知识库是否过期。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-knowledge-ingest
|
|
8
8
|
|
|
9
9
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|
|
@@ -3,7 +3,7 @@ name: harness-knowledge-query
|
|
|
3
3
|
description: 根据新需求、设计任务、代码修改请求或问题排查,在规划和编码前查询 .harness/knowledge
|
|
4
4
|
历史上下文。适用场景:query knowledge、查找历史需求、根据归档理解需求、继续之前类似开发、生成需求上下文包。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-knowledge-query
|
|
8
8
|
|
|
9
9
|
根据用户的新需求或排查问题,从项目本地 `.harness/knowledge` 中检索历史需求、决策、实现、风险和测试证据,并生成 AI 可读的 context pack。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-plan
|
|
3
3
|
description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。使用场景:需求分析、feature plan、技术方案设计、实现方案规划
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=codebuddy; do not edit -->
|
|
6
6
|
# harness-plan — 需求规划
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-review
|
|
|
3
3
|
description: 6维度代码审查(架构/安全/规范/兼容/测试/性能),对照项目规则(见
|
|
4
4
|
.harness/context-index.json)和测试场景表,在隔离上下文运行。使用场景:代码审查、提交前检查、合并评审
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-review — 代码审查
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-run
|
|
3
3
|
description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。使用场景:开始编码、实现功能、写代码、TDD 编码
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=codebuddy; do not edit -->
|
|
6
6
|
# harness-run — 需求编码
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -56,7 +56,7 @@ description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编
|
|
|
56
56
|
0. 加载上下文(change-name、spec/plan/detail/scenarios/ledger/run-task-status/worktree;`--fixback` 读 fixback)→ append `phase.start`
|
|
57
57
|
0.5. **测试基础设施探测**(先写 `CHECKING`,四项证据齐备后再结论)→ `reference.md` Step 0.5
|
|
58
58
|
1. **变更簇 TDD** — `protocols.md` `run-tdd-protocol`;批量 RED/GREEN;按需 `change-cluster-review-protocol`(高风险 + reviewer 预检可用)
|
|
59
|
-
2. 构建验证 + 写 ledger
|
|
59
|
+
2. 构建验证 + 写 ledger(`harness_ledger.py diff-hash --change-dir` 纳入 ignored tests,`reference.md` Step 2c)
|
|
60
60
|
3. **场景覆盖检查**(场景表映射,禁止用用例数冒充场景数)
|
|
61
61
|
4. **关门检查**(10 项)+ 计划状态持久化
|
|
62
62
|
|
|
@@ -78,12 +78,24 @@ description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编
|
|
|
78
78
|
| **变更簇 TDD** | 一簇一次 RED/GREEN;低价值项豁免;新分支必须 RED |
|
|
79
79
|
| **RED/GREEN** | RED 须有效;静态验证 ≠ 测试通过;greenfield 大重写豁免见 reference |
|
|
80
80
|
| **Mapper/DB** | 纯 Mock 不得宣称 DB 验证通过;迁移脚本**永不自动执行** |
|
|
81
|
-
| **探测/ledger** | 基础设施先探测;每次构建/测试写 ledger
|
|
81
|
+
| **探测/ledger** | 基础设施先探测;每次构建/测试写 ledger;用 canonical `diff-hash --change-dir` |
|
|
82
82
|
| **预存变更** | 保留 → baseline 隔离;存在则最终 ≥ 🟡WARN |
|
|
83
83
|
| **关门/状态** | 10 项关门检查;持久化 run-task-status;P0 静态-only 不得建议 submit |
|
|
84
84
|
| **Worktree** | `requested=true` 时代码只写 worktree |
|
|
85
85
|
| **PowerShell** | 所有 git/构建经 `powershell.exe -NoProfile -Command` |
|
|
86
86
|
|
|
87
|
+
### 陈旧测试安全修复与精确跟踪
|
|
88
|
+
|
|
89
|
+
测试编译或 RED/GREEN 失败时,先区分当前实现缺陷、测试基础设施故障与陈旧测试。只有同时满足以下条件才允许自动修复陈旧测试:当前生产代码、已批准计划或可验证的历史变更能唯一确定新契约;修改范围仅限测试文件;修复后会立即重跑该测试及本变更目标测试。符合时以 `stale-test-repair` 记录:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
python <skills-root>/scripts/harness_test_guard.py record --project . --change-dir ".harness/changes/<change-name>" --files "<精确测试文件路径,逗号分隔>" --reason stale-test-repair --json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
新建或正常更新测试分别使用 `tdd-created` / `test-updated`。若预期行为存在业务歧义,停止测试修复并记录 `BLOCKED_PREEXISTING`,不得猜测新断言。
|
|
96
|
+
|
|
97
|
+
**禁止临时排除测试**:不得将测试改名为 `.bak`、移出测试目录、删除、添加 `@Disabled`/`@Ignore`、修改 Surefire/Gradle exclude 或跳过测试来制造绿色结果;也不得仅为满足陈旧测试而修改生产代码。`.gitignore` 中的测试只能通过 manifest 的精确路径闭环处理,禁止全局放宽 ignore。
|
|
98
|
+
|
|
87
99
|
## Output Format
|
|
88
100
|
|
|
89
101
|
变更文件表 + 构建/测试证据 + 场景覆盖摘要 + 最终状态(✅OK / 🟡WARN / ❌FAIL)。→ `reference.md`
|