feat(consistency): bootstrap empty repo projections
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
parent
52a7d6bb73
commit
03c7924b7f
3 changed files with 187 additions and 7 deletions
|
|
@ -402,6 +402,8 @@ def cmd_fix_consistency(args: argparse.Namespace) -> None:
|
|||
cmd.append("--remote")
|
||||
if args.no_writeback:
|
||||
cmd.append("--no-writeback")
|
||||
if getattr(args, "bootstrap_empty_projection", False):
|
||||
cmd.append("--bootstrap-empty-projection")
|
||||
if args.archive_closed:
|
||||
cmd.append("--archive-closed")
|
||||
if args.archive_workplan:
|
||||
|
|
@ -720,6 +722,11 @@ def main() -> None:
|
|||
fix.add_argument("--remote", action="store_true", help="Pull before fixing; requires --repo or --all")
|
||||
fix.add_argument("--max-seconds", type=int, default=None, help="Wall-clock budget for --remote --all")
|
||||
fix.add_argument("--no-writeback", action="store_true", help="Disable DB-to-file status writeback")
|
||||
fix.add_argument(
|
||||
"--bootstrap-empty-projection",
|
||||
action="store_true",
|
||||
help="Rebuild file UUIDs only after proving the repo projection is empty",
|
||||
)
|
||||
fix.add_argument("--archive-closed", action="store_true", help="Archive closed root workplans after fixing")
|
||||
fix.add_argument("--archive-workplan", default=None, help="Archive only the matching workplan id or filename")
|
||||
fix.add_argument("--archive-date", default=None, help="YYMMDD archive prefix for --archive-closed")
|
||||
|
|
|
|||
|
|
@ -1154,7 +1154,12 @@ def _check_work_record_index_freshness(
|
|||
)
|
||||
|
||||
|
||||
def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = None) -> ConsistencyReport:
|
||||
def check_repo(
|
||||
api_base: str,
|
||||
repo_slug: str,
|
||||
repo_path_override: str | None = None,
|
||||
bootstrap_empty_projection: bool = False,
|
||||
) -> ConsistencyReport:
|
||||
"""Run all consistency checks for a registered repo."""
|
||||
repo = _api_get(api_base, f"/repos/{repo_slug}", return_error=True)
|
||||
if isinstance(repo, dict) and "_error" in repo:
|
||||
|
|
@ -1180,6 +1185,28 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||
repo_path: str = resolve_repo_path(repo, repo_path_override)
|
||||
report = ConsistencyReport(repo_slug=repo_slug, repo_path=repo_path)
|
||||
|
||||
if bootstrap_empty_projection:
|
||||
projected = _api_get(api_base, "/workplans", {"repo_id": repo_id}, return_error=True)
|
||||
if not isinstance(projected, list):
|
||||
report.add(
|
||||
severity="FAIL",
|
||||
check_id="C-36",
|
||||
message="Could not prove that the repository projection is empty",
|
||||
fixable=False,
|
||||
)
|
||||
return report
|
||||
if projected:
|
||||
report.add(
|
||||
severity="FAIL",
|
||||
check_id="C-36",
|
||||
message=(
|
||||
"Empty-projection bootstrap refused: repository already has "
|
||||
f"{len(projected)} projected workplan(s)"
|
||||
),
|
||||
fixable=False,
|
||||
)
|
||||
return report
|
||||
|
||||
if not repo_path:
|
||||
report.add(
|
||||
severity="FAIL", check_id="C-00",
|
||||
|
|
@ -1328,14 +1355,21 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||
ws = _api_get(api_base, f"/workplans/{ws_id}")
|
||||
if ws is None:
|
||||
wp_id = str(meta.get("id", "")).strip()
|
||||
if wp_id and ws_id == _derived_work_record_uuid(wp_id):
|
||||
if wp_id and (
|
||||
ws_id == _derived_work_record_uuid(wp_id) or bootstrap_empty_projection
|
||||
):
|
||||
# A deterministic file identifier missing from a replaceable
|
||||
# projection is registration work, not a stale reference.
|
||||
report.add(
|
||||
severity="WARN",
|
||||
check_id="C-06",
|
||||
message=(
|
||||
f"Derived workplan {ws_id[:8]}… is absent from this projection"
|
||||
f"Authoritative workplan {ws_id[:8]}… is absent from this "
|
||||
+ (
|
||||
"explicitly empty projection"
|
||||
if bootstrap_empty_projection
|
||||
else "projection"
|
||||
)
|
||||
),
|
||||
file_path=fname,
|
||||
db_id=ws_id,
|
||||
|
|
@ -2755,10 +2789,16 @@ def fix_repo(
|
|||
repo_slug: str,
|
||||
repo_path_override: str | None = None,
|
||||
no_writeback: bool = False,
|
||||
bootstrap_empty_projection: bool = False,
|
||||
) -> ConsistencyReport:
|
||||
"""Run checks then apply all auto-fixable issues. Returns updated report."""
|
||||
report = check_repo(api_base, repo_slug, repo_path_override)
|
||||
if any(i.check_id == "C-00" for i in report.failures):
|
||||
report = check_repo(
|
||||
api_base,
|
||||
repo_slug,
|
||||
repo_path_override,
|
||||
bootstrap_empty_projection=bootstrap_empty_projection,
|
||||
)
|
||||
if any(i.check_id in {"C-00", "C-36"} for i in report.failures):
|
||||
return report
|
||||
|
||||
# RMGR-WP-0002: optional repo-manager reconcile proxy (index rebuild) for pilots
|
||||
|
|
@ -3675,6 +3715,14 @@ def main() -> None:
|
|||
f"(default: {DEFAULT_REMOTE_ALL_MAX_SECONDS}; 0 disables)")
|
||||
parser.add_argument("--no-writeback", action="store_true", dest="no_writeback",
|
||||
help="Disable DB→file status writeback (C-15) while keeping other fixes")
|
||||
parser.add_argument(
|
||||
"--bootstrap-empty-projection",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Rebuild authoritative legacy UUIDs only after proving the target "
|
||||
"repository has zero projected workplans"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--archive-closed", action="store_true",
|
||||
help="Move closed root workplans to workplans/archived/YYMMDD-*.md")
|
||||
parser.add_argument("--archive-workplan", metavar="ID_OR_FILE", default=None,
|
||||
|
|
@ -3712,6 +3760,11 @@ def main() -> None:
|
|||
import os as _os
|
||||
no_wb = getattr(args, "no_writeback", False)
|
||||
do_fix = args.fix or args.remote
|
||||
if args.bootstrap_empty_projection and (not do_fix or args.all or args.remote):
|
||||
parser.error(
|
||||
"--bootstrap-empty-projection requires --fix for one local repository "
|
||||
"and cannot be combined with --all or --remote"
|
||||
)
|
||||
|
||||
# --here: infer slug from git remote URL, then run as single-repo check/fix
|
||||
if args.here is not None:
|
||||
|
|
@ -3730,7 +3783,15 @@ def main() -> None:
|
|||
inferred_slug, git_root = inferred
|
||||
print(f" Detected: {inferred_slug} ({git_root})")
|
||||
if do_fix:
|
||||
reports = [fix_repo(args.api_base, inferred_slug, git_root, no_writeback=no_wb)]
|
||||
reports = [
|
||||
fix_repo(
|
||||
args.api_base,
|
||||
inferred_slug,
|
||||
git_root,
|
||||
no_writeback=no_wb,
|
||||
bootstrap_empty_projection=args.bootstrap_empty_projection,
|
||||
)
|
||||
]
|
||||
else:
|
||||
reports = [check_repo(args.api_base, inferred_slug, git_root)]
|
||||
if args.archive_closed:
|
||||
|
|
@ -3792,7 +3853,13 @@ def main() -> None:
|
|||
reports = [report]
|
||||
elif do_fix:
|
||||
reports = [
|
||||
fix_repo(args.api_base, slug, path_override, no_writeback=no_wb)
|
||||
fix_repo(
|
||||
args.api_base,
|
||||
slug,
|
||||
path_override,
|
||||
no_writeback=no_wb,
|
||||
bootstrap_empty_projection=args.bootstrap_empty_projection,
|
||||
)
|
||||
for slug in repo_slugs
|
||||
]
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1272,6 +1272,112 @@ class TestC20DependencyDetection:
|
|||
|
||||
|
||||
class TestC06WorkstreamCreation:
|
||||
def test_fix_repo_bootstraps_legacy_ids_only_into_empty_projection(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
repo = tmp_path / "repo"
|
||||
workplans = repo / "workplans"
|
||||
workplans.mkdir(parents=True)
|
||||
workplan_id = "11111111-1111-4111-8111-111111111111"
|
||||
task_id = "22222222-2222-4222-8222-222222222222"
|
||||
wp = workplans / "DEMO-WP-0001.md"
|
||||
wp.write_text(
|
||||
"---\n"
|
||||
"id: DEMO-WP-0001\n"
|
||||
"type: workplan\n"
|
||||
"title: Demo Workplan\n"
|
||||
"domain: financials\n"
|
||||
"repo: demo-repo\n"
|
||||
"status: ready\n"
|
||||
f'state_hub_workstream_id: "{workplan_id}"\n'
|
||||
"---\n\n"
|
||||
"## Task\n\n"
|
||||
"```task\n"
|
||||
"id: DEMO-WP-0001-T01\n"
|
||||
"status: todo\n"
|
||||
"priority: high\n"
|
||||
f'state_hub_task_id: "{task_id}"\n'
|
||||
"```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
created = []
|
||||
|
||||
def fake_get(_api_base, path, params=None, **_kwargs):
|
||||
if path == "/repos/demo-repo":
|
||||
return {
|
||||
"id": "repo-1",
|
||||
"slug": "demo-repo",
|
||||
"local_path": str(repo),
|
||||
"host_paths": {},
|
||||
"domain_slug": "financials",
|
||||
}
|
||||
if path == "/topics":
|
||||
return [{"id": "topic-1", "domain_slug": "financials"}]
|
||||
if path == "/workplans" and params == {"repo_id": "repo-1"}:
|
||||
return []
|
||||
if path == f"/workplans/{workplan_id}":
|
||||
return None
|
||||
if path == "/workplans" and params and "slug" in params:
|
||||
return []
|
||||
return []
|
||||
|
||||
def fake_post(_api_base, path, body):
|
||||
created.append((path, body))
|
||||
return body
|
||||
|
||||
monkeypatch.setattr("consistency_check._api_get", fake_get)
|
||||
monkeypatch.setattr("consistency_check._api_post", fake_post)
|
||||
monkeypatch.setattr("consistency_check._api_patch", lambda *args, **kwargs: {"ok": True})
|
||||
monkeypatch.setattr("consistency_check._detect_behind_remote", lambda _repo_path: False)
|
||||
monkeypatch.setattr("consistency_check._detect_ahead_of_remote", lambda _repo_path: 0)
|
||||
monkeypatch.setattr("consistency_check._write_custodian_brief", lambda *args, **kwargs: False)
|
||||
monkeypatch.setattr("consistency_check._git_push", lambda _repo_path: (True, "pushed"))
|
||||
monkeypatch.setenv("STATEHUB_REGISTRAR", "1")
|
||||
|
||||
report = fix_repo(
|
||||
"http://unused",
|
||||
"demo-repo",
|
||||
bootstrap_empty_projection=True,
|
||||
)
|
||||
|
||||
assert [body["id"] for path, body in created if path == "/workplans"] == [workplan_id]
|
||||
assert [body["id"] for path, body in created if path == "/tasks"] == [task_id]
|
||||
assert any("explicitly empty projection" in issue.message for issue in report.issues)
|
||||
|
||||
def test_fix_repo_refuses_empty_bootstrap_when_projection_has_rows(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
repo = tmp_path / "repo"
|
||||
(repo / "workplans").mkdir(parents=True)
|
||||
|
||||
def fake_get(_api_base, path, params=None, **_kwargs):
|
||||
if path == "/repos/demo-repo":
|
||||
return {
|
||||
"id": "repo-1",
|
||||
"slug": "demo-repo",
|
||||
"local_path": str(repo),
|
||||
"host_paths": {},
|
||||
"domain_slug": "financials",
|
||||
}
|
||||
if path == "/workplans" and params == {"repo_id": "repo-1"}:
|
||||
return [{"id": "existing"}]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("consistency_check._api_get", fake_get)
|
||||
monkeypatch.setattr(
|
||||
"consistency_check._api_post",
|
||||
lambda *_args, **_kwargs: pytest.fail("bootstrap must not mutate a non-empty projection"),
|
||||
)
|
||||
|
||||
report = fix_repo(
|
||||
"http://unused",
|
||||
"demo-repo",
|
||||
bootstrap_empty_projection=True,
|
||||
)
|
||||
|
||||
assert len(report.failures) == 1
|
||||
assert report.failures[0].check_id == "C-36"
|
||||
|
||||
def test_fix_repo_rebuilds_missing_projection_from_derived_file_ids(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue