fix: refuse to scaffold prj- repos from statehub register
STATE-WP-0080-T01: detect project flavor from classification, GOAL.md, or a prj- slug, then exit pointing at rmgr scaffold. Durable-repo write path is unchanged. Rebind 0080 hub IDs to the live workstream and open T02 now that RMGR-WP-0004-T03 has landed.
This commit is contained in:
parent
131b824dd3
commit
f564e99a14
4 changed files with 216 additions and 12 deletions
|
|
@ -15,11 +15,18 @@ from datetime import date
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
STATE_HUB_DIR = Path(__file__).resolve().parent
|
||||
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000")
|
||||
RULES_TEMPLATES_DIR = STATE_HUB_DIR / "scripts" / "project_rules"
|
||||
|
||||
from api.classification import ( # noqa: E402
|
||||
CLASSIFICATION_FILENAME,
|
||||
extract_classification_block,
|
||||
load_classification_document,
|
||||
)
|
||||
from scripts.ensure_gitignore_claude_rules import ensure_claude_gitignore # noqa: E402
|
||||
|
||||
KEY_CONTEXT_FILES = [
|
||||
|
|
@ -60,12 +67,87 @@ class RegisterInference:
|
|||
current_state: str | None = None
|
||||
|
||||
|
||||
_FRONTMATTER_RE = re.compile(r"\A---\s*\n(.*?)\n---", re.DOTALL)
|
||||
|
||||
|
||||
def detect_project_flavor_signal(
|
||||
project_path: Path,
|
||||
repo_slug: str | None = None,
|
||||
) -> str | None:
|
||||
"""Return the first project-flavor signal, or None for durable repos.
|
||||
|
||||
Precedence matches repo-manager: classification category, GOAL.md
|
||||
``repo_flavor``, then a ``prj-`` slug prefix. Any match is enough to
|
||||
refuse scaffolding here (STATE-WP-0080-T01).
|
||||
"""
|
||||
category = _classification_category(project_path)
|
||||
if category == "project":
|
||||
return f"{CLASSIFICATION_FILENAME} category=project"
|
||||
|
||||
goal_flavor = _goal_repo_flavor(project_path)
|
||||
if goal_flavor == "project":
|
||||
return "GOAL.md repo_flavor=project"
|
||||
|
||||
for candidate in (repo_slug, project_path.name):
|
||||
if not candidate:
|
||||
continue
|
||||
slug = _slugify(candidate)
|
||||
if slug.startswith("prj-"):
|
||||
return f"slug prefix prj- ({slug})"
|
||||
return None
|
||||
|
||||
|
||||
def refuse_project_flavor_scaffold(
|
||||
project_path: Path,
|
||||
repo_slug: str | None = None,
|
||||
) -> None:
|
||||
"""Exit before writing INTENT.md / PRJ-WP- files into a project repo."""
|
||||
signal = detect_project_flavor_signal(project_path, repo_slug)
|
||||
if signal is None:
|
||||
return
|
||||
raise SystemExit(
|
||||
"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"
|
||||
" Choose --wp-prefix from the project identity, never PRJ-WP-."
|
||||
)
|
||||
|
||||
|
||||
def _classification_category(project_path: Path) -> str | None:
|
||||
doc = load_classification_document(project_path / CLASSIFICATION_FILENAME)
|
||||
block = extract_classification_block(doc)
|
||||
if not block:
|
||||
return None
|
||||
raw = block.get("category")
|
||||
return str(raw).strip().lower() if raw else None
|
||||
|
||||
|
||||
def _goal_repo_flavor(project_path: Path) -> str | None:
|
||||
path = project_path / "GOAL.md"
|
||||
if not path.is_file():
|
||||
return None
|
||||
match = _FRONTMATTER_RE.match(_read_limited(path, 4000))
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
frontmatter = yaml.safe_load(match.group(1)) or {}
|
||||
except yaml.YAMLError:
|
||||
return None
|
||||
if not isinstance(frontmatter, dict):
|
||||
return None
|
||||
raw = frontmatter.get("repo_flavor")
|
||||
return str(raw).strip().lower() if raw else None
|
||||
|
||||
|
||||
def run_register(args: argparse.Namespace) -> None:
|
||||
project_path = Path(args.path).expanduser().resolve()
|
||||
if not project_path.is_dir():
|
||||
print(f"ERROR: {project_path} is not a directory.")
|
||||
sys.exit(1)
|
||||
|
||||
refuse_project_flavor_scaffold(project_path, args.repo_slug)
|
||||
|
||||
snapshot = collect_repo_snapshot(project_path)
|
||||
print(f"==> Inspecting repo at {snapshot.path}")
|
||||
|
||||
|
|
@ -75,6 +157,7 @@ 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)
|
||||
domain = args.domain or inference.domain_slug or _detect_domain_from_files(snapshot)
|
||||
project_description = (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue