@hunter-harness/workflow-harness 0.2.26 → 0.2.28
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-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +44 -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 +493 -74
- 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 +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +8 -4
- package/harness/bundles/general/claude-code/harness-sync/reference.md +28 -0
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +2147 -189
- package/harness/bundles/general/claude-code/scripts/harness_change.py +8 -3
- package/harness/bundles/general/claude-code/scripts/harness_events.py +57 -9
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +530 -8
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +263 -6
- package/harness/bundles/general/claude-code/scripts/harness_phase.py +758 -1
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/general/claude-code/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +702 -22
- 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-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +44 -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 +493 -74
- 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 +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +8 -4
- package/harness/bundles/general/codebuddy/harness-sync/reference.md +28 -0
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +2147 -189
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +8 -3
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +57 -9
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +530 -8
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +263 -6
- package/harness/bundles/general/codebuddy/scripts/harness_phase.py +758 -1
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/general/codebuddy/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +702 -22
- 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-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +44 -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 +493 -74
- 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 +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +8 -4
- package/harness/bundles/general/codex/harness-sync/reference.md +28 -0
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +2147 -189
- package/harness/bundles/general/codex/scripts/harness_change.py +8 -3
- package/harness/bundles/general/codex/scripts/harness_events.py +57 -9
- package/harness/bundles/general/codex/scripts/harness_gate.py +530 -8
- package/harness/bundles/general/codex/scripts/harness_integration.py +263 -6
- package/harness/bundles/general/codex/scripts/harness_phase.py +758 -1
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/general/codex/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +702 -22
- 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-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +44 -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 +493 -74
- 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 +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +8 -4
- package/harness/bundles/general/cursor/harness-sync/reference.md +28 -0
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +2147 -189
- package/harness/bundles/general/cursor/scripts/harness_change.py +8 -3
- package/harness/bundles/general/cursor/scripts/harness_events.py +57 -9
- package/harness/bundles/general/cursor/scripts/harness_gate.py +530 -8
- package/harness/bundles/general/cursor/scripts/harness_integration.py +263 -6
- package/harness/bundles/general/cursor/scripts/harness_phase.py +758 -1
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/general/cursor/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +702 -22
- 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-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +44 -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 +493 -74
- 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 +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +8 -4
- package/harness/bundles/java/claude-code/harness-sync/reference.md +28 -0
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +2147 -189
- package/harness/bundles/java/claude-code/scripts/harness_change.py +8 -3
- package/harness/bundles/java/claude-code/scripts/harness_events.py +57 -9
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +530 -8
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +263 -6
- package/harness/bundles/java/claude-code/scripts/harness_phase.py +758 -1
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/java/claude-code/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +702 -22
- 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-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +44 -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 +493 -74
- 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 +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +8 -4
- package/harness/bundles/java/codebuddy/harness-sync/reference.md +28 -0
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +2147 -189
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +8 -3
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +57 -9
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +530 -8
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +263 -6
- package/harness/bundles/java/codebuddy/scripts/harness_phase.py +758 -1
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/java/codebuddy/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +702 -22
- 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-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +44 -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 +493 -74
- 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 +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +8 -4
- package/harness/bundles/java/codex/harness-sync/reference.md +28 -0
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +2147 -189
- package/harness/bundles/java/codex/scripts/harness_change.py +8 -3
- package/harness/bundles/java/codex/scripts/harness_events.py +57 -9
- package/harness/bundles/java/codex/scripts/harness_gate.py +530 -8
- package/harness/bundles/java/codex/scripts/harness_integration.py +263 -6
- package/harness/bundles/java/codex/scripts/harness_phase.py +758 -1
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/java/codex/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +702 -22
- 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-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +44 -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 +493 -74
- 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 +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +8 -4
- package/harness/bundles/java/cursor/harness-sync/reference.md +28 -0
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +2147 -189
- package/harness/bundles/java/cursor/scripts/harness_change.py +8 -3
- package/harness/bundles/java/cursor/scripts/harness_events.py +57 -9
- package/harness/bundles/java/cursor/scripts/harness_gate.py +530 -8
- package/harness/bundles/java/cursor/scripts/harness_integration.py +263 -6
- package/harness/bundles/java/cursor/scripts/harness_phase.py +758 -1
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/java/cursor/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +702 -22
- package/harness/manifests/general/claude-code.json +28 -24
- package/harness/manifests/general/codebuddy.json +28 -24
- package/harness/manifests/general/codex.json +28 -24
- package/harness/manifests/general/cursor.json +28 -24
- package/harness/manifests/java/claude-code.json +30 -26
- package/harness/manifests/java/codebuddy.json +30 -26
- package/harness/manifests/java/codex.json +30 -26
- package/harness/manifests/java/cursor.json +30 -26
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py
CHANGED
|
@@ -353,65 +353,267 @@ def changed_file_paths(summary: dict[str, Any]) -> list[str]:
|
|
|
353
353
|
return files
|
|
354
354
|
|
|
355
355
|
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
AUTHORITATIVE_RESOLVER_VERSION = 1
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _path_is_within(path: Path, root: Path) -> bool:
|
|
360
|
+
try:
|
|
361
|
+
path.relative_to(root)
|
|
362
|
+
return True
|
|
363
|
+
except ValueError:
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _authority_file_state(path: Path, root: Path) -> dict[str, Any]:
|
|
368
|
+
"""Describe one repair-control file without reading outside its allowed root."""
|
|
369
|
+
if not os.path.lexists(path):
|
|
370
|
+
return {"state": "missing", "sha256": None}
|
|
371
|
+
try:
|
|
372
|
+
resolved = path.resolve(strict=True)
|
|
373
|
+
except OSError:
|
|
374
|
+
return {"state": "unreadable", "sha256": None}
|
|
375
|
+
if not _path_is_within(resolved, root):
|
|
376
|
+
target_hash = hashlib.sha256(str(resolved).encode("utf-8")).hexdigest()
|
|
377
|
+
return {"state": "path-escape", "sha256": None, "targetHash": target_hash}
|
|
378
|
+
if not resolved.is_file():
|
|
379
|
+
return {"state": "not-regular", "sha256": None}
|
|
380
|
+
try:
|
|
381
|
+
return {"state": "file", "sha256": sha256_file(resolved)}
|
|
382
|
+
except OSError:
|
|
383
|
+
return {"state": "unreadable", "sha256": None}
|
|
384
|
+
|
|
358
385
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
386
|
+
def resolve_archive_summary(summary_path: Path) -> dict[str, Any]:
|
|
387
|
+
"""Resolve an archive's original or hash-valid authoritative summary.
|
|
388
|
+
|
|
389
|
+
Invalid repair metadata never becomes authoritative. The original path is
|
|
390
|
+
retained as a quarantined extraction fallback so one broken repair cannot
|
|
391
|
+
remove an archive from the knowledge index.
|
|
363
392
|
"""
|
|
393
|
+
archive_dir = archive_dir_from_summary(Path(summary_path))
|
|
394
|
+
archive_root = archive_dir.resolve()
|
|
395
|
+
original_path = archive_dir / "reports" / "final" / "summary-data.json"
|
|
396
|
+
fingerprint: dict[str, Any] = {
|
|
397
|
+
"resolverVersion": AUTHORITATIVE_RESOLVER_VERSION,
|
|
398
|
+
"status": "original",
|
|
399
|
+
"allowed": True,
|
|
400
|
+
"version": None,
|
|
401
|
+
"reasons": [],
|
|
402
|
+
"original": {"state": "unknown", "sha256": None},
|
|
403
|
+
"pointer": {"state": "missing", "sha256": None},
|
|
404
|
+
"record": {"state": "not-applicable", "sha256": None},
|
|
405
|
+
"summary": {"state": "not-applicable", "sha256": None},
|
|
406
|
+
}
|
|
407
|
+
result: dict[str, Any] = {
|
|
408
|
+
"summaryPath": original_path,
|
|
409
|
+
"summarySha256": None,
|
|
410
|
+
"status": "original",
|
|
411
|
+
"allowed": True,
|
|
412
|
+
"version": None,
|
|
413
|
+
"reasons": [],
|
|
414
|
+
"fingerprint": fingerprint,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
def degraded(reason: str) -> dict[str, Any]:
|
|
418
|
+
result["status"] = "degraded"
|
|
419
|
+
result["allowed"] = False
|
|
420
|
+
result["reasons"] = [reason]
|
|
421
|
+
fingerprint["status"] = "degraded"
|
|
422
|
+
fingerprint["allowed"] = False
|
|
423
|
+
fingerprint["reasons"] = [reason]
|
|
424
|
+
return result
|
|
425
|
+
|
|
426
|
+
original_state = _authority_file_state(original_path, archive_root)
|
|
427
|
+
fingerprint["original"] = {
|
|
428
|
+
**original_state,
|
|
429
|
+
"sha256": None,
|
|
430
|
+
}
|
|
431
|
+
if original_state["state"] == "path-escape":
|
|
432
|
+
return degraded("summary-data.json escapes archive root")
|
|
433
|
+
if original_state["state"] == "missing":
|
|
434
|
+
return degraded("summary-data.json missing")
|
|
435
|
+
if original_state["state"] == "not-regular":
|
|
436
|
+
return degraded("summary-data.json is not a regular file")
|
|
437
|
+
if original_state["state"] != "file":
|
|
438
|
+
return degraded("summary-data.json unreadable")
|
|
439
|
+
result["summarySha256"] = original_state["sha256"]
|
|
440
|
+
|
|
441
|
+
derived_path = archive_dir / "derived"
|
|
442
|
+
pointer_path = derived_path / "authoritative.json"
|
|
443
|
+
if not os.path.lexists(pointer_path):
|
|
444
|
+
return result
|
|
445
|
+
|
|
446
|
+
derived_root = derived_path.resolve()
|
|
447
|
+
if not _path_is_within(derived_root, archive_root):
|
|
448
|
+
fingerprint["pointer"] = {
|
|
449
|
+
"state": "path-escape",
|
|
450
|
+
"sha256": None,
|
|
451
|
+
"targetHash": hashlib.sha256(str(derived_root).encode("utf-8")).hexdigest(),
|
|
452
|
+
}
|
|
453
|
+
return degraded("authoritative pointer escapes archive root")
|
|
454
|
+
|
|
455
|
+
pointer_state = _authority_file_state(pointer_path, derived_root)
|
|
456
|
+
fingerprint["pointer"] = pointer_state
|
|
457
|
+
if pointer_state["state"] == "path-escape":
|
|
458
|
+
return degraded("authoritative pointer escapes derived root")
|
|
459
|
+
if pointer_state["state"] == "missing":
|
|
460
|
+
return degraded("authoritative pointer missing")
|
|
461
|
+
if pointer_state["state"] == "not-regular":
|
|
462
|
+
return degraded("authoritative pointer is not a regular file")
|
|
463
|
+
if pointer_state["state"] != "file":
|
|
464
|
+
return degraded("authoritative pointer unreadable")
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
pointer = read_json(pointer_path.resolve(strict=True))
|
|
468
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
469
|
+
return degraded("authoritative pointer unreadable")
|
|
470
|
+
if not isinstance(pointer, dict):
|
|
471
|
+
return degraded("authoritative pointer must be an object")
|
|
472
|
+
|
|
473
|
+
version = str(pointer.get("version") or "")
|
|
474
|
+
fingerprint["version"] = version or None
|
|
475
|
+
result["version"] = version or None
|
|
476
|
+
if re.fullmatch(r"v[0-9]+", version) is None:
|
|
477
|
+
return degraded("invalid authoritative version")
|
|
478
|
+
|
|
479
|
+
version_path = derived_path / version
|
|
480
|
+
try:
|
|
481
|
+
version_root = version_path.resolve(strict=True)
|
|
482
|
+
except OSError:
|
|
483
|
+
return degraded("authoritative version missing")
|
|
484
|
+
if not _path_is_within(version_root, derived_root):
|
|
485
|
+
return degraded("authoritative version escapes derived root")
|
|
486
|
+
if not version_root.is_dir():
|
|
487
|
+
return degraded("authoritative version is not a directory")
|
|
488
|
+
|
|
489
|
+
record_path = version_path / "repair-record.json"
|
|
490
|
+
record_state = _authority_file_state(record_path, version_root)
|
|
491
|
+
fingerprint["record"] = record_state
|
|
492
|
+
if record_state["state"] == "path-escape":
|
|
493
|
+
return degraded("repair record escapes authoritative version")
|
|
494
|
+
if record_state["state"] == "missing":
|
|
495
|
+
return degraded("repair record missing")
|
|
496
|
+
if record_state["state"] == "not-regular":
|
|
497
|
+
return degraded("repair record is not a regular file")
|
|
498
|
+
if record_state["state"] != "file":
|
|
499
|
+
return degraded("repair record unreadable")
|
|
500
|
+
|
|
501
|
+
derived_summary_path = version_path / "summary-data.json"
|
|
502
|
+
summary_state = _authority_file_state(derived_summary_path, version_root)
|
|
503
|
+
fingerprint["summary"] = summary_state
|
|
504
|
+
if summary_state["state"] == "path-escape":
|
|
505
|
+
return degraded("authoritative summary escapes authoritative version")
|
|
506
|
+
if summary_state["state"] == "missing":
|
|
507
|
+
return degraded("authoritative summary missing")
|
|
508
|
+
if summary_state["state"] == "not-regular":
|
|
509
|
+
return degraded("authoritative summary is not a regular file")
|
|
510
|
+
if summary_state["state"] != "file":
|
|
511
|
+
return degraded("authoritative summary unreadable")
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
record = read_json(record_path.resolve(strict=True))
|
|
515
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
516
|
+
return degraded("repair record unreadable")
|
|
517
|
+
if not isinstance(record, dict):
|
|
518
|
+
return degraded("repair record must be an object")
|
|
519
|
+
record_version = record.get("version")
|
|
520
|
+
if not isinstance(record_version, str) or record_version != version:
|
|
521
|
+
return degraded("repair record version mismatch")
|
|
522
|
+
|
|
523
|
+
actual = "sha256:" + str(summary_state["sha256"])
|
|
524
|
+
if record.get("summarySha256") != actual:
|
|
525
|
+
return degraded("repair record summary hash mismatch")
|
|
526
|
+
if pointer.get("summarySha256") != actual:
|
|
527
|
+
return degraded("authoritative pointer hash mismatch")
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
summary = read_json(derived_summary_path.resolve(strict=True))
|
|
531
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
532
|
+
return degraded("authoritative summary unreadable")
|
|
533
|
+
if not isinstance(summary, dict):
|
|
534
|
+
return degraded("authoritative summary must be an object")
|
|
535
|
+
|
|
536
|
+
result["summaryPath"] = derived_summary_path.resolve(strict=True)
|
|
537
|
+
result["summarySha256"] = summary_state["sha256"]
|
|
538
|
+
result["status"] = "authoritative"
|
|
539
|
+
result["version"] = version
|
|
540
|
+
fingerprint["status"] = "authoritative"
|
|
541
|
+
fingerprint["version"] = version
|
|
542
|
+
return result
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def discover_archive_summary_paths(project: Path) -> list[Path]:
|
|
546
|
+
archive_root = project / ".harness" / "archive"
|
|
547
|
+
resolved_archive_root = archive_root.resolve()
|
|
548
|
+
originals = sorted(archive_root.glob("*/reports/final/summary-data.json"))
|
|
549
|
+
selected: list[Path] = []
|
|
550
|
+
for original in originals:
|
|
551
|
+
resolved_archive = archive_dir_from_summary(original).resolve()
|
|
552
|
+
if (
|
|
553
|
+
not _path_is_within(resolved_archive, resolved_archive_root)
|
|
554
|
+
or resolved_archive.parent != resolved_archive_root
|
|
555
|
+
):
|
|
556
|
+
continue
|
|
557
|
+
resolution = resolve_archive_summary(original)
|
|
558
|
+
original_state = (resolution.get("fingerprint") or {}).get("original") or {}
|
|
559
|
+
if original_state.get("state") != "file":
|
|
560
|
+
continue
|
|
561
|
+
try:
|
|
562
|
+
selected_path = Path(resolution["summaryPath"]).resolve(strict=True)
|
|
563
|
+
except (KeyError, OSError):
|
|
564
|
+
continue
|
|
565
|
+
if (
|
|
566
|
+
not _path_is_within(selected_path, resolved_archive)
|
|
567
|
+
or not selected_path.is_file()
|
|
568
|
+
):
|
|
569
|
+
continue
|
|
570
|
+
selected.append(selected_path)
|
|
571
|
+
return selected
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def archive_publication_status(project: Path, archive_rel: str) -> dict[str, Any]:
|
|
575
|
+
"""Knowledge publication gate for one archive (API-006/RET-40)."""
|
|
364
576
|
result: dict[str, Any] = {"status": "missing", "allowed": False, "reasons": []}
|
|
577
|
+
archive_root = (Path(project) / ".harness" / "archive").resolve()
|
|
365
578
|
archive_dir = (Path(project) / archive_rel).resolve()
|
|
366
|
-
|
|
367
|
-
|
|
579
|
+
if not _path_is_within(archive_dir, archive_root) or archive_dir.parent != archive_root:
|
|
580
|
+
result["reasons"].append("archive path escapes archive root")
|
|
581
|
+
return result
|
|
582
|
+
|
|
583
|
+
original_path = archive_dir / "reports" / "final" / "summary-data.json"
|
|
584
|
+
resolution = resolve_archive_summary(original_path)
|
|
585
|
+
if resolution.get("version"):
|
|
586
|
+
result["authoritativeVersion"] = resolution["version"]
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
summary_path = Path(resolution["summaryPath"]).resolve(strict=True)
|
|
590
|
+
except (KeyError, OSError):
|
|
368
591
|
result["reasons"].append("summary-data.json missing")
|
|
369
592
|
return result
|
|
593
|
+
if not _path_is_within(summary_path, archive_dir) or not summary_path.is_file():
|
|
594
|
+
result["status"] = "degraded"
|
|
595
|
+
result["reasons"].append("resolved summary escapes archive root")
|
|
596
|
+
return result
|
|
370
597
|
try:
|
|
371
|
-
|
|
372
|
-
except
|
|
598
|
+
summary_hash = sha256_file(summary_path)
|
|
599
|
+
except OSError:
|
|
600
|
+
result["reasons"].append("summary-data.json unreadable")
|
|
601
|
+
return result
|
|
602
|
+
result["summaryData"] = rel_to_project(project, summary_path)
|
|
603
|
+
result["summarySha256"] = summary_hash
|
|
604
|
+
if not resolution.get("allowed"):
|
|
605
|
+
result["status"] = "degraded"
|
|
606
|
+
result["reasons"] = list(resolution.get("reasons") or [])
|
|
607
|
+
return result
|
|
608
|
+
try:
|
|
609
|
+
summary = read_json(summary_path)
|
|
610
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
373
611
|
result["reasons"].append("summary-data.json unreadable")
|
|
374
612
|
return result
|
|
375
613
|
if not isinstance(summary, dict):
|
|
376
614
|
result["reasons"].append("summary-data.json must be an object")
|
|
377
615
|
return result
|
|
378
616
|
|
|
379
|
-
# A versioned repair becomes authoritative only after its pointer and
|
|
380
|
-
# repair-record hashes are verified. Gate the authoritative summary, not
|
|
381
|
-
# the known-bad original that caused the repair.
|
|
382
|
-
pointer_path = archive_dir / "derived" / "authoritative.json"
|
|
383
|
-
if pointer_path.is_file():
|
|
384
|
-
try:
|
|
385
|
-
pointer = json.loads(pointer_path.read_text(encoding="utf-8"))
|
|
386
|
-
version = str(pointer.get("version") or "")
|
|
387
|
-
if re.fullmatch(r"v[0-9]+", version) is None:
|
|
388
|
-
raise ValueError("invalid authoritative version")
|
|
389
|
-
version_dir = (archive_dir / "derived" / version).resolve()
|
|
390
|
-
if not version_dir.is_relative_to((archive_dir / "derived").resolve()):
|
|
391
|
-
raise ValueError("authoritative version escapes derived root")
|
|
392
|
-
record = json.loads(
|
|
393
|
-
(version_dir / "repair-record.json").read_text(encoding="utf-8")
|
|
394
|
-
)
|
|
395
|
-
derived_path = version_dir / "summary-data.json"
|
|
396
|
-
summary_bytes = derived_path.read_bytes()
|
|
397
|
-
actual = "sha256:" + hashlib.sha256(summary_bytes).hexdigest()
|
|
398
|
-
if record.get("summarySha256") != actual:
|
|
399
|
-
result["status"] = "degraded"
|
|
400
|
-
result["reasons"].append("repair record summary hash mismatch")
|
|
401
|
-
return result
|
|
402
|
-
if pointer.get("summarySha256") not in {None, actual}:
|
|
403
|
-
result["status"] = "degraded"
|
|
404
|
-
result["reasons"].append("authoritative pointer hash mismatch")
|
|
405
|
-
return result
|
|
406
|
-
summary = json.loads(summary_bytes.decode("utf-8"))
|
|
407
|
-
if not isinstance(summary, dict):
|
|
408
|
-
raise ValueError("authoritative summary must be an object")
|
|
409
|
-
result["authoritativeVersion"] = version
|
|
410
|
-
except (OSError, json.JSONDecodeError, UnicodeDecodeError, ValueError):
|
|
411
|
-
result["status"] = "degraded"
|
|
412
|
-
result["reasons"].append("authoritative version unreadable")
|
|
413
|
-
return result
|
|
414
|
-
|
|
415
617
|
consistency = (summary.get("reportPipeline") or {}).get("sourceConsistency")
|
|
416
618
|
if consistency is None:
|
|
417
619
|
result["status"] = "unverified"
|
|
@@ -442,16 +644,69 @@ def archive_publication_status(project: Path, archive_rel: str) -> dict[str, Any
|
|
|
442
644
|
return result
|
|
443
645
|
|
|
444
646
|
|
|
647
|
+
def archive_publication_statuses(project: Path) -> dict[str, dict[str, Any]]:
|
|
648
|
+
"""Resolve publication state for every safe immediate archive directory."""
|
|
649
|
+
archive_root = Path(project) / ".harness" / "archive"
|
|
650
|
+
try:
|
|
651
|
+
resolved_archive_root = archive_root.resolve(strict=True)
|
|
652
|
+
children = sorted(archive_root.iterdir())
|
|
653
|
+
except OSError:
|
|
654
|
+
return {}
|
|
655
|
+
|
|
656
|
+
publications: dict[str, dict[str, Any]] = {}
|
|
657
|
+
for child in children:
|
|
658
|
+
try:
|
|
659
|
+
archive_dir = child.resolve(strict=True)
|
|
660
|
+
except OSError:
|
|
661
|
+
continue
|
|
662
|
+
if (
|
|
663
|
+
not archive_dir.is_dir()
|
|
664
|
+
or not _path_is_within(archive_dir, resolved_archive_root)
|
|
665
|
+
or archive_dir.parent != resolved_archive_root
|
|
666
|
+
):
|
|
667
|
+
continue
|
|
668
|
+
archive_rel = rel_to_project(project, archive_dir)
|
|
669
|
+
publications[archive_rel] = archive_publication_status(project, archive_rel)
|
|
670
|
+
return publications
|
|
671
|
+
|
|
672
|
+
|
|
445
673
|
def archive_summary_records(project: Path, summary_paths: list[Path]) -> list[dict[str, Any]]:
|
|
446
674
|
records = []
|
|
675
|
+
archive_root = (Path(project) / ".harness" / "archive").resolve()
|
|
447
676
|
for summary_path in summary_paths:
|
|
448
|
-
|
|
677
|
+
resolution = resolve_archive_summary(summary_path)
|
|
678
|
+
archive_dir = archive_dir_from_summary(summary_path).resolve()
|
|
679
|
+
if (
|
|
680
|
+
not _path_is_within(archive_dir, archive_root)
|
|
681
|
+
or archive_dir.parent != archive_root
|
|
682
|
+
):
|
|
683
|
+
continue
|
|
684
|
+
original_state = (resolution.get("fingerprint") or {}).get("original") or {}
|
|
685
|
+
if original_state.get("state") != "file":
|
|
686
|
+
continue
|
|
687
|
+
try:
|
|
688
|
+
selected_path = Path(resolution["summaryPath"]).resolve(strict=True)
|
|
689
|
+
except (KeyError, OSError):
|
|
690
|
+
continue
|
|
691
|
+
if (
|
|
692
|
+
not _path_is_within(selected_path, archive_dir)
|
|
693
|
+
or not selected_path.is_file()
|
|
694
|
+
):
|
|
695
|
+
continue
|
|
696
|
+
stat = selected_path.stat()
|
|
449
697
|
records.append(
|
|
450
698
|
{
|
|
451
|
-
"archive": rel_to_project(project, archive_dir_from_summary(
|
|
452
|
-
"summaryData": rel_to_project(project,
|
|
453
|
-
"summarySha256": sha256_file(
|
|
699
|
+
"archive": rel_to_project(project, archive_dir_from_summary(selected_path)),
|
|
700
|
+
"summaryData": rel_to_project(project, selected_path),
|
|
701
|
+
"summarySha256": sha256_file(selected_path),
|
|
454
702
|
"mtime": dt.datetime.fromtimestamp(stat.st_mtime, dt.timezone.utc).isoformat(timespec="seconds"),
|
|
703
|
+
"_authority": {
|
|
704
|
+
"status": resolution["status"],
|
|
705
|
+
"allowed": resolution["allowed"],
|
|
706
|
+
"version": resolution["version"],
|
|
707
|
+
"reasons": list(resolution["reasons"]),
|
|
708
|
+
"fingerprint": resolution["fingerprint"],
|
|
709
|
+
},
|
|
455
710
|
}
|
|
456
711
|
)
|
|
457
712
|
return records
|
|
@@ -778,15 +1033,18 @@ def prune_generated_entries(knowledge: Path, current_entries: list[dict[str, Any
|
|
|
778
1033
|
path.unlink(missing_ok=True)
|
|
779
1034
|
removed += 1
|
|
780
1035
|
continue
|
|
781
|
-
if eid in preserved_ids:
|
|
782
|
-
continue
|
|
783
1036
|
replacement = current_by_id.get(str(eid))
|
|
784
|
-
if replacement is None:
|
|
1037
|
+
if replacement is not None:
|
|
1038
|
+
if (
|
|
1039
|
+
str(replacement.get("status") or "") == status
|
|
1040
|
+
and source_identity(entry) == source_identity(replacement)
|
|
1041
|
+
):
|
|
1042
|
+
continue
|
|
785
1043
|
path.unlink(missing_ok=True)
|
|
786
1044
|
removed += 1
|
|
787
|
-
elif
|
|
788
|
-
|
|
789
|
-
|
|
1045
|
+
elif eid in preserved_ids:
|
|
1046
|
+
continue
|
|
1047
|
+
else:
|
|
790
1048
|
path.unlink(missing_ok=True)
|
|
791
1049
|
removed += 1
|
|
792
1050
|
return removed
|
|
@@ -875,12 +1133,129 @@ def load_preserved_entries(knowledge: Path) -> list[dict[str, Any]]:
|
|
|
875
1133
|
return active_entries + manually_decided_entries
|
|
876
1134
|
|
|
877
1135
|
|
|
1136
|
+
def source_identity(entry: dict[str, Any]) -> tuple[str, str]:
|
|
1137
|
+
source = entry.get("source")
|
|
1138
|
+
if not isinstance(source, dict):
|
|
1139
|
+
return ("", "")
|
|
1140
|
+
return (
|
|
1141
|
+
str(source.get("summaryData") or ""),
|
|
1142
|
+
str(source.get("summarySha256") or ""),
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def entry_matches_publication(
|
|
1147
|
+
entry: dict[str, Any],
|
|
1148
|
+
publication: dict[str, Any],
|
|
1149
|
+
) -> bool:
|
|
1150
|
+
selected = (
|
|
1151
|
+
str(publication.get("summaryData") or ""),
|
|
1152
|
+
str(publication.get("summarySha256") or ""),
|
|
1153
|
+
)
|
|
1154
|
+
return bool(all(selected)) and source_identity(entry) == selected
|
|
1155
|
+
|
|
1156
|
+
|
|
878
1157
|
def combine_generated_with_preserved(knowledge: Path, generated: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
879
1158
|
preserved_entries = load_preserved_entries(knowledge)
|
|
1159
|
+
generated_by_id = {
|
|
1160
|
+
str(entry["id"]): entry
|
|
1161
|
+
for entry in generated
|
|
1162
|
+
if isinstance(entry, dict) and entry.get("id")
|
|
1163
|
+
}
|
|
1164
|
+
preserved_entries = [
|
|
1165
|
+
entry
|
|
1166
|
+
for entry in preserved_entries
|
|
1167
|
+
if not (
|
|
1168
|
+
str(entry.get("id") or "") in generated_by_id
|
|
1169
|
+
and source_identity(entry)
|
|
1170
|
+
!= source_identity(generated_by_id[str(entry["id"])])
|
|
1171
|
+
)
|
|
1172
|
+
]
|
|
880
1173
|
preserved_ids = {entry["id"] for entry in preserved_entries}
|
|
881
1174
|
return preserved_entries + [entry for entry in generated if entry["id"] not in preserved_ids]
|
|
882
1175
|
|
|
883
1176
|
|
|
1177
|
+
def reconcile_archive_publication_entries(
|
|
1178
|
+
knowledge: Path,
|
|
1179
|
+
publications: dict[str, dict[str, Any]],
|
|
1180
|
+
) -> dict[str, int]:
|
|
1181
|
+
"""Demote preserved active entries when their source archive becomes blocked."""
|
|
1182
|
+
demoted = 0
|
|
1183
|
+
written = 0
|
|
1184
|
+
for status in ["active", "candidate", "stale", "superseded", "conflicted"]:
|
|
1185
|
+
status_dir = knowledge / "entries" / status
|
|
1186
|
+
if not status_dir.exists():
|
|
1187
|
+
continue
|
|
1188
|
+
for source_path in sorted(status_dir.glob("*.json")):
|
|
1189
|
+
try:
|
|
1190
|
+
entry = read_json(source_path)
|
|
1191
|
+
except (OSError, json.JSONDecodeError):
|
|
1192
|
+
continue
|
|
1193
|
+
if not isinstance(entry, dict):
|
|
1194
|
+
continue
|
|
1195
|
+
source = entry.get("source")
|
|
1196
|
+
archive = source.get("archive") if isinstance(source, dict) else None
|
|
1197
|
+
publication = publications.get(str(archive)) if archive else None
|
|
1198
|
+
if publication is None:
|
|
1199
|
+
continue
|
|
1200
|
+
|
|
1201
|
+
lifecycle = entry.setdefault("lifecycle", {})
|
|
1202
|
+
publication_allowed = bool(publication.get("allowed"))
|
|
1203
|
+
source_matches = entry_matches_publication(entry, publication)
|
|
1204
|
+
allowed = publication_allowed and source_matches
|
|
1205
|
+
reasons = list(publication.get("reasons") or [])
|
|
1206
|
+
if publication_allowed and not source_matches:
|
|
1207
|
+
reasons = ["entry source is not current authoritative summary"]
|
|
1208
|
+
changed = False
|
|
1209
|
+
target_path = source_path
|
|
1210
|
+
if not allowed:
|
|
1211
|
+
if lifecycle.get("publishBlocked") != reasons:
|
|
1212
|
+
lifecycle["publishBlocked"] = reasons
|
|
1213
|
+
lifecycle["lastCheckedAt"] = now_iso()
|
|
1214
|
+
changed = True
|
|
1215
|
+
if status == "active":
|
|
1216
|
+
entry["status"] = "candidate"
|
|
1217
|
+
lifecycle["previousStatus"] = "active"
|
|
1218
|
+
lifecycle["demotedAt"] = now_iso()
|
|
1219
|
+
lifecycle["demotionReason"] = (
|
|
1220
|
+
"archive publication blocked"
|
|
1221
|
+
+ (": " + str(reasons[0]) if reasons else "")
|
|
1222
|
+
)
|
|
1223
|
+
lifecycle["publicationAutoDemoted"] = True
|
|
1224
|
+
lifecycle["lastCheckedAt"] = now_iso()
|
|
1225
|
+
target_path = (
|
|
1226
|
+
knowledge / "entries" / "candidate" / entry_filename(entry)
|
|
1227
|
+
)
|
|
1228
|
+
changed = True
|
|
1229
|
+
demoted += 1
|
|
1230
|
+
elif lifecycle.get("publicationAutoDemoted"):
|
|
1231
|
+
restore_active = (
|
|
1232
|
+
status == "candidate"
|
|
1233
|
+
and lifecycle.get("previousStatus") == "active"
|
|
1234
|
+
and not lifecycle.get("judgeAction")
|
|
1235
|
+
)
|
|
1236
|
+
lifecycle.pop("publishBlocked", None)
|
|
1237
|
+
lifecycle.pop("publicationAutoDemoted", None)
|
|
1238
|
+
if not lifecycle.get("judgeAction"):
|
|
1239
|
+
lifecycle.pop("demotedAt", None)
|
|
1240
|
+
lifecycle.pop("demotionReason", None)
|
|
1241
|
+
lifecycle.pop("previousStatus", None)
|
|
1242
|
+
if restore_active:
|
|
1243
|
+
entry["status"] = "active"
|
|
1244
|
+
target_path = (
|
|
1245
|
+
knowledge / "entries" / "active" / entry_filename(entry)
|
|
1246
|
+
)
|
|
1247
|
+
lifecycle["lastCheckedAt"] = now_iso()
|
|
1248
|
+
changed = True
|
|
1249
|
+
|
|
1250
|
+
if not changed:
|
|
1251
|
+
continue
|
|
1252
|
+
if write_json_if_changed(target_path, entry):
|
|
1253
|
+
written += 1
|
|
1254
|
+
if source_path != target_path and source_path.exists():
|
|
1255
|
+
source_path.unlink()
|
|
1256
|
+
return {"demoted": demoted, "written": written}
|
|
1257
|
+
|
|
1258
|
+
|
|
884
1259
|
def archive_sort_key(entry: dict[str, Any]) -> tuple[str, str]:
|
|
885
1260
|
archive = entry.get("source", {}).get("archive", "")
|
|
886
1261
|
match = re.search(r"(\d{4}-\d{2}-\d{2})", archive)
|
|
@@ -1126,8 +1501,8 @@ def persist_entry_updates(knowledge: Path, entries: list[dict[str, Any]]) -> int
|
|
|
1126
1501
|
path = knowledge / "entries" / status / entry_filename(entry)
|
|
1127
1502
|
if path.exists():
|
|
1128
1503
|
_preserve_confidence_timestamp(path, entry)
|
|
1129
|
-
|
|
1130
|
-
|
|
1504
|
+
if write_json_if_changed(path, entry):
|
|
1505
|
+
written += 1
|
|
1131
1506
|
return written
|
|
1132
1507
|
|
|
1133
1508
|
|
|
@@ -1839,7 +2214,11 @@ def compute_inputs_hash_from_preserved(
|
|
|
1839
2214
|
archive_fingerprint = json.dumps(
|
|
1840
2215
|
sorted(
|
|
1841
2216
|
(
|
|
1842
|
-
{
|
|
2217
|
+
{
|
|
2218
|
+
"path": r.get("summaryData"),
|
|
2219
|
+
"sha256": r.get("summarySha256"),
|
|
2220
|
+
"authority": r.get("_authority", {}),
|
|
2221
|
+
}
|
|
1843
2222
|
for r in records
|
|
1844
2223
|
),
|
|
1845
2224
|
key=lambda item: item["path"] or "",
|
|
@@ -1926,9 +2305,7 @@ def build_snapshot(project: Path) -> KnowledgeSnapshot:
|
|
|
1926
2305
|
knowledge = project / ".harness" / "knowledge"
|
|
1927
2306
|
pname = project_id(project)
|
|
1928
2307
|
config = load_config(knowledge)
|
|
1929
|
-
summary_paths =
|
|
1930
|
-
(project / ".harness" / "archive").glob("*/reports/final/summary-data.json")
|
|
1931
|
-
)
|
|
2308
|
+
summary_paths = discover_archive_summary_paths(project)
|
|
1932
2309
|
archive_records = archive_summary_records(project, summary_paths)
|
|
1933
2310
|
inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
|
|
1934
2311
|
return KnowledgeSnapshot(
|
|
@@ -1956,9 +2333,7 @@ def build_index(
|
|
|
1956
2333
|
knowledge = project / ".harness" / "knowledge"
|
|
1957
2334
|
pname = project_id(project)
|
|
1958
2335
|
config = load_config(knowledge)
|
|
1959
|
-
summary_paths =
|
|
1960
|
-
(project / ".harness" / "archive").glob("*/reports/final/summary-data.json")
|
|
1961
|
-
)
|
|
2336
|
+
summary_paths = discover_archive_summary_paths(project)
|
|
1962
2337
|
archive_records = archive_summary_records(project, summary_paths)
|
|
1963
2338
|
inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
|
|
1964
2339
|
ensure_knowledge_dirs(knowledge)
|
|
@@ -2005,6 +2380,7 @@ def build_index(
|
|
|
2005
2380
|
current_head = git_head(project) # recorded in manifest only; not an invalidation key
|
|
2006
2381
|
entries: list[dict[str, Any]] = []
|
|
2007
2382
|
failures: list[dict[str, str]] = []
|
|
2383
|
+
publications = archive_publication_statuses(project)
|
|
2008
2384
|
ingest_mode: dict[str, Any] = {
|
|
2009
2385
|
"incremental": incremental,
|
|
2010
2386
|
"mode": mode,
|
|
@@ -2053,7 +2429,10 @@ def build_index(
|
|
|
2053
2429
|
# API-006/RET-40: entries from archives failing the publication
|
|
2054
2430
|
# gate stay quarantined (candidate only, never active/promoted).
|
|
2055
2431
|
archive_rel = rel_to_project(project, archive_dir_from_summary(summary_path))
|
|
2056
|
-
publication =
|
|
2432
|
+
publication = publications.get(archive_rel)
|
|
2433
|
+
if publication is None:
|
|
2434
|
+
publication = archive_publication_status(project, archive_rel)
|
|
2435
|
+
publications[archive_rel] = publication
|
|
2057
2436
|
if not publication.get("allowed"):
|
|
2058
2437
|
for entry in archive_entries:
|
|
2059
2438
|
lifecycle = entry.setdefault("lifecycle", {})
|
|
@@ -2065,6 +2444,13 @@ def build_index(
|
|
|
2065
2444
|
except Exception as exc: # keep one bad archive from blocking the index
|
|
2066
2445
|
failures.append({"path": rel_to_project(project, summary_path), "error": str(exc)})
|
|
2067
2446
|
|
|
2447
|
+
publication_reconciliation = reconcile_archive_publication_entries(
|
|
2448
|
+
knowledge,
|
|
2449
|
+
publications,
|
|
2450
|
+
)
|
|
2451
|
+
ingest_mode["activeAutoDemoted"] += publication_reconciliation["demoted"]
|
|
2452
|
+
ingest_mode["entriesWritten"] += publication_reconciliation["written"]
|
|
2453
|
+
|
|
2068
2454
|
seen: set[str] = set()
|
|
2069
2455
|
deduped: list[dict[str, Any]] = []
|
|
2070
2456
|
duplicates = 0
|
|
@@ -2118,7 +2504,7 @@ def build_index(
|
|
|
2118
2504
|
apply_confidence_scores(indexed_entries, config)
|
|
2119
2505
|
ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
|
|
2120
2506
|
auto_demotions = apply_active_lifecycle_policy(knowledge, indexed_entries, config)
|
|
2121
|
-
ingest_mode["activeAutoDemoted"]
|
|
2507
|
+
ingest_mode["activeAutoDemoted"] += len(auto_demotions)
|
|
2122
2508
|
validation = apply_knowledge_validation(project, knowledge, config)
|
|
2123
2509
|
ingest_mode["validationChecked"] = validation["checked"]
|
|
2124
2510
|
ingest_mode["validationFailed"] = validation["failed"]
|
|
@@ -2198,10 +2584,16 @@ def promote_entry(project: Path, entry_id: str, note: str, allow_stale: bool = F
|
|
|
2198
2584
|
archive_rel = (entry.get("source") or {}).get("archive")
|
|
2199
2585
|
if archive_rel:
|
|
2200
2586
|
publication = archive_publication_status(project, archive_rel)
|
|
2201
|
-
if
|
|
2587
|
+
if (
|
|
2588
|
+
not publication.get("allowed")
|
|
2589
|
+
or not entry_matches_publication(entry, publication)
|
|
2590
|
+
):
|
|
2591
|
+
reasons = list(publication.get("reasons") or [])
|
|
2592
|
+
if publication.get("allowed"):
|
|
2593
|
+
reasons = ["entry source is not current authoritative summary"]
|
|
2202
2594
|
raise ValueError(
|
|
2203
2595
|
"publication blocked: "
|
|
2204
|
-
+ "; ".join(str(r) for r in
|
|
2596
|
+
+ "; ".join(str(r) for r in reasons)
|
|
2205
2597
|
)
|
|
2206
2598
|
entry["status"] = "active"
|
|
2207
2599
|
lifecycle = entry.setdefault("lifecycle", {})
|
|
@@ -2424,6 +2816,15 @@ def make_manifest(
|
|
|
2424
2816
|
}
|
|
2425
2817
|
for e in sorted(entries, key=lambda item: (item["source"]["archive"], item["type"], item["title"]))
|
|
2426
2818
|
]
|
|
2819
|
+
manifest_archive_records = [
|
|
2820
|
+
{
|
|
2821
|
+
"archive": record.get("archive"),
|
|
2822
|
+
"summaryData": record.get("summaryData"),
|
|
2823
|
+
"summarySha256": record.get("summarySha256"),
|
|
2824
|
+
"mtime": record.get("mtime"),
|
|
2825
|
+
}
|
|
2826
|
+
for record in archive_records
|
|
2827
|
+
]
|
|
2427
2828
|
|
|
2428
2829
|
return {
|
|
2429
2830
|
"schemaVersion": 1,
|
|
@@ -2436,7 +2837,7 @@ def make_manifest(
|
|
|
2436
2837
|
"scanned": len(summary_paths),
|
|
2437
2838
|
"indexed": len(summary_paths) - len(failures),
|
|
2438
2839
|
"failed": len(failures),
|
|
2439
|
-
"items":
|
|
2840
|
+
"items": manifest_archive_records,
|
|
2440
2841
|
},
|
|
2441
2842
|
"stats": stats,
|
|
2442
2843
|
"byType": by_type,
|
|
@@ -2793,8 +3194,13 @@ def refresh_outputs_from_entry_files(
|
|
|
2793
3194
|
knowledge: Path,
|
|
2794
3195
|
ingest_mode: dict[str, Any] | None = None,
|
|
2795
3196
|
) -> dict[str, Any]:
|
|
2796
|
-
summary_paths =
|
|
3197
|
+
summary_paths = discover_archive_summary_paths(project)
|
|
2797
3198
|
archive_records = archive_summary_records(project, summary_paths)
|
|
3199
|
+
publications = archive_publication_statuses(project)
|
|
3200
|
+
publication_reconciliation = reconcile_archive_publication_entries(
|
|
3201
|
+
knowledge,
|
|
3202
|
+
publications,
|
|
3203
|
+
)
|
|
2798
3204
|
existing: dict[str, Any] = {}
|
|
2799
3205
|
index_path = knowledge / "index.json"
|
|
2800
3206
|
if index_path.exists():
|
|
@@ -2820,6 +3226,20 @@ def refresh_outputs_from_entry_files(
|
|
|
2820
3226
|
entries.extend(entry for entry in file_entries if entry["id"] not in existing_ids)
|
|
2821
3227
|
else:
|
|
2822
3228
|
entries = file_entries
|
|
3229
|
+
effective_ingest_mode = dict(
|
|
3230
|
+
ingest_mode or existing.get("ingestMode") or {}
|
|
3231
|
+
)
|
|
3232
|
+
effective_ingest_mode["activeAutoDemoted"] = int(
|
|
3233
|
+
effective_ingest_mode.get("activeAutoDemoted") or 0
|
|
3234
|
+
) + publication_reconciliation["demoted"]
|
|
3235
|
+
effective_ingest_mode["entriesWritten"] = int(
|
|
3236
|
+
effective_ingest_mode.get("entriesWritten") or 0
|
|
3237
|
+
) + publication_reconciliation["written"]
|
|
3238
|
+
effective_ingest_mode["inputsHash"] = compute_inputs_hash_from_preserved(
|
|
3239
|
+
archive_records,
|
|
3240
|
+
load_config(knowledge),
|
|
3241
|
+
load_preserved_entries(knowledge),
|
|
3242
|
+
)
|
|
2823
3243
|
index = make_manifest(
|
|
2824
3244
|
project,
|
|
2825
3245
|
project_id(project),
|
|
@@ -2828,7 +3248,7 @@ def refresh_outputs_from_entry_files(
|
|
|
2828
3248
|
entries,
|
|
2829
3249
|
existing.get("failures") or [],
|
|
2830
3250
|
int(existing.get("duplicatesSkipped") or 0),
|
|
2831
|
-
|
|
3251
|
+
effective_ingest_mode,
|
|
2832
3252
|
)
|
|
2833
3253
|
write_sqlite(knowledge / "index.sqlite", entries)
|
|
2834
3254
|
write_json(index_path, index)
|
|
@@ -3212,8 +3632,7 @@ def sync_status(project: Path, update: bool = False, incremental: bool = True) -
|
|
|
3212
3632
|
knowledge = project / ".harness" / "knowledge"
|
|
3213
3633
|
index_path = knowledge / "index.json"
|
|
3214
3634
|
sqlite_path = knowledge / "index.sqlite"
|
|
3215
|
-
|
|
3216
|
-
summary_paths = sorted(archive_root.glob("*/reports/final/summary-data.json"))
|
|
3635
|
+
summary_paths = discover_archive_summary_paths(project)
|
|
3217
3636
|
current_records = archive_summary_records(project, summary_paths)
|
|
3218
3637
|
reasons: list[str] = []
|
|
3219
3638
|
index: dict[str, Any] | None = None
|