feat: delegate project register; registrar-only ID minting
STATE-WP-0080-T02: statehub register routes project-flavor scaffolding through rmgr scaffold and keeps only repo + host-path registration. T01 refuse remains when GOAL.md is missing and --wp-prefix is not set. RMGR-WP-0005-T01: C-06/C-11/C-32 skip mint+writeback unless this instance is the identifier registrar (STATEHUB_REGISTRAR or railiance hostname).
This commit is contained in:
parent
71ad6c5d17
commit
d8e0eddb22
6 changed files with 419 additions and 25 deletions
|
|
@ -105,15 +105,82 @@ def refuse_project_flavor_scaffold(
|
|||
signal = detect_project_flavor_signal(project_path, repo_slug)
|
||||
if signal is None:
|
||||
return
|
||||
raise SystemExit(
|
||||
raise SystemExit(_project_scaffold_refusal_message(project_path, signal))
|
||||
|
||||
|
||||
def project_already_scaffolded(project_path: Path) -> bool:
|
||||
"""True when the project purpose document already exists."""
|
||||
return (project_path / "GOAL.md").is_file()
|
||||
|
||||
|
||||
def project_registration_plan(
|
||||
project_path: Path,
|
||||
repo_slug: str | None = None,
|
||||
wp_prefix: str | None = None,
|
||||
) -> tuple[str, str | None]:
|
||||
"""Decide how ``statehub register`` treats a checkout.
|
||||
|
||||
Returns one of:
|
||||
- ``("durable", None)`` — existing hub templating for non-project repos
|
||||
- ``("refuse", signal)`` — project flavor, not yet scaffolded, no prefix
|
||||
- ``("delegate", prefix)`` — call ``rmgr scaffold``, then register only
|
||||
- ``("register-only", None)`` — files already exist; hub records the repo
|
||||
"""
|
||||
signal = detect_project_flavor_signal(project_path, repo_slug)
|
||||
if signal is None:
|
||||
return "durable", None
|
||||
if project_already_scaffolded(project_path):
|
||||
return "register-only", None
|
||||
prefix = (wp_prefix or "").strip()
|
||||
if not prefix:
|
||||
return "refuse", signal
|
||||
return "delegate", prefix
|
||||
|
||||
|
||||
def _project_scaffold_refusal_message(project_path: Path, signal: str) -> str:
|
||||
return (
|
||||
"ERROR: statehub register refuses to scaffold a project-flavor repository.\n"
|
||||
f" Detected via: {signal}\n"
|
||||
" Durable-repo registration is unchanged. Scaffold project repos with Repo Manager:\n"
|
||||
f" rmgr scaffold --path {project_path} --flavor project --wp-prefix <PREFIX>-WP\n"
|
||||
" Then re-run ``statehub register`` (or pass --wp-prefix now to delegate).\n"
|
||||
" Choose --wp-prefix from the project identity, never PRJ-WP-."
|
||||
)
|
||||
|
||||
|
||||
def delegate_project_scaffold(
|
||||
project_path: Path,
|
||||
*,
|
||||
slug: str,
|
||||
wp_prefix: str,
|
||||
domain: str = "infotech",
|
||||
force: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Call ``rmgr scaffold`` via the dual-run adapter (STATE-WP-0080-T02)."""
|
||||
from api.services.repo_manager_dual_run import record_mutation, rm_scaffold
|
||||
|
||||
result = rm_scaffold(
|
||||
repo_path=project_path,
|
||||
flavor="project",
|
||||
wp_prefix=wp_prefix,
|
||||
slug=slug,
|
||||
domain=domain,
|
||||
force=force,
|
||||
commit=False,
|
||||
)
|
||||
record_mutation(
|
||||
source="state-hub-register",
|
||||
kind="project_scaffold_delegated",
|
||||
repo_slug=slug,
|
||||
detail={
|
||||
"status": result.get("status"),
|
||||
"wp_prefix": wp_prefix,
|
||||
"exit_code": result.get("exit_code"),
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _classification_category(project_path: Path) -> str | None:
|
||||
doc = load_classification_document(project_path / CLASSIFICATION_FILENAME)
|
||||
block = extract_classification_block(doc)
|
||||
|
|
@ -146,7 +213,12 @@ def run_register(args: argparse.Namespace) -> None:
|
|||
print(f"ERROR: {project_path} is not a directory.")
|
||||
sys.exit(1)
|
||||
|
||||
refuse_project_flavor_scaffold(project_path, args.repo_slug)
|
||||
explicit_prefix = getattr(args, "wp_prefix", None)
|
||||
plan, _ = project_registration_plan(
|
||||
project_path, getattr(args, "repo_slug", None), explicit_prefix
|
||||
)
|
||||
if plan == "refuse":
|
||||
refuse_project_flavor_scaffold(project_path, getattr(args, "repo_slug", None))
|
||||
|
||||
snapshot = collect_repo_snapshot(project_path)
|
||||
print(f"==> Inspecting repo at {snapshot.path}")
|
||||
|
|
@ -157,8 +229,16 @@ def run_register(args: argparse.Namespace) -> None:
|
|||
|
||||
inference = infer_registration(snapshot, args, domain_slugs)
|
||||
repo_slug = args.repo_slug or inference.repo_slug or _slugify(snapshot.project_name)
|
||||
refuse_project_flavor_scaffold(project_path, repo_slug)
|
||||
wp_prefix = args.wp_prefix or inference.workplan_prefix or _default_wp_prefix(repo_slug)
|
||||
plan, delegated_prefix = project_registration_plan(
|
||||
project_path, repo_slug, explicit_prefix
|
||||
)
|
||||
if plan == "refuse":
|
||||
refuse_project_flavor_scaffold(project_path, repo_slug)
|
||||
wp_prefix = (
|
||||
explicit_prefix
|
||||
or inference.workplan_prefix
|
||||
or _default_wp_prefix(repo_slug)
|
||||
)
|
||||
domain = args.domain or inference.domain_slug or _detect_domain_from_files(snapshot)
|
||||
project_description = (
|
||||
args.description
|
||||
|
|
@ -170,29 +250,50 @@ def run_register(args: argparse.Namespace) -> None:
|
|||
if domain not in domain_slugs:
|
||||
domain = _ask_for_domain(domain, domain_slugs)
|
||||
|
||||
intent_markdown = _resolve_intent_markdown(snapshot, inference, args, project_description)
|
||||
topic = _find_or_create_topic(domain, snapshot.project_name, repo_slug, inference, args.api_base)
|
||||
topic_id = topic["id"]
|
||||
topic_slug = topic.get("slug") or domain
|
||||
|
||||
print(f"==> Writing State Hub agent files for '{repo_slug}'")
|
||||
written = write_registration_files(
|
||||
project_path=project_path,
|
||||
project_name=snapshot.project_name,
|
||||
project_description=project_description,
|
||||
domain=domain,
|
||||
topic_id=topic_id,
|
||||
topic_slug=topic_slug,
|
||||
repo_slug=repo_slug,
|
||||
wp_prefix=wp_prefix,
|
||||
intent_markdown=intent_markdown,
|
||||
inference=inference,
|
||||
force=args.force,
|
||||
)
|
||||
for path in written:
|
||||
print(f" wrote {path}")
|
||||
if not written:
|
||||
print(" files already present; nothing overwritten")
|
||||
skip_hub_templating = plan in {"delegate", "register-only"}
|
||||
if plan == "delegate":
|
||||
print(f"==> Delegating project scaffold to rmgr (prefix {delegated_prefix})")
|
||||
result = delegate_project_scaffold(
|
||||
project_path,
|
||||
slug=repo_slug,
|
||||
wp_prefix=delegated_prefix or wp_prefix,
|
||||
domain=domain,
|
||||
force=bool(getattr(args, "force", False)),
|
||||
)
|
||||
status = result.get("status")
|
||||
if status != "applied" and not project_already_scaffolded(project_path):
|
||||
err = (result.get("error") or {}).get("message") or result
|
||||
raise SystemExit(f"ERROR: rmgr scaffold failed: {err}")
|
||||
print(f" rmgr scaffold status={status}")
|
||||
elif plan == "register-only":
|
||||
print("==> Project files already present; skipping hub templating")
|
||||
|
||||
if not skip_hub_templating:
|
||||
intent_markdown = _resolve_intent_markdown(
|
||||
snapshot, inference, args, project_description
|
||||
)
|
||||
print(f"==> Writing State Hub agent files for '{repo_slug}'")
|
||||
written = write_registration_files(
|
||||
project_path=project_path,
|
||||
project_name=snapshot.project_name,
|
||||
project_description=project_description,
|
||||
domain=domain,
|
||||
topic_id=topic_id,
|
||||
topic_slug=topic_slug,
|
||||
repo_slug=repo_slug,
|
||||
wp_prefix=wp_prefix,
|
||||
intent_markdown=intent_markdown,
|
||||
inference=inference,
|
||||
force=args.force,
|
||||
)
|
||||
for path in written:
|
||||
print(f" wrote {path}")
|
||||
if not written:
|
||||
print(" files already present; nothing overwritten")
|
||||
|
||||
repo = _register_or_update_repo(
|
||||
domain=domain,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue