@hunter-harness/workflow-harness 0.2.33 → 0.2.34
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/README.md +25 -25
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +8 -0
- 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-ingest/scripts/harness_knowledge.py +389 -117
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +4 -4
- package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +2 -1
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +404 -12
- package/harness/bundles/general/claude-code/scripts/harness_context.py +717 -0
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +1 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +77 -9
- package/harness/bundles/general/claude-code/scripts/harness_phase.py +1 -0
- package/harness/bundles/general/claude-code/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +272 -15
- package/harness/bundles/general/claude-code/scripts/harness_report_model.py +420 -0
- package/harness/bundles/general/claude-code/scripts/harness_review.py +16 -1
- package/harness/bundles/general/claude-code/scripts/harness_verification.py +114 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +8 -0
- 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-ingest/scripts/harness_knowledge.py +389 -117
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +4 -4
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -1
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +404 -12
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +717 -0
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +1 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +77 -9
- package/harness/bundles/general/codebuddy/scripts/harness_phase.py +1 -0
- package/harness/bundles/general/codebuddy/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +272 -15
- package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +420 -0
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +16 -1
- package/harness/bundles/general/codebuddy/scripts/harness_verification.py +114 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +8 -0
- 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-ingest/scripts/harness_knowledge.py +389 -117
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-test/checklist.md +1 -0
- package/harness/bundles/general/codex/harness-test/reference.md +2 -1
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/general/codex/scripts/harness_archive.py +404 -12
- package/harness/bundles/general/codex/scripts/harness_context.py +717 -0
- package/harness/bundles/general/codex/scripts/harness_gate.py +1 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +77 -9
- package/harness/bundles/general/codex/scripts/harness_phase.py +1 -0
- package/harness/bundles/general/codex/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/codex/scripts/harness_profile.py +272 -15
- package/harness/bundles/general/codex/scripts/harness_report_model.py +420 -0
- package/harness/bundles/general/codex/scripts/harness_review.py +16 -1
- package/harness/bundles/general/codex/scripts/harness_verification.py +114 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +8 -0
- 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-ingest/scripts/harness_knowledge.py +389 -117
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-test/checklist.md +1 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +2 -1
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/general/cursor/scripts/harness_archive.py +404 -12
- package/harness/bundles/general/cursor/scripts/harness_context.py +717 -0
- package/harness/bundles/general/cursor/scripts/harness_gate.py +1 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +77 -9
- package/harness/bundles/general/cursor/scripts/harness_phase.py +1 -0
- package/harness/bundles/general/cursor/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/cursor/scripts/harness_profile.py +272 -15
- package/harness/bundles/general/cursor/scripts/harness_report_model.py +420 -0
- package/harness/bundles/general/cursor/scripts/harness_review.py +16 -1
- package/harness/bundles/general/cursor/scripts/harness_verification.py +114 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -0
- 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-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +8 -0
- 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-ingest/scripts/harness_knowledge.py +389 -117
- 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 +3 -3
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -0
- package/harness/bundles/java/claude-code/harness-test/reference.md +2 -0
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +404 -12
- package/harness/bundles/java/claude-code/scripts/harness_context.py +717 -0
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +1 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +77 -9
- package/harness/bundles/java/claude-code/scripts/harness_phase.py +1 -0
- package/harness/bundles/java/claude-code/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +272 -15
- package/harness/bundles/java/claude-code/scripts/harness_report_model.py +420 -0
- package/harness/bundles/java/claude-code/scripts/harness_review.py +16 -1
- package/harness/bundles/java/claude-code/scripts/harness_verification.py +114 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -0
- 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-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +8 -0
- 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-ingest/scripts/harness_knowledge.py +389 -117
- 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 +3 -3
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -0
- package/harness/bundles/java/codebuddy/harness-test/reference.md +2 -0
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +404 -12
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +717 -0
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +1 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +77 -9
- package/harness/bundles/java/codebuddy/scripts/harness_phase.py +1 -0
- package/harness/bundles/java/codebuddy/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +272 -15
- package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +420 -0
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +16 -1
- package/harness/bundles/java/codebuddy/scripts/harness_verification.py +114 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -0
- 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-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +8 -0
- 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-ingest/scripts/harness_knowledge.py +389 -117
- 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 +3 -3
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-test/checklist.md +1 -0
- package/harness/bundles/java/codex/harness-test/reference.md +2 -0
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/java/codex/scripts/harness_archive.py +404 -12
- package/harness/bundles/java/codex/scripts/harness_context.py +717 -0
- package/harness/bundles/java/codex/scripts/harness_gate.py +1 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +77 -9
- package/harness/bundles/java/codex/scripts/harness_phase.py +1 -0
- package/harness/bundles/java/codex/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/codex/scripts/harness_profile.py +272 -15
- package/harness/bundles/java/codex/scripts/harness_report_model.py +420 -0
- package/harness/bundles/java/codex/scripts/harness_review.py +16 -1
- package/harness/bundles/java/codex/scripts/harness_verification.py +114 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -0
- 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-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +8 -0
- 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-ingest/scripts/harness_knowledge.py +389 -117
- 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 +3 -3
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-test/checklist.md +1 -0
- package/harness/bundles/java/cursor/harness-test/reference.md +2 -0
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/java/cursor/scripts/harness_archive.py +404 -12
- package/harness/bundles/java/cursor/scripts/harness_context.py +717 -0
- package/harness/bundles/java/cursor/scripts/harness_gate.py +1 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +77 -9
- package/harness/bundles/java/cursor/scripts/harness_phase.py +1 -0
- package/harness/bundles/java/cursor/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/cursor/scripts/harness_profile.py +272 -15
- package/harness/bundles/java/cursor/scripts/harness_report_model.py +420 -0
- package/harness/bundles/java/cursor/scripts/harness_review.py +16 -1
- package/harness/bundles/java/cursor/scripts/harness_verification.py +114 -0
- package/harness/manifests/general/claude-code.json +42 -26
- package/harness/manifests/general/codebuddy.json +42 -26
- package/harness/manifests/general/codex.json +42 -26
- package/harness/manifests/general/cursor.json +42 -26
- package/harness/manifests/java/claude-code.json +44 -28
- package/harness/manifests/java/codebuddy.json +44 -28
- package/harness/manifests/java/codex.json +44 -28
- package/harness/manifests/java/cursor.json +44 -28
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -346,6 +346,26 @@ def run_git(project: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
|
346
346
|
return subprocess.CompletedProcess(args, 1, "", str(exc))
|
|
347
347
|
|
|
348
348
|
|
|
349
|
+
def run_git_with_input(
|
|
350
|
+
project: Path,
|
|
351
|
+
args: list[str],
|
|
352
|
+
input_text: str,
|
|
353
|
+
) -> subprocess.CompletedProcess[str]:
|
|
354
|
+
global _GIT_SUBPROCESS_COUNT
|
|
355
|
+
_GIT_SUBPROCESS_COUNT += 1
|
|
356
|
+
try:
|
|
357
|
+
return subprocess.run(
|
|
358
|
+
["git", "-C", str(project), *args],
|
|
359
|
+
input=input_text,
|
|
360
|
+
text=True,
|
|
361
|
+
stdout=subprocess.PIPE,
|
|
362
|
+
stderr=subprocess.PIPE,
|
|
363
|
+
timeout=20,
|
|
364
|
+
)
|
|
365
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
366
|
+
return subprocess.CompletedProcess(args, 1, "", str(exc))
|
|
367
|
+
|
|
368
|
+
|
|
349
369
|
def archive_name(summary_path: Path) -> str:
|
|
350
370
|
parts = summary_path.parts
|
|
351
371
|
try:
|
|
@@ -592,17 +612,12 @@ def discover_archive_summary_paths(project: Path) -> list[Path]:
|
|
|
592
612
|
return selected
|
|
593
613
|
|
|
594
614
|
|
|
595
|
-
def
|
|
596
|
-
|
|
615
|
+
def _publication_status_from_resolution(
|
|
616
|
+
project: Path,
|
|
617
|
+
archive_dir: Path,
|
|
618
|
+
resolution: dict[str, Any],
|
|
619
|
+
) -> dict[str, Any]:
|
|
597
620
|
result: dict[str, Any] = {"status": "missing", "allowed": False, "reasons": []}
|
|
598
|
-
archive_root = (Path(project) / ".harness" / "archive").resolve()
|
|
599
|
-
archive_dir = (Path(project) / archive_rel).resolve()
|
|
600
|
-
if not _path_is_within(archive_dir, archive_root) or archive_dir.parent != archive_root:
|
|
601
|
-
result["reasons"].append("archive path escapes archive root")
|
|
602
|
-
return result
|
|
603
|
-
|
|
604
|
-
original_path = archive_dir / "reports" / "final" / "summary-data.json"
|
|
605
|
-
resolution = resolve_archive_summary(original_path)
|
|
606
621
|
if resolution.get("version"):
|
|
607
622
|
result["authoritativeVersion"] = resolution["version"]
|
|
608
623
|
|
|
@@ -615,11 +630,13 @@ def archive_publication_status(project: Path, archive_rel: str) -> dict[str, Any
|
|
|
615
630
|
result["status"] = "degraded"
|
|
616
631
|
result["reasons"].append("resolved summary escapes archive root")
|
|
617
632
|
return result
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
633
|
+
summary_hash = str(resolution.get("summarySha256") or "")
|
|
634
|
+
if not summary_hash:
|
|
635
|
+
try:
|
|
636
|
+
summary_hash = sha256_file(summary_path)
|
|
637
|
+
except OSError:
|
|
638
|
+
result["reasons"].append("summary-data.json unreadable")
|
|
639
|
+
return result
|
|
623
640
|
result["summaryData"] = rel_to_project(project, summary_path)
|
|
624
641
|
result["summarySha256"] = summary_hash
|
|
625
642
|
if not resolution.get("allowed"):
|
|
@@ -665,6 +682,20 @@ def archive_publication_status(project: Path, archive_rel: str) -> dict[str, Any
|
|
|
665
682
|
return result
|
|
666
683
|
|
|
667
684
|
|
|
685
|
+
def archive_publication_status(project: Path, archive_rel: str) -> dict[str, Any]:
|
|
686
|
+
"""Knowledge publication gate for one archive (API-006/RET-40)."""
|
|
687
|
+
result: dict[str, Any] = {"status": "missing", "allowed": False, "reasons": []}
|
|
688
|
+
archive_root = (Path(project) / ".harness" / "archive").resolve()
|
|
689
|
+
archive_dir = (Path(project) / archive_rel).resolve()
|
|
690
|
+
if not _path_is_within(archive_dir, archive_root) or archive_dir.parent != archive_root:
|
|
691
|
+
result["reasons"].append("archive path escapes archive root")
|
|
692
|
+
return result
|
|
693
|
+
resolution = resolve_archive_summary(
|
|
694
|
+
archive_dir / "reports" / "final" / "summary-data.json"
|
|
695
|
+
)
|
|
696
|
+
return _publication_status_from_resolution(project, archive_dir, resolution)
|
|
697
|
+
|
|
698
|
+
|
|
668
699
|
def archive_publication_statuses(project: Path) -> dict[str, dict[str, Any]]:
|
|
669
700
|
"""Resolve publication state for every safe immediate archive directory."""
|
|
670
701
|
archive_root = Path(project) / ".harness" / "archive"
|
|
@@ -733,6 +764,74 @@ def archive_summary_records(project: Path, summary_paths: list[Path]) -> list[di
|
|
|
733
764
|
return records
|
|
734
765
|
|
|
735
766
|
|
|
767
|
+
def discover_archive_inputs(
|
|
768
|
+
project: Path,
|
|
769
|
+
) -> tuple[list[Path], list[dict[str, Any]], dict[str, dict[str, Any]]]:
|
|
770
|
+
"""Resolve, hash and publication-check every archive in a single pass."""
|
|
771
|
+
archive_root = Path(project) / ".harness" / "archive"
|
|
772
|
+
try:
|
|
773
|
+
resolved_archive_root = archive_root.resolve(strict=True)
|
|
774
|
+
children = sorted(archive_root.iterdir())
|
|
775
|
+
except OSError:
|
|
776
|
+
return [], [], {}
|
|
777
|
+
|
|
778
|
+
summary_paths: list[Path] = []
|
|
779
|
+
records: list[dict[str, Any]] = []
|
|
780
|
+
publications: dict[str, dict[str, Any]] = {}
|
|
781
|
+
for child in children:
|
|
782
|
+
try:
|
|
783
|
+
archive_dir = child.resolve(strict=True)
|
|
784
|
+
except OSError:
|
|
785
|
+
continue
|
|
786
|
+
if (
|
|
787
|
+
not archive_dir.is_dir()
|
|
788
|
+
or not _path_is_within(archive_dir, resolved_archive_root)
|
|
789
|
+
or archive_dir.parent != resolved_archive_root
|
|
790
|
+
):
|
|
791
|
+
continue
|
|
792
|
+
archive_rel = rel_to_project(project, archive_dir)
|
|
793
|
+
resolution = resolve_archive_summary(
|
|
794
|
+
archive_dir / "reports" / "final" / "summary-data.json"
|
|
795
|
+
)
|
|
796
|
+
publication = _publication_status_from_resolution(
|
|
797
|
+
project, archive_dir, resolution
|
|
798
|
+
)
|
|
799
|
+
publications[archive_rel] = publication
|
|
800
|
+
original_state = (resolution.get("fingerprint") or {}).get("original") or {}
|
|
801
|
+
if original_state.get("state") != "file":
|
|
802
|
+
continue
|
|
803
|
+
try:
|
|
804
|
+
selected_path = Path(resolution["summaryPath"]).resolve(strict=True)
|
|
805
|
+
except (KeyError, OSError):
|
|
806
|
+
continue
|
|
807
|
+
if (
|
|
808
|
+
not _path_is_within(selected_path, archive_dir)
|
|
809
|
+
or not selected_path.is_file()
|
|
810
|
+
or not publication.get("summarySha256")
|
|
811
|
+
):
|
|
812
|
+
continue
|
|
813
|
+
stat = selected_path.stat()
|
|
814
|
+
summary_paths.append(selected_path)
|
|
815
|
+
records.append(
|
|
816
|
+
{
|
|
817
|
+
"archive": archive_rel,
|
|
818
|
+
"summaryData": rel_to_project(project, selected_path),
|
|
819
|
+
"summarySha256": publication["summarySha256"],
|
|
820
|
+
"mtime": dt.datetime.fromtimestamp(
|
|
821
|
+
stat.st_mtime, dt.timezone.utc
|
|
822
|
+
).isoformat(timespec="seconds"),
|
|
823
|
+
"_authority": {
|
|
824
|
+
"status": resolution["status"],
|
|
825
|
+
"allowed": resolution["allowed"],
|
|
826
|
+
"version": resolution["version"],
|
|
827
|
+
"reasons": list(resolution["reasons"]),
|
|
828
|
+
"fingerprint": resolution["fingerprint"],
|
|
829
|
+
},
|
|
830
|
+
}
|
|
831
|
+
)
|
|
832
|
+
return summary_paths, records, publications
|
|
833
|
+
|
|
834
|
+
|
|
736
835
|
def make_entry(
|
|
737
836
|
*,
|
|
738
837
|
project: Path,
|
|
@@ -756,48 +855,9 @@ def make_entry(
|
|
|
756
855
|
status = "candidate"
|
|
757
856
|
stale_reasons: list[str] = []
|
|
758
857
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
"source commit missing from local git history: " + final_commit[:12]
|
|
763
|
-
)
|
|
764
|
-
elif source_files:
|
|
765
|
-
diff_key = (
|
|
766
|
-
str(project.resolve()),
|
|
767
|
-
final_commit,
|
|
768
|
-
tuple(sorted(source_files)),
|
|
769
|
-
)
|
|
770
|
-
changed = _GIT_DIFF_CACHE.get(diff_key)
|
|
771
|
-
if changed is None and diff_key not in _GIT_DIFF_CACHE:
|
|
772
|
-
diff = run_git(
|
|
773
|
-
project,
|
|
774
|
-
[
|
|
775
|
-
"diff",
|
|
776
|
-
"--name-only",
|
|
777
|
-
f"{final_commit}..HEAD",
|
|
778
|
-
"--",
|
|
779
|
-
*sorted(source_files),
|
|
780
|
-
],
|
|
781
|
-
)
|
|
782
|
-
changed = (
|
|
783
|
-
[
|
|
784
|
-
line.strip()
|
|
785
|
-
for line in diff.stdout.splitlines()
|
|
786
|
-
if line.strip()
|
|
787
|
-
]
|
|
788
|
-
if diff.returncode == 0
|
|
789
|
-
else None
|
|
790
|
-
)
|
|
791
|
-
_GIT_DIFF_CACHE[diff_key] = changed
|
|
792
|
-
if changed is not None:
|
|
793
|
-
if changed:
|
|
794
|
-
stale_reasons.append(
|
|
795
|
-
"source files changed after source commit: " + ", ".join(changed[:8])
|
|
796
|
-
)
|
|
797
|
-
else:
|
|
798
|
-
stale_reasons.append(
|
|
799
|
-
"source commit could not be compared with current HEAD"
|
|
800
|
-
)
|
|
858
|
+
# Git freshness is evaluated for the full entry batch after extraction.
|
|
859
|
+
# Keeping the candidate status here preserves extraction as a pure operation
|
|
860
|
+
# and gives the batch evaluator one opportunity to inspect each source commit.
|
|
801
861
|
|
|
802
862
|
if stale_reasons:
|
|
803
863
|
status = "stale"
|
|
@@ -859,6 +919,114 @@ def make_entry(
|
|
|
859
919
|
}
|
|
860
920
|
|
|
861
921
|
|
|
922
|
+
def apply_batch_git_freshness(
|
|
923
|
+
project: Path,
|
|
924
|
+
entries: list[dict[str, Any]],
|
|
925
|
+
) -> dict[str, int]:
|
|
926
|
+
"""Refresh generated-entry Git freshness using bounded, grouped probes."""
|
|
927
|
+
stats = {
|
|
928
|
+
"gitCommitBatches": 0,
|
|
929
|
+
"gitDiffBatches": 0,
|
|
930
|
+
"gitEntriesChecked": 0,
|
|
931
|
+
}
|
|
932
|
+
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
933
|
+
for entry in entries:
|
|
934
|
+
commit = str(entry.get("source", {}).get("sourceCommit") or "").strip()
|
|
935
|
+
if commit:
|
|
936
|
+
grouped.setdefault(commit, []).append(entry)
|
|
937
|
+
if not grouped or not is_git_repo(project):
|
|
938
|
+
return stats
|
|
939
|
+
|
|
940
|
+
commits = sorted(grouped)
|
|
941
|
+
batch = run_git_with_input(
|
|
942
|
+
project,
|
|
943
|
+
["cat-file", "--batch-check=%(objectname) %(objecttype)"],
|
|
944
|
+
"".join(f"{commit}^{{commit}}\n" for commit in commits),
|
|
945
|
+
)
|
|
946
|
+
stats["gitCommitBatches"] = 1
|
|
947
|
+
commit_exists: dict[str, bool] = {}
|
|
948
|
+
output_lines = batch.stdout.splitlines() if batch.returncode == 0 else []
|
|
949
|
+
for index, commit in enumerate(commits):
|
|
950
|
+
line = output_lines[index] if index < len(output_lines) else ""
|
|
951
|
+
commit_exists[commit] = bool(line and not line.endswith(" missing") and line.endswith(" commit"))
|
|
952
|
+
_GIT_COMMIT_CACHE[(str(project.resolve()), commit)] = commit_exists[commit]
|
|
953
|
+
|
|
954
|
+
generated_prefixes = (
|
|
955
|
+
"source commit missing from local git history:",
|
|
956
|
+
"source files changed after source commit:",
|
|
957
|
+
"source commit could not be compared with current HEAD",
|
|
958
|
+
)
|
|
959
|
+
checked_at = now_iso()
|
|
960
|
+
for commit in commits:
|
|
961
|
+
commit_entries = grouped[commit]
|
|
962
|
+
stats["gitEntriesChecked"] += len(commit_entries)
|
|
963
|
+
union_files = sorted(
|
|
964
|
+
{
|
|
965
|
+
str(path).replace("\\", "/")
|
|
966
|
+
for entry in commit_entries
|
|
967
|
+
for path in entry.get("scope", {}).get("sourceFiles", [])
|
|
968
|
+
if str(path).strip()
|
|
969
|
+
}
|
|
970
|
+
)
|
|
971
|
+
changed: set[str] | None = set()
|
|
972
|
+
if commit_exists.get(commit) and union_files:
|
|
973
|
+
result = run_git(
|
|
974
|
+
project,
|
|
975
|
+
["diff", "--name-only", f"{commit}..HEAD", "--", *union_files],
|
|
976
|
+
)
|
|
977
|
+
stats["gitDiffBatches"] += 1
|
|
978
|
+
changed = (
|
|
979
|
+
{
|
|
980
|
+
line.strip().replace("\\", "/")
|
|
981
|
+
for line in result.stdout.splitlines()
|
|
982
|
+
if line.strip()
|
|
983
|
+
}
|
|
984
|
+
if result.returncode == 0
|
|
985
|
+
else None
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
for entry in commit_entries:
|
|
989
|
+
lifecycle = entry.setdefault("lifecycle", {})
|
|
990
|
+
previous_reasons = [
|
|
991
|
+
str(reason)
|
|
992
|
+
for reason in lifecycle.get("staleReasons", [])
|
|
993
|
+
if str(reason)
|
|
994
|
+
]
|
|
995
|
+
had_generated_reason = any(
|
|
996
|
+
reason.startswith(generated_prefixes) for reason in previous_reasons
|
|
997
|
+
)
|
|
998
|
+
reasons = [
|
|
999
|
+
reason
|
|
1000
|
+
for reason in previous_reasons
|
|
1001
|
+
if not reason.startswith(generated_prefixes)
|
|
1002
|
+
]
|
|
1003
|
+
entry_files = {
|
|
1004
|
+
str(path).replace("\\", "/")
|
|
1005
|
+
for path in entry.get("scope", {}).get("sourceFiles", [])
|
|
1006
|
+
if str(path).strip()
|
|
1007
|
+
}
|
|
1008
|
+
if not commit_exists.get(commit):
|
|
1009
|
+
reasons.append(
|
|
1010
|
+
"source commit missing from local git history: " + commit[:12]
|
|
1011
|
+
)
|
|
1012
|
+
elif changed is None:
|
|
1013
|
+
reasons.append("source commit could not be compared with current HEAD")
|
|
1014
|
+
else:
|
|
1015
|
+
changed_for_entry = sorted(changed.intersection(entry_files))
|
|
1016
|
+
if changed_for_entry:
|
|
1017
|
+
reasons.append(
|
|
1018
|
+
"source files changed after source commit: "
|
|
1019
|
+
+ ", ".join(changed_for_entry[:8])
|
|
1020
|
+
)
|
|
1021
|
+
lifecycle["staleReasons"] = reasons
|
|
1022
|
+
lifecycle["lastCheckedAt"] = checked_at
|
|
1023
|
+
if reasons:
|
|
1024
|
+
entry["status"] = "stale"
|
|
1025
|
+
elif entry.get("status") == "stale" and had_generated_reason:
|
|
1026
|
+
entry["status"] = "candidate"
|
|
1027
|
+
return stats
|
|
1028
|
+
|
|
1029
|
+
|
|
862
1030
|
def first_sentence(text: str) -> str:
|
|
863
1031
|
clean = re.sub(r"\s+", " ", text).strip()
|
|
864
1032
|
if len(clean) <= 180:
|
|
@@ -2379,6 +2547,7 @@ class KnowledgeSnapshot:
|
|
|
2379
2547
|
"config",
|
|
2380
2548
|
"summary_paths",
|
|
2381
2549
|
"archive_records",
|
|
2550
|
+
"publications",
|
|
2382
2551
|
"inputs_hash",
|
|
2383
2552
|
)
|
|
2384
2553
|
|
|
@@ -2390,6 +2559,7 @@ class KnowledgeSnapshot:
|
|
|
2390
2559
|
config: dict[str, Any],
|
|
2391
2560
|
summary_paths: list[Path],
|
|
2392
2561
|
archive_records: list[dict[str, Any]],
|
|
2562
|
+
publications: dict[str, dict[str, Any]],
|
|
2393
2563
|
inputs_hash: str,
|
|
2394
2564
|
) -> None:
|
|
2395
2565
|
self.project = project
|
|
@@ -2398,6 +2568,7 @@ class KnowledgeSnapshot:
|
|
|
2398
2568
|
self.config = config
|
|
2399
2569
|
self.summary_paths = summary_paths
|
|
2400
2570
|
self.archive_records = archive_records
|
|
2571
|
+
self.publications = publications
|
|
2401
2572
|
self.inputs_hash = inputs_hash
|
|
2402
2573
|
|
|
2403
2574
|
|
|
@@ -2446,11 +2617,17 @@ def build_snapshot(project: Path) -> KnowledgeSnapshot:
|
|
|
2446
2617
|
knowledge = project / ".harness" / "knowledge"
|
|
2447
2618
|
pname = project_id(project)
|
|
2448
2619
|
config = load_config(knowledge)
|
|
2449
|
-
summary_paths =
|
|
2450
|
-
archive_records = archive_summary_records(project, summary_paths)
|
|
2620
|
+
summary_paths, archive_records, publications = discover_archive_inputs(project)
|
|
2451
2621
|
inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
|
|
2452
2622
|
return KnowledgeSnapshot(
|
|
2453
|
-
project,
|
|
2623
|
+
project,
|
|
2624
|
+
knowledge,
|
|
2625
|
+
pname,
|
|
2626
|
+
config,
|
|
2627
|
+
summary_paths,
|
|
2628
|
+
archive_records,
|
|
2629
|
+
publications,
|
|
2630
|
+
inputs_hash,
|
|
2454
2631
|
)
|
|
2455
2632
|
|
|
2456
2633
|
|
|
@@ -2473,13 +2650,13 @@ def build_index(
|
|
|
2473
2650
|
config = snapshot.config
|
|
2474
2651
|
summary_paths = snapshot.summary_paths
|
|
2475
2652
|
archive_records = snapshot.archive_records
|
|
2653
|
+
publications = snapshot.publications
|
|
2476
2654
|
inputs_hash = snapshot.inputs_hash
|
|
2477
2655
|
else:
|
|
2478
2656
|
knowledge = project / ".harness" / "knowledge"
|
|
2479
2657
|
pname = project_id(project)
|
|
2480
2658
|
config = load_config(knowledge)
|
|
2481
|
-
summary_paths =
|
|
2482
|
-
archive_records = archive_summary_records(project, summary_paths)
|
|
2659
|
+
summary_paths, archive_records, publications = discover_archive_inputs(project)
|
|
2483
2660
|
inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
|
|
2484
2661
|
ensure_knowledge_dirs(knowledge)
|
|
2485
2662
|
reporter.emit("snapshot", len(summary_paths), len(summary_paths), force=True)
|
|
@@ -2526,7 +2703,11 @@ def build_index(
|
|
|
2526
2703
|
current_head = git_head(project) # recorded in manifest only; not an invalidation key
|
|
2527
2704
|
entries: list[dict[str, Any]] = []
|
|
2528
2705
|
failures: list[dict[str, str]] = []
|
|
2529
|
-
|
|
2706
|
+
archive_records_by_summary = {
|
|
2707
|
+
str(record.get("summaryData") or ""): record
|
|
2708
|
+
for record in archive_records
|
|
2709
|
+
if record.get("summaryData")
|
|
2710
|
+
}
|
|
2530
2711
|
ingest_mode: dict[str, Any] = {
|
|
2531
2712
|
"incremental": incremental,
|
|
2532
2713
|
"mode": mode,
|
|
@@ -2545,11 +2726,22 @@ def build_index(
|
|
|
2545
2726
|
"sqliteRebuild": 0,
|
|
2546
2727
|
"sqliteUpsert": 0,
|
|
2547
2728
|
"sqliteDelete": 0,
|
|
2729
|
+
"summaryHashesReused": 0,
|
|
2730
|
+
"summaryHashesComputed": 0,
|
|
2548
2731
|
}
|
|
2549
2732
|
|
|
2550
2733
|
for summary_index, summary_path in enumerate(summary_paths, start=1):
|
|
2551
2734
|
summary_rel = rel_to_project(project, summary_path)
|
|
2552
|
-
|
|
2735
|
+
summary_record = archive_records_by_summary.get(summary_rel)
|
|
2736
|
+
summary_hash = (
|
|
2737
|
+
str(summary_record.get("summarySha256"))
|
|
2738
|
+
if summary_record and summary_record.get("summarySha256")
|
|
2739
|
+
else sha256_file(summary_path)
|
|
2740
|
+
)
|
|
2741
|
+
if summary_record and summary_record.get("summarySha256"):
|
|
2742
|
+
ingest_mode["summaryHashesReused"] += 1
|
|
2743
|
+
else:
|
|
2744
|
+
ingest_mode["summaryHashesComputed"] += 1
|
|
2553
2745
|
cache_path = archive_entry_cache_path(project, knowledge, summary_path, summary_hash)
|
|
2554
2746
|
try:
|
|
2555
2747
|
archive_entries = None
|
|
@@ -2618,11 +2810,19 @@ def build_index(
|
|
|
2618
2810
|
seen.add(fp)
|
|
2619
2811
|
deduped.append(entry)
|
|
2620
2812
|
|
|
2813
|
+
ingest_mode.update(apply_batch_git_freshness(project, deduped))
|
|
2621
2814
|
reporter.emit("near-deduplicate", 0, len(deduped), force=True)
|
|
2622
2815
|
near_dedupe = dedupe_near_duplicates(deduped, progress=reporter)
|
|
2623
2816
|
ingest_mode["nearDuplicatesMerged"] = near_dedupe["merged"]
|
|
2624
2817
|
ingest_mode["similarityCandidates"] = near_dedupe["similarityCandidates"]
|
|
2625
2818
|
ingest_mode["exactSimilarityComparisons"] = near_dedupe["exactSimilarityComparisons"]
|
|
2819
|
+
ingest_mode["similarityCandidatePairs"] = near_dedupe["candidatePairs"]
|
|
2820
|
+
ingest_mode["similarityBucketCount"] = near_dedupe["bucketCount"]
|
|
2821
|
+
ingest_mode["similarityComparisonBudget"] = near_dedupe["comparisonBudget"]
|
|
2822
|
+
ingest_mode["similarityComparisonBudgetExceeded"] = near_dedupe[
|
|
2823
|
+
"comparisonBudgetExceeded"
|
|
2824
|
+
]
|
|
2825
|
+
ingest_mode["similarityComparisonsSkipped"] = near_dedupe["comparisonsSkipped"]
|
|
2626
2826
|
|
|
2627
2827
|
mark_conflicting_generated_entries(deduped)
|
|
2628
2828
|
mark_degraded_test_evidence(deduped)
|
|
@@ -4299,26 +4499,68 @@ def dedupe_near_duplicates(
|
|
|
4299
4499
|
threshold: float = NEAR_DUPLICATE_THRESHOLD,
|
|
4300
4500
|
*,
|
|
4301
4501
|
progress: ProgressReporter | None = None,
|
|
4502
|
+
max_exact_comparisons: int = 50_000,
|
|
4503
|
+
time_budget_ms: int = 2_000,
|
|
4302
4504
|
) -> dict[str, Any]:
|
|
4303
|
-
"""Merge near
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
if entry.get("status") == "superseded":
|
|
4307
|
-
continue
|
|
4308
|
-
archive = str(entry.get("source", {}).get("archive") or "")
|
|
4309
|
-
by_archive.setdefault(archive, []).append(entry)
|
|
4310
|
-
|
|
4505
|
+
"""Merge near duplicates through sparse buckets and an explicit work budget."""
|
|
4506
|
+
started = time.monotonic()
|
|
4507
|
+
deadline = started + max(0, time_budget_ms) / 1_000
|
|
4311
4508
|
merges: list[dict[str, Any]] = []
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
}
|
|
4509
|
+
active_entries = [
|
|
4510
|
+
entry for entry in entries if entry.get("status") != "superseded"
|
|
4511
|
+
]
|
|
4512
|
+
normalized = {id(entry): entry_compare_text(entry) for entry in active_entries}
|
|
4316
4513
|
similarity_cache: dict[tuple[int, int], float] = {}
|
|
4317
4514
|
similarity_candidates = 0
|
|
4318
4515
|
exact_comparisons = 0
|
|
4516
|
+
comparison_budget_exceeded = False
|
|
4517
|
+
|
|
4518
|
+
grouped: dict[tuple[str, str], list[dict[str, Any]]] = {}
|
|
4519
|
+
for entry in active_entries:
|
|
4520
|
+
key = (
|
|
4521
|
+
str(entry.get("source", {}).get("archive") or ""),
|
|
4522
|
+
str(entry.get("type") or ""),
|
|
4523
|
+
)
|
|
4524
|
+
grouped.setdefault(key, []).append(entry)
|
|
4525
|
+
|
|
4526
|
+
candidate_pairs: set[tuple[int, int]] = set()
|
|
4527
|
+
entry_by_identity = {id(entry): entry for entry in active_entries}
|
|
4528
|
+
bucket_count = 0
|
|
4529
|
+
token_pattern = re.compile(r"[\w-]{3,}", flags=re.UNICODE)
|
|
4530
|
+
for group in grouped.values():
|
|
4531
|
+
token_sets = {
|
|
4532
|
+
id(entry): set(token_pattern.findall(normalized[id(entry)]))
|
|
4533
|
+
for entry in group
|
|
4534
|
+
}
|
|
4535
|
+
frequencies: dict[str, int] = {}
|
|
4536
|
+
for tokens in token_sets.values():
|
|
4537
|
+
for token in tokens:
|
|
4538
|
+
frequencies[token] = frequencies.get(token, 0) + 1
|
|
4539
|
+
buckets: dict[str, list[int]] = {}
|
|
4540
|
+
rare_cutoff = max(2, len(group) // 10)
|
|
4541
|
+
for entry in group:
|
|
4542
|
+
identity = id(entry)
|
|
4543
|
+
tokens = token_sets[identity]
|
|
4544
|
+
selected = sorted(
|
|
4545
|
+
(token for token in tokens if frequencies[token] <= rare_cutoff),
|
|
4546
|
+
key=lambda token: (frequencies[token], token),
|
|
4547
|
+
)[:4]
|
|
4548
|
+
if not selected:
|
|
4549
|
+
selected = [f"length:{len(normalized[identity]) // 40}"]
|
|
4550
|
+
for token in selected:
|
|
4551
|
+
buckets.setdefault(token, []).append(identity)
|
|
4552
|
+
bucket_count += len(buckets)
|
|
4553
|
+
for members in buckets.values():
|
|
4554
|
+
if len(members) < 2:
|
|
4555
|
+
continue
|
|
4556
|
+
for left_index, left_identity in enumerate(members):
|
|
4557
|
+
for right_identity in members[left_index + 1 :]:
|
|
4558
|
+
if left_identity == right_identity:
|
|
4559
|
+
continue
|
|
4560
|
+
candidate_pairs.add(tuple(sorted((left_identity, right_identity))))
|
|
4319
4561
|
|
|
4320
4562
|
def similarity(left: dict[str, Any], right: dict[str, Any]) -> float:
|
|
4321
|
-
nonlocal similarity_candidates, exact_comparisons
|
|
4563
|
+
nonlocal similarity_candidates, exact_comparisons, comparison_budget_exceeded
|
|
4322
4564
|
pair = tuple(sorted((id(left), id(right))))
|
|
4323
4565
|
if pair in similarity_cache:
|
|
4324
4566
|
return similarity_cache[pair]
|
|
@@ -4338,6 +4580,9 @@ def dedupe_near_duplicates(
|
|
|
4338
4580
|
if matcher.real_quick_ratio() < threshold or matcher.quick_ratio() < threshold:
|
|
4339
4581
|
similarity_cache[pair] = 0.0
|
|
4340
4582
|
return 0.0
|
|
4583
|
+
if exact_comparisons >= max(0, max_exact_comparisons) or time.monotonic() >= deadline:
|
|
4584
|
+
comparison_budget_exceeded = True
|
|
4585
|
+
return 0.0
|
|
4341
4586
|
exact_comparisons += 1
|
|
4342
4587
|
value = matcher.ratio()
|
|
4343
4588
|
similarity_cache[pair] = value
|
|
@@ -4350,52 +4595,79 @@ def dedupe_near_duplicates(
|
|
|
4350
4595
|
)
|
|
4351
4596
|
return value
|
|
4352
4597
|
|
|
4353
|
-
for
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4598
|
+
parent = {identity: identity for identity in entry_by_identity}
|
|
4599
|
+
|
|
4600
|
+
def find(identity: int) -> int:
|
|
4601
|
+
while parent[identity] != identity:
|
|
4602
|
+
parent[identity] = parent[parent[identity]]
|
|
4603
|
+
identity = parent[identity]
|
|
4604
|
+
return identity
|
|
4605
|
+
|
|
4606
|
+
def union(left_identity: int, right_identity: int) -> None:
|
|
4607
|
+
left_root = find(left_identity)
|
|
4608
|
+
right_root = find(right_identity)
|
|
4609
|
+
if left_root != right_root:
|
|
4610
|
+
parent[right_root] = left_root
|
|
4611
|
+
|
|
4612
|
+
ordered_pairs = sorted(candidate_pairs)
|
|
4613
|
+
processed_pairs = 0
|
|
4614
|
+
pair_scores: dict[tuple[int, int], float] = {}
|
|
4615
|
+
for left_identity, right_identity in ordered_pairs:
|
|
4616
|
+
if (
|
|
4617
|
+
exact_comparisons >= max(0, max_exact_comparisons)
|
|
4618
|
+
or time.monotonic() >= deadline
|
|
4619
|
+
):
|
|
4620
|
+
comparison_budget_exceeded = True
|
|
4621
|
+
break
|
|
4622
|
+
left = entry_by_identity[left_identity]
|
|
4623
|
+
right = entry_by_identity[right_identity]
|
|
4624
|
+
score = similarity(left, right)
|
|
4625
|
+
pair_scores[(left_identity, right_identity)] = score
|
|
4626
|
+
processed_pairs += 1
|
|
4627
|
+
if score >= threshold:
|
|
4628
|
+
union(left_identity, right_identity)
|
|
4629
|
+
|
|
4630
|
+
clusters: dict[int, list[dict[str, Any]]] = {}
|
|
4631
|
+
for identity, entry in entry_by_identity.items():
|
|
4632
|
+
clusters.setdefault(find(identity), []).append(entry)
|
|
4633
|
+
for cluster in clusters.values():
|
|
4634
|
+
if len(cluster) < 2:
|
|
4635
|
+
continue
|
|
4636
|
+
keeper = cluster[0]
|
|
4637
|
+
for candidate in cluster[1:]:
|
|
4638
|
+
keeper = prefer_entry(keeper, candidate)
|
|
4639
|
+
for absorbed in cluster:
|
|
4640
|
+
if absorbed is keeper or absorbed.get("id") == keeper.get("id"):
|
|
4373
4641
|
continue
|
|
4374
|
-
keeper
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
"
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
"archive": archive,
|
|
4391
|
-
"similarity": round(similarity(keeper, absorbed), 3),
|
|
4392
|
-
}
|
|
4393
|
-
)
|
|
4642
|
+
merge_entry_provenance(keeper, absorbed)
|
|
4643
|
+
supersede_entry(
|
|
4644
|
+
absorbed,
|
|
4645
|
+
str(keeper["id"]),
|
|
4646
|
+
"near-duplicate merged into: " + str(keeper["id"]),
|
|
4647
|
+
)
|
|
4648
|
+
pair = tuple(sorted((id(keeper), id(absorbed))))
|
|
4649
|
+
merges.append(
|
|
4650
|
+
{
|
|
4651
|
+
"keptId": keeper["id"],
|
|
4652
|
+
"mergedId": absorbed["id"],
|
|
4653
|
+
"archive": str(keeper.get("source", {}).get("archive") or ""),
|
|
4654
|
+
"similarity": round(pair_scores.get(pair, threshold), 3),
|
|
4655
|
+
}
|
|
4656
|
+
)
|
|
4657
|
+
comparisons_skipped = max(0, len(ordered_pairs) - processed_pairs)
|
|
4394
4658
|
return {
|
|
4395
4659
|
"merged": len(merges),
|
|
4396
4660
|
"merges": merges,
|
|
4397
4661
|
"similarityCandidates": similarity_candidates,
|
|
4398
4662
|
"exactSimilarityComparisons": exact_comparisons,
|
|
4663
|
+
"candidatePairs": len(candidate_pairs),
|
|
4664
|
+
"bucketCount": bucket_count,
|
|
4665
|
+
"comparisonBudget": {
|
|
4666
|
+
"maxExactComparisons": max(0, max_exact_comparisons),
|
|
4667
|
+
"timeBudgetMs": max(0, time_budget_ms),
|
|
4668
|
+
},
|
|
4669
|
+
"comparisonBudgetExceeded": comparison_budget_exceeded,
|
|
4670
|
+
"comparisonsSkipped": comparisons_skipped,
|
|
4399
4671
|
}
|
|
4400
4672
|
|
|
4401
4673
|
|
|
@@ -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=fe208e6a7ffbc888; overlay=java; agent=cursor; do not edit -->
|
|
7
7
|
# harness-knowledge-query
|
|
8
8
|
|
|
9
9
|
并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-package
|
|
3
3
|
description: 增量模块打包:拉取最新→编译验证→mvn package→汇总 jar/war。使用场景:打包、package、发版准备
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=fe208e6a7ffbc888; overlay=java; agent=cursor; do not edit -->
|
|
6
6
|
# harness-package — 增量模块打包(Java overlay)
|
|
7
7
|
|
|
8
8
|
## Purpose
|