From 04145a59d9a7d3fba14874a975a9cd9588740d99 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 13:28:40 +0200 Subject: [PATCH] feat: model repo standards and detect prefix collisions Implement RMGR-WP-0004 T01 (flavor, required files, anti-patterns from canon) and T08 (prefix ownership registry plus uniqueness scan). rmgr conform and rmgr prefix-uniqueness are detection only. --- README.md | 3 + config/workplan-prefix-registry.yaml | 53 ++++ docs/repository-standards_v0.1.md | 35 +++ src/repo_manager/cli.py | 26 ++ src/repo_manager/prefix_registry.py | 112 +++++++ src/repo_manager/standards.py | 279 ++++++++++++++++++ tests/test_standards.py | 112 +++++++ ...P-0004-repository-standards-conformance.md | 24 +- 8 files changed, 640 insertions(+), 4 deletions(-) create mode 100644 config/workplan-prefix-registry.yaml create mode 100644 docs/repository-standards_v0.1.md create mode 100644 src/repo_manager/prefix_registry.py create mode 100644 src/repo_manager/standards.py create mode 100644 tests/test_standards.py diff --git a/README.md b/README.md index 64d1c02..a93698d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Architecture: - [State Hub extraction inventory v0.1](docs/state-hub-extraction-inventory_v0.1.md) - [ADR-001 Implementation foundation](docs/adr-001-implementation-foundation.md) - [Railiance app deployment guide](docs/RailianceAppDeploymentGuide.md) (`RMGR-WP-0006`) +- [Repository standards v0.1](docs/repository-standards_v0.1.md) (`RMGR-WP-0004`) ```bash make install # or: uv pip install -e ".[dev]" @@ -37,6 +38,8 @@ rmgr rapp wrap --path ../rapp-some-app --app some-app \ --ownership-repo some-app --from-app ../some-app rmgr rapp validate --path ../rapp-user-engine --family-root .. rmgr rapp place --path ../rapp-some-app --reef reef-railiance +rmgr conform --path . +rmgr prefix-uniqueness --root .. ``` Vertical-slice proof: [docs/evidence/t05-vertical-slice.md](docs/evidence/t05-vertical-slice.md). diff --git a/config/workplan-prefix-registry.yaml b/config/workplan-prefix-registry.yaml new file mode 100644 index 0000000..0a52d7b --- /dev/null +++ b/config/workplan-prefix-registry.yaml @@ -0,0 +1,53 @@ +# RMGR-WP-0004-T08 / ADR-007 decision 1 +# One prefix, one repository. Prefixes derive from repo or project identity, +# never from a flavor marker or family name. Detection only — historical +# collisions stay until ADR-007 § Migration is ruled. + +standard: workplan-prefix-ownership +version: "0.1" +canon: + - the-custodian/canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md + - the-custodian/canon/standards/project-repository-flavor_v0.1.md + +retired: + - prefix: PRJ-WP + reason: flavor marker; never a valid prefix + - prefix: RAIL-BS-WP + reason: shared family name, not a repository + successors: [RCLUSTER-WP, RBS-WP] + - prefix: RAILIANCE-WP + reason: family name, not a repository + successors: [RPF-WP, RAPPS-WP, RFORGE-WP, RTELE-WP] + +owners: + RMGR-WP: repo-manager + CUST-WP: the-custodian + STATE-WP: state-hub + CFED-WP: prj-canon-federation + SHR-WP: prj-state-hub-retirement + RCLUSTER-WP: railiance-cluster + RBS-WP: railiance-bootstrap + RPF-WP: railiance-platform + RAPPS-WP: railiance-apps + RFORGE-WP: railiance-forge + RTELE-WP: railiance-telemetry + RMASTER-WP: railiance-master + RAIL-K8S-WP: rail-kubernetes + RAPP-OPENBAO-WP: rapp-openbao + RAPP-USER-ENGINE-WP: rapp-user-engine + RAPP-TENANT-ENGINE-WP: rapp-tenant-engine + RAPP-POLICY-NEXUS-WP: rapp-policy-nexus + USER-WP: user-engine + TEN-WP: tenant-engine + +historical_collisions: + - id: RAILIANCE-WP-0015 + repos: [railiance-platform, railiance-apps] + - id: RAILIANCE-WP-0016 + repos: [railiance-platform, railiance-apps] + - id: CUST-WP-0000 + repos: [the-custodian] + - id: CUST-WP-0010 + repos: [the-custodian] + - id: CUST-WP-0045 + repos: [the-custodian] diff --git a/docs/repository-standards_v0.1.md b/docs/repository-standards_v0.1.md new file mode 100644 index 0000000..c6a1f7d --- /dev/null +++ b/docs/repository-standards_v0.1.md @@ -0,0 +1,35 @@ +# Repository standards v0.1 + +Repo Manager **implements** Custodian canon. It does not author a second +definition. This note is an index. + +| Concern | Canon | +| --- | --- | +| Categories and `.repo-classification.yaml` | `the-custodian/canon/standards/repo-classification-standard_v1.0.md` | +| `prj-` layout, `GOAL.md`, anti-pattern both purpose docs | `the-custodian/canon/standards/project-repository-flavor_v0.1.md` | +| One workplan prefix per repository | `the-custodian/canon/architecture/adr-007-workplan-identity-and-repo-worker-topology.md` | + +## Flavor resolution + +Precedence, then warn on disagreement (do not silently pick a winner for +operators — the first match is used only to choose the required-file set): + +1. `.repo-classification.yaml` `category` +2. `GOAL.md` `repo_flavor` +3. slug prefix `prj-` + +`prj-` layout (`GOAL.md`, no `INTENT.md`) applies when the slug starts +with `prj-` or `GOAL.md` declares `repo_flavor: project`. Not every +`category: project` repo is a `prj-` repo. + +## Commands + +```bash +rmgr conform --path . +rmgr prefix-uniqueness --root .. +``` + +`prefix-uniqueness` is detection only. It does not rename files. +Registry: [`config/workplan-prefix-registry.yaml`](../config/workplan-prefix-registry.yaml). + +Work: `RMGR-WP-0004-T01`, `RMGR-WP-0004-T08`. diff --git a/src/repo_manager/cli.py b/src/repo_manager/cli.py index cbef411..a4a464b 100644 --- a/src/repo_manager/cli.py +++ b/src/repo_manager/cli.py @@ -64,6 +64,17 @@ def main(argv: list[str] | None = None) -> int: add_rapp_parser(sub) + p_conf = sub.add_parser("conform", help="Check a repository against flavor standards") + p_conf.add_argument("--path", default=".", help="Repository checkout path") + p_conf.add_argument("--slug", default=None) + + p_pref = sub.add_parser( + "prefix-uniqueness", + help="Detect shared workplan prefixes and reused identifiers (ADR-007)", + ) + p_pref.add_argument("--root", default=".", help="Fleet root or a single repository") + p_pref.add_argument("--registry", default=None, help="Override prefix registry YAML") + args = parser.parse_args(argv) if args.version or args.command in (None, "version"): @@ -188,6 +199,21 @@ def main(argv: list[str] | None = None) -> int: print(json.dumps(result, indent=2)) return 0 if result.get("ok") else 1 + if args.command == "conform": + from repo_manager.standards import check_repository + + report = check_repository(Path(args.path), slug=args.slug) + print(json.dumps(report.to_dict(), indent=2)) + return 0 if report.ok else 1 + + if args.command == "prefix-uniqueness": + from repo_manager.prefix_registry import scan_prefixes + + registry = Path(args.registry) if args.registry else None + report = scan_prefixes(Path(args.root), registry_path=registry) + print(json.dumps(report, indent=2)) + return 0 if report.get("ok") else 1 + parser.print_help() return 0 diff --git a/src/repo_manager/prefix_registry.py b/src/repo_manager/prefix_registry.py new file mode 100644 index 0000000..20568ef --- /dev/null +++ b/src/repo_manager/prefix_registry.py @@ -0,0 +1,112 @@ +"""Fleet workplan-prefix ownership and uniqueness (RMGR-WP-0004-T08).""" + +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +from typing import Any + +import yaml + +from repo_manager.standards import FLAVOR_MARKER_PREFIX, workplan_ids + +DEFAULT_REGISTRY = Path(__file__).resolve().parents[2] / "config" / "workplan-prefix-registry.yaml" + + +def load_registry(path: Path | None = None) -> dict[str, Any]: + target = path or DEFAULT_REGISTRY + data = yaml.safe_load(target.read_text(encoding="utf-8")) or {} + if not isinstance(data, dict): + return {} + return data + + +def _skip_dir(name: str) -> bool: + return name.startswith(".") or name in { + "node_modules", + "__pycache__", + ".venv", + "venv", + "archive", + } + + +def iter_repo_roots(root: Path) -> list[Path]: + root = root.resolve() + if (root / "workplans").is_dir() and (root / ".git").exists(): + return [root] + found: list[Path] = [] + if not root.is_dir(): + return found + for child in sorted(root.iterdir()): + if not child.is_dir() or _skip_dir(child.name): + continue + if (child / "workplans").is_dir(): + found.append(child) + return found + + +def scan_prefixes(root: Path, *, registry_path: Path | None = None) -> dict[str, Any]: + """Detect prefix sharing, identifier reuse, flavor-derived prefixes, number reuse. + + Detection only. Does not rewrite files or remediations. + """ + registry = load_registry(registry_path) + owners = {str(k): str(v) for k, v in (registry.get("owners") or {}).items()} + retired = {str(item.get("prefix")) for item in registry.get("retired") or [] if item.get("prefix")} + + by_prefix: dict[str, set[str]] = defaultdict(set) + by_id: dict[str, list[tuple[str, str]]] = defaultdict(list) + numbers: dict[tuple[str, str], list[str]] = defaultdict(list) + + repos_scanned = [] + for repo in iter_repo_roots(root): + repos_scanned.append(repo.name) + for rel, prefix, number in workplan_ids(repo): + by_prefix[prefix].add(repo.name) + ident = f"{prefix}-{number}" + by_id[ident].append((repo.name, rel)) + numbers[(repo.name, prefix)].append(number) + + shared = { + prefix: sorted(repos) + for prefix, repos in sorted(by_prefix.items()) + if len(repos) > 1 + } + reused = { + ident: [{"repo": repo, "path": path} for repo, path in entries] + for ident, entries in sorted(by_id.items()) + if len({repo for repo, _path in entries}) > 1 or len(entries) > 1 + } + flavor_derived = sorted(repo for repo in by_prefix.get(FLAVOR_MARKER_PREFIX, [])) + retired_in_use = { + prefix: sorted(repos) + for prefix, repos in by_prefix.items() + if prefix in retired + } + owner_mismatch = { + prefix: {"declared_owner": owners[prefix], "seen_in": sorted(repos)} + for prefix, repos in by_prefix.items() + if prefix in owners and any(repo != owners[prefix] for repo in repos) + } + + reused_numbers = [] + for (repo, prefix), nums in numbers.items(): + seen: set[str] = set() + for num in nums: + if num in seen: + reused_numbers.append({"repo": repo, "prefix": prefix, "number": num}) + seen.add(num) + + return { + "ok": not shared and not flavor_derived and not reused_numbers and not reused, + "root": str(root.resolve()), + "repos_scanned": repos_scanned, + "shared_prefixes": shared, + "reused_identifiers": reused, + "flavor_derived_prefix": flavor_derived, + "retired_in_use": retired_in_use, + "owner_mismatch": owner_mismatch, + "reused_numbers_in_repo": reused_numbers, + "detection_only": True, + } diff --git a/src/repo_manager/standards.py b/src/repo_manager/standards.py new file mode 100644 index 0000000..82b57b4 --- /dev/null +++ b/src/repo_manager/standards.py @@ -0,0 +1,279 @@ +"""Repository standards as Repo Manager implements Custodian canon. + +Canon remains authoritative. This module does not fork the rules; it names +the signals, required files, and anti-patterns the canon already states. + +Sources: +- the-custodian/canon/standards/project-repository-flavor_v0.1.md +- the-custodian/canon/standards/repo-classification-standard_v1.0.md +- ADR-007 decision 1 (prefix from repo/project identity, never flavor) +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Literal + +import yaml + +from repo_manager.observe import load_classification +from repo_manager.parse.workplan import parse_frontmatter + +CANON_FLAVOR = "the-custodian/canon/standards/project-repository-flavor_v0.1.md" +CANON_CLASSIFICATION = "the-custodian/canon/standards/repo-classification-standard_v1.0.md" + +DURABLE_FLAVORS = ("experimental", "research", "tooling", "product", "business") +CATEGORIES = (*DURABLE_FLAVORS, "project") +FLAVOR_MARKER_PREFIX = "PRJ-WP" +WP_ID_RE = re.compile(r"^([A-Z][A-Z0-9]*)-WP-(\d{4})(?:-T\d{2})?$") +GOAL_SECTIONS = ("outcome", "invariants", "success gates", "project retirement") + +Severity = Literal["missing", "contradictory", "warning"] + + +@dataclass +class Finding: + code: str + severity: Severity + path: str + message: str + canon: str + + +@dataclass +class FlavorResolution: + flavor: str + prj_layout: bool + signals: dict[str, str | None] + disagreements: list[str] + + +@dataclass +class ConformanceReport: + slug: str + flavor: FlavorResolution + findings: list[Finding] = field(default_factory=list) + + @property + def ok(self) -> bool: + return not any(f.severity in {"missing", "contradictory"} for f in self.findings) + + def to_dict(self) -> dict[str, Any]: + return { + "ok": self.ok, + "slug": self.slug, + "flavor": self.flavor.flavor, + "prj_layout": self.flavor.prj_layout, + "signals": self.flavor.signals, + "disagreements": self.flavor.disagreements, + "findings": [f.__dict__ for f in self.findings], + "canon": [CANON_FLAVOR, CANON_CLASSIFICATION], + } + + +def _slug(repo_root: Path) -> str: + return re.sub(r"[^a-z0-9]+", "-", repo_root.name.lower()).strip("-") or "repo" + + +def _goal_flavor(repo_root: Path) -> str | None: + path = repo_root / "GOAL.md" + if not path.is_file(): + return None + fm, _ = parse_frontmatter(path.read_text(encoding="utf-8")) + raw = fm.get("repo_flavor") + return str(raw).strip().lower() if raw else None + + +def resolve_flavor(repo_root: Path, *, slug: str | None = None) -> FlavorResolution: + """Precedence: classification category, GOAL.md repo_flavor, slug prefix.""" + slug = slug or _slug(repo_root) + classification = load_classification(repo_root) or {} + category = classification.get("category") + category = str(category).strip().lower() if category else None + goal_flavor = _goal_flavor(repo_root) + slug_prj = slug.startswith("prj-") + signals = { + "classification.category": category, + "GOAL.md.repo_flavor": goal_flavor, + "slug_prefix": "prj-" if slug_prj else None, + } + + if category in CATEGORIES: + flavor = category + elif goal_flavor in CATEGORIES: + flavor = goal_flavor + elif slug_prj: + flavor = "project" + else: + flavor = "tooling" + + disagreements: list[str] = [] + if category and goal_flavor and category != goal_flavor: + disagreements.append( + f"classification.category={category!r} disagrees with GOAL.md repo_flavor={goal_flavor!r}" + ) + if slug_prj and flavor != "project": + disagreements.append(f"slug is prj-* but resolved flavor is {flavor!r}") + if category == "project" and not slug_prj and goal_flavor != "project": + disagreements.append( + "category is project but slug is not prj-*; not every category:project is a prj- repo" + ) + + prj_layout = slug_prj or goal_flavor == "project" + return FlavorResolution( + flavor=flavor, + prj_layout=prj_layout, + signals=signals, + disagreements=disagreements, + ) + + +def required_files(flavor: FlavorResolution) -> tuple[str, ...]: + common = ("README.md", "SCOPE.md", "AGENTS.md", ".repo-classification.yaml", "workplans") + if flavor.prj_layout: + return ("GOAL.md", *common) + return ("INTENT.md", *common) + + +def purpose_document(flavor: FlavorResolution) -> str: + return "GOAL.md" if flavor.prj_layout else "INTENT.md" + + +def expected_workplan_prefix(repo_root: Path, *, slug: str | None = None) -> str | None: + """Prefix must come from repo/project identity, never the flavor marker.""" + slug = slug or _slug(repo_root) + if slug.startswith("prj-"): + stem = slug.removeprefix("prj-") + letters = re.sub(r"[^a-z0-9]", "", stem) + if letters: + return letters[:8].upper() + "-WP" + return None + compact = re.sub(r"[^a-z0-9]", "", slug) + if not compact: + return None + return compact[:8].upper() + "-WP" + + +def workplan_ids(repo_root: Path) -> list[tuple[str, str, str]]: + """Return (path, prefix, number) for each PREFIX-WP-NNNN workplan id.""" + found: list[tuple[str, str, str]] = [] + wp_dir = repo_root / "workplans" + if not wp_dir.is_dir(): + return found + for path in sorted(wp_dir.rglob("*.md")): + if path.name.startswith("ADHOC-"): + continue + fm, _ = parse_frontmatter(path.read_text(encoding="utf-8")) + ident = fm.get("id") + if ident is None: + continue + match = WP_ID_RE.match(str(ident).strip()) + if not match: + continue + found.append((str(path.relative_to(repo_root)), match.group(1) + "-WP", match.group(2))) + return found + + +def check_repository(repo_root: Path, *, slug: str | None = None) -> ConformanceReport: + repo_root = repo_root.resolve() + slug = slug or _slug(repo_root) + flavor = resolve_flavor(repo_root, slug=slug) + report = ConformanceReport(slug=slug, flavor=flavor) + + for note in flavor.disagreements: + report.findings.append( + Finding( + code="flavor-signal-disagreement", + severity="warning", + path=".repo-classification.yaml", + message=note, + canon=CANON_CLASSIFICATION, + ) + ) + + for rel in required_files(flavor): + target = repo_root / rel + present = target.is_dir() if rel.endswith("/") or rel == "workplans" else target.is_file() + if not present: + report.findings.append( + Finding( + code="required-file-missing", + severity="missing", + path=rel, + message=f"{rel} is required for flavor {flavor.flavor}", + canon=CANON_FLAVOR if flavor.prj_layout else CANON_CLASSIFICATION, + ) + ) + + has_intent = (repo_root / "INTENT.md").is_file() + has_goal = (repo_root / "GOAL.md").is_file() + if flavor.prj_layout and has_intent and has_goal: + report.findings.append( + Finding( + code="intent-and-goal", + severity="contradictory", + path="INTENT.md", + message="prj- flavor forbids shipping both INTENT.md and GOAL.md", + canon=CANON_FLAVOR, + ) + ) + if flavor.prj_layout and has_intent and not has_goal: + report.findings.append( + Finding( + code="intent-instead-of-goal", + severity="contradictory", + path="INTENT.md", + message="prj- flavor requires GOAL.md and must not use INTENT.md as the purpose document", + canon=CANON_FLAVOR, + ) + ) + if not flavor.prj_layout and has_intent and has_goal: + report.findings.append( + Finding( + code="intent-and-goal", + severity="warning", + path="GOAL.md", + message="durable flavors use INTENT.md; GOAL.md is the project-flavor purpose document", + canon=CANON_FLAVOR, + ) + ) + + if flavor.prj_layout and has_goal: + body = (repo_root / "GOAL.md").read_text(encoding="utf-8").lower() + missing = [name for name in GOAL_SECTIONS if name not in body] + if missing: + report.findings.append( + Finding( + code="goal-sections-missing", + severity="missing", + path="GOAL.md", + message="GOAL.md must include " + ", ".join(GOAL_SECTIONS) + f"; missing {missing}", + canon=CANON_FLAVOR, + ) + ) + + ids = workplan_ids(repo_root) + prefixes = {prefix for _path, prefix, _num in ids} + if FLAVOR_MARKER_PREFIX in prefixes: + report.findings.append( + Finding( + code="flavor-derived-prefix", + severity="contradictory", + path="workplans/", + message="PRJ-WP- is derived from the flavor marker; prefixes must come from project identity", + canon=CANON_FLAVOR, + ) + ) + if len(prefixes) > 1: + report.findings.append( + Finding( + code="multiple-prefixes", + severity="warning", + path="workplans/", + message=f"repository uses more than one workplan prefix: {sorted(prefixes)}", + canon="ADR-007", + ) + ) + return report diff --git a/tests/test_standards.py b/tests/test_standards.py new file mode 100644 index 0000000..49bcd08 --- /dev/null +++ b/tests/test_standards.py @@ -0,0 +1,112 @@ +from pathlib import Path + +from repo_manager.cli import main +from repo_manager.prefix_registry import scan_prefixes +from repo_manager.standards import check_repository, resolve_flavor + + +def _write(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + + +def _durable_files(root: Path) -> None: + _write(root / "INTENT.md", "# Intent\n\nWhy this exists.\n") + _write(root / "SCOPE.md", "# Scope\n") + _write(root / "README.md", "# Demo\n") + _write(root / "AGENTS.md", "# Agents\n") + _write( + root / ".repo-classification.yaml", + "repo_classification:\n category: tooling\n domain: infotech\n", + ) + _write( + root / "workplans" / "DEMO-WP-0001.md", + "---\nid: DEMO-WP-0001\nstatus: proposed\n---\n# Demo\n", + ) + + +def test_prj_repo_flags_intent_and_goal(tmp_path: Path): + root = tmp_path / "prj-demo" + _durable_files(root) + _write( + root / "GOAL.md", + "---\nrepo_flavor: project\n---\n# Goal\n\n" + "## Outcome\nX\n## Invariants\nY\n## Success gates\nZ\n## Project retirement\nW\n", + ) + _write( + root / ".repo-classification.yaml", + "repo_classification:\n category: project\n domain: infotech\n", + ) + report = check_repository(root) + codes = {f.code for f in report.findings} + assert "intent-and-goal" in codes + assert report.ok is False + assert report.flavor.prj_layout is True + + +def test_prj_repo_flags_flavor_derived_prefix(tmp_path: Path): + root = tmp_path / "prj-cutover" + _write(root / "GOAL.md", "## Outcome\n## Invariants\n## Success gates\n## Project retirement\n") + _write(root / "SCOPE.md", "# S\n") + _write(root / "README.md", "# R\n") + _write(root / "AGENTS.md", "# A\n") + _write( + root / ".repo-classification.yaml", + "repo_classification:\n category: project\n domain: infotech\n", + ) + _write( + root / "workplans" / "PRJ-WP-0001.md", + "---\nid: PRJ-WP-0001\nstatus: proposed\n---\n# Bad\n", + ) + report = check_repository(root) + assert any(f.code == "flavor-derived-prefix" for f in report.findings) + + +def test_durable_repo_missing_intent(tmp_path: Path): + root = tmp_path / "some-tool" + _write(root / "SCOPE.md", "# S\n") + _write(root / "README.md", "# R\n") + _write(root / "AGENTS.md", "# A\n") + _write( + root / ".repo-classification.yaml", + "repo_classification:\n category: tooling\n domain: infotech\n", + ) + (root / "workplans").mkdir() + report = check_repository(root) + assert any(f.code == "required-file-missing" and f.path == "INTENT.md" for f in report.findings) + + +def test_flavor_disagreement_is_warning(tmp_path: Path): + root = tmp_path / "mixed" + _durable_files(root) + _write(root / "GOAL.md", "---\nrepo_flavor: project\n---\n# G\n") + flavor = resolve_flavor(root) + assert flavor.disagreements + report = check_repository(root) + assert any(f.code == "flavor-signal-disagreement" and f.severity == "warning" for f in report.findings) + + +def test_prefix_uniqueness_detects_share_and_reuse(tmp_path: Path): + a = tmp_path / "alpha" + b = tmp_path / "beta" + for repo, ident in ((a, "SHARED-WP-0001"), (b, "SHARED-WP-0001")): + _write( + repo / "workplans" / f"{ident}.md", + f"---\nid: {ident}\nstatus: finished\n---\n# X\n", + ) + report = scan_prefixes(tmp_path) + assert report["ok"] is False + assert "SHARED-WP" in report["shared_prefixes"] + assert "SHARED-WP-0001" in report["reused_identifiers"] + + +def test_cli_conform_and_prefix(tmp_path: Path, capsys): + root = tmp_path / "ok-tool" + _durable_files(root) + assert main(["conform", "--path", str(root)]) == 0 + fleet = tmp_path / "fleet" + one = fleet / "one" + _write(one / "workplans" / "PRJ-WP-0001.md", "---\nid: PRJ-WP-0001\nstatus: proposed\n---\n") + assert main(["prefix-uniqueness", "--root", str(fleet)]) == 1 + out = capsys.readouterr().out + assert "PRJ-WP" in out or "flavor_derived" in out diff --git a/workplans/RMGR-WP-0004-repository-standards-conformance.md b/workplans/RMGR-WP-0004-repository-standards-conformance.md index 7611d24..f919831 100644 --- a/workplans/RMGR-WP-0004-repository-standards-conformance.md +++ b/workplans/RMGR-WP-0004-repository-standards-conformance.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: infotech created: "2026-08-16" -updated: "2026-08-16" +updated: "2026-08-18" parent_project: prj-state-hub-retirement parent_workplan: SHR-WP-0001 related: @@ -54,7 +54,7 @@ claims "repository registration, identity, classification, and lifecycle" and ```task id: RMGR-WP-0004-T01 -status: todo +status: done priority: high state_hub_task_id: "dd2db30f-c48f-404d-a1b4-bd75b41d42f4" ``` @@ -77,11 +77,16 @@ Source standards: `project-repository-flavor_v0.1.md` and authoritative; Repo Manager implements against it and must not fork the rules into a second definition. +**Result (2026-08-18):** `src/repo_manager/standards.py` resolves flavor +(classification → GOAL.md → `prj-` slug), lists required files, and names +anti-patterns (`INTENT.md`+`GOAL.md`, `PRJ-WP-`). Index: +`docs/repository-standards_v0.1.md`. + ## Implement conformance checking ```task id: RMGR-WP-0004-T02 -status: wait +status: progress priority: high state_hub_task_id: "2c9b0cb1-f0f0-40f7-83ef-1828298338d3" ``` @@ -101,6 +106,10 @@ Known first findings, both live today: - every `prj-` repo would collide on workplan prefix `PRJ-WP-`, because the prefix is derived from the flavor marker rather than from the project. +**In progress (2026-08-18):** `rmgr conform` reports `missing` vs +`contradictory` and flags both known findings. Remaining: call from the +consistency lane / STATE-WP-0080 guard. + ## Own governed scaffolding ```task @@ -196,7 +205,7 @@ with compatibility tests. Cover: ```task id: RMGR-WP-0004-T08 -status: wait +status: done priority: high state_hub_task_id: "fc7395b0-e86d-4231-baac-ea7fa5dc2174" ``` @@ -234,6 +243,13 @@ Detection only. Remediating the existing collisions is an open ruling in **This task gates `RMGR-WP-0005`.** Deterministic identifier derivation from a non-unique identifier would manufacture UUID collisions rather than remove them. +**Result (2026-08-18):** `config/workplan-prefix-registry.yaml` plus +`rmgr prefix-uniqueness --root `. Detection only: shared prefixes, +reused identifiers, `PRJ-WP-`, retired prefixes still in use. A 112-repo +scan still finds `CUST-WP`, `RAILIANCE-WP`, `PRJ-WP` (in +`prj-forgejo-org-refactor`). Remediating historical collisions remains +ADR-007 § Migration. + ## Assign ownership of the shared prefixes ```task