Skip to content

Commit 80fd43d

Browse files
committed
fix(toolkit): repair G5 and G6 contracts
1 parent 9a38267 commit 80fd43d

4 files changed

Lines changed: 134 additions & 4 deletions

File tree

scripts/work-bundle/control_plane.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,26 @@ def _publish_evidence(control: Path, payload: dict[str, object]) -> Path:
10831083
return path
10841084

10851085

1086+
def _control_plane_gitlink_paths(control: Path) -> list[str]:
1087+
paths = {
1088+
str(marker.parent.relative_to(control)).replace("\\", "/")
1089+
for marker in control.rglob(".git")
1090+
if marker.parent != control and (marker.is_dir() or marker.is_file())
1091+
}
1092+
if (control / ".git").exists():
1093+
result = subprocess.run(
1094+
["git", "-C", str(control), "ls-files", "--stage", "-z"],
1095+
check=False,
1096+
capture_output=True,
1097+
)
1098+
if result.returncode == 0:
1099+
for entry in result.stdout.split(b"\0"):
1100+
if not entry.startswith(b"160000 ") or b"\t" not in entry:
1101+
continue
1102+
paths.add(entry.split(b"\t", 1)[1].decode("utf-8", errors="surrogateescape"))
1103+
return sorted(paths)
1104+
1105+
10861106
def cmd_publish_control_plane(args: list[str]) -> int:
10871107
parser = argparse.ArgumentParser(prog="wb.py publish-control-plane")
10881108
parser.add_argument("workspace_root")
@@ -1109,6 +1129,16 @@ def cmd_publish_control_plane(args: list[str]) -> int:
11091129
if not remote:
11101130
out({"command": "publish-control-plane", "status": "issues-found", "failure_code": "WB_CONTROL_PLANE_REMOTE_REQUIRED", "changed_files": []})
11111131
return 1
1132+
gitlink_paths = _control_plane_gitlink_paths(control)
1133+
if gitlink_paths:
1134+
out({
1135+
"command": "publish-control-plane",
1136+
"status": "issues-found",
1137+
"failure_code": "WB_CONTROL_PLANE_GITLINK_FORBIDDEN",
1138+
"gitlink_paths": gitlink_paths,
1139+
"changed_files": [],
1140+
})
1141+
return 1
11121142
if parsed.dry_run:
11131143
out({"command": "publish-control-plane", "status": "passed", "dry_run": True, "remote": remote, "changed_files": [], "git_actions": ["init", "configure-origin", "commit", "push"]})
11141144
return 0

scripts/work-bundle/project.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1895,7 +1895,7 @@ def assess_legacy_topology(
18951895
)
18961896
registry_sources = registry_sources if isinstance(registry_sources, list) else []
18971897

1898-
def identities(sources: object) -> list[dict[str, str]]:
1898+
def identities(sources: object, identity_kind: str) -> list[dict[str, str]]:
18991899
result: list[dict[str, str]] = []
19001900
if not isinstance(sources, list):
19011901
return result
@@ -1906,11 +1906,14 @@ def identities(sources: object) -> list[dict[str, str]]:
19061906
result.append({
19071907
'id': str(source.get('id') or ''),
19081908
'path': str(Path(str(raw_path)).expanduser().resolve()) if raw_path else '',
1909+
'kind': identity_kind,
19091910
})
19101911
return result
19111912

1912-
metadata_identities = identities(metadata_sources)
1913-
registry_identities = identities([*registry_sources, *(registry_origin_data or [])])
1913+
metadata_identities = identities(metadata_sources, 'member')
1914+
registry_member_identities = identities(registry_sources, 'member')
1915+
registry_origin_identities = identities(registry_origin_data or [], 'origin')
1916+
registry_identities = [*registry_member_identities, *registry_origin_identities]
19141917
registry_identities = [
19151918
dict(identity)
19161919
for identity in {
@@ -1920,7 +1923,7 @@ def identities(sources: object) -> list[dict[str, str]]:
19201923
]
19211924
conflicts: list[str] = []
19221925
by_id: dict[str, set[str]] = {}
1923-
for identity in [*metadata_identities, *registry_identities]:
1926+
for identity in [*metadata_identities, *registry_member_identities]:
19241927
if identity['id'] and identity['path']:
19251928
by_id.setdefault(identity['id'], set()).add(identity['path'])
19261929
for source_id, paths in sorted(by_id.items()):

tests/test_control_plane_v4.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,6 +1550,49 @@ def test_publish_existing_dirty_control_plane_fails_closed_without_mutation(tmp_
15501550
assert evidence["recovery_required"] is True
15511551

15521552

1553+
def test_publish_rejects_nested_gitlink_before_staging(tmp_path: Path) -> None:
1554+
config = config_root(tmp_path / "config-root")
1555+
source_remote, _, _ = make_remote(tmp_path / "source-fixture", "source")
1556+
workspace = tmp_path / "workspace"
1557+
assert run_wb(
1558+
config,
1559+
"init-workspace",
1560+
str(workspace),
1561+
"--slug",
1562+
"demo",
1563+
"--repository",
1564+
f"source-main={source_remote}",
1565+
"--apply",
1566+
).returncode == 0
1567+
control = workspace / ".work-bundle"
1568+
nested = control / "knowledge/notes/nested-repository"
1569+
nested.mkdir(parents=True)
1570+
git(nested, "init", "-q", "-b", "main")
1571+
git(nested, "config", "user.email", "test@example.com")
1572+
git(nested, "config", "user.name", "Test")
1573+
(nested / "README.md").write_text("nested\n", encoding="utf-8")
1574+
git(nested, "add", "README.md")
1575+
git(nested, "commit", "-q", "-m", "nested")
1576+
remote = tmp_path / "control.git"
1577+
subprocess.run(["git", "init", "--bare", "-q", str(remote)], check=True)
1578+
1579+
failed = run_wb(
1580+
config,
1581+
"publish-control-plane",
1582+
str(workspace),
1583+
"--remote",
1584+
str(remote),
1585+
"--apply",
1586+
)
1587+
1588+
assert failed.returncode == 1
1589+
data = json.loads(failed.stdout)
1590+
assert data["failure_code"] == "WB_CONTROL_PLANE_GITLINK_FORBIDDEN"
1591+
assert data["gitlink_paths"] == ["knowledge/notes/nested-repository"]
1592+
assert not (control / ".git").exists()
1593+
assert (nested / ".git").is_dir()
1594+
1595+
15531596
def test_publish_failed_git_snapshot_fails_closed_without_mutation(tmp_path: Path) -> None:
15541597
config = config_root(tmp_path / "config-root")
15551598
source_remote, _, _ = make_remote(tmp_path / "source-fixture", "source")

tests/test_project_initialization.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,60 @@ def test_migrate_project_routes_registry_multi_source_to_workspace_migration(tmp
14401440
assert metadata_path.read_bytes() == before
14411441

14421442

1443+
def test_migrate_project_accepts_same_id_origin_and_member_paths(tmp_path: Path) -> None:
1444+
config_root, project = _init_fixture_project(tmp_path)
1445+
origin = tmp_path / "origin-locator"
1446+
origin.mkdir()
1447+
registry_path = config_root / "registry" / "projects.yaml"
1448+
registry_path.write_text(
1449+
"\n".join(
1450+
[
1451+
"projects:",
1452+
" - slug: demo",
1453+
" name: demo",
1454+
f" work_bundle_root: {project.resolve() / '.work-bundle'}",
1455+
f" knowledge_root: {project.resolve() / '.work-bundle' / 'knowledge'}",
1456+
" aliases: []",
1457+
" repository_origins:",
1458+
" - id: demo-main",
1459+
f" origin_path: {origin.resolve()}",
1460+
" git_repository: true",
1461+
" source_repositories:",
1462+
" - id: demo-main",
1463+
f" path: {project.resolve()}",
1464+
" work_dir: true",
1465+
' remote: ""',
1466+
" git_repository: true",
1467+
" status: active",
1468+
" updated_at: 2026-01-01",
1469+
"",
1470+
]
1471+
),
1472+
encoding="utf-8",
1473+
)
1474+
metadata_path = project / ".work-bundle/project.yaml"
1475+
metadata_path.write_text(
1476+
"\n".join(
1477+
[
1478+
"metadata_version: 1",
1479+
"authority: canonical",
1480+
f"project_root: {project.resolve()}",
1481+
"industry: legacy",
1482+
"",
1483+
]
1484+
),
1485+
encoding="utf-8",
1486+
)
1487+
1488+
migrated = run_wb(config_root, "migrate-project", str(project), "--dry-run", "--name", "demo")
1489+
1490+
assert migrated.returncode == 1
1491+
data = json.loads(migrated.stdout)
1492+
assert data["mode"] == "multi-repository-migration-required"
1493+
assert data["topology_assessment"]["conflicts"] == []
1494+
assert data["topology_assessment"]["required_command"] == "migrate-to-multi-repository"
1495+
1496+
14431497
def _seed_committed_repository(path: Path) -> None:
14441498
path.mkdir()
14451499
git(path, "init", "-q", "-b", "main")

0 commit comments

Comments
 (0)