diff --git a/dev-docs/specs/2026-10-11-plcc-ng-demo-migration-design.md b/dev-docs/specs/2026-10-11-plcc-ng-demo-migration-design.md new file mode 100644 index 0000000..6bb7f43 --- /dev/null +++ b/dev-docs/specs/2026-10-11-plcc-ng-demo-migration-design.md @@ -0,0 +1,132 @@ +# Migrating plcc-ng-demo's tracker into `ourPLCC/dev` — design + +Date: 2026-10-11 + +## Problem + +plcc-ng-demo keeps a small file-based tracker in `dev-docs/issues/`: five +issues, 001–005, all closed. The central tracker design +([2026-10-08-199-central-tracker-design.md](../../../plcc-ng/dev-docs/specs/2026-10-08-199-central-tracker-design.md), +"the central spec" below) says plcc-ng-demo is migrated "from this spec", but +leaves two things open: + +- `migrate/` converts plcc-ng's tracker only. plcc-ng-demo's files use the + same format (a `**Type:**` line, closed issues in `done/`), but the plcc-ng + driver hard-codes plcc-ng's project, offset, and GitHub URL. +- Three demo issues were copied into plcc-ng by hand. The central spec's rule + for such duplicates names only languages-ng's. + +This spec records only what plcc-ng-demo adds to the central spec and to the +[languages-ng migration spec](2026-10-11-languages-ng-migration-design.md). +Everything those specs say about the migration and do not override here +applies. + +## Supersedes + +[2026-10-08-199-central-tracker-design.md](../../../plcc-ng/dev-docs/specs/2026-10-08-199-central-tracker-design.md), +for plcc-ng-demo only: "Duplicated upstream findings" (below lists the demo's). + +## Decision summary + +Add a plcc-ng-demo driver to `migrate/` that reuses plcc-ng's loader and +converter with the demo's project, offset (+800), and GitHub URL, and adds a +`duplicates` triage table in the shape languages-ng uses. Converting by hand is +not an option: the `backlog` CLI allocates IDs itself, so it cannot create +`CR-801`–`CR-805`, and the tracker files are not edited by hand. + +No legacy number repeats, so no duplicate-number range is reserved. + +The issues contain no Markdown links and no bare `#N`, so link rewriting has +nothing to do. The driver still runs it, so the report would show anything +missed. + +## Triage + +Recorded in `migrate/plcc-ng-demo-triage.json`: `open` is empty, and +`duplicates` maps legacy number → plcc-ng number. + +**Duplicates of plcc-ng issues** become `Done` + `wontdo` in `completed/`, +with project `plcc-ng`, the type of the plcc-ng CR they duplicate, and the +final summary `Duplicate of CR-, filed upstream by hand.` + +| plcc-ng-demo | CR | plcc-ng | type | +| --- | --- | --- | --- | +| 001 | CR-801 | CR-159 | docs | +| 002 | CR-802 | CR-160 | fix | +| 005 | CR-805 | CR-161 | feat | + +CR-160 and CR-161 already cite CR-802 and CR-805 as where they were first +filed, so those IDs must exist. + +**Demo-local issues** become `Done` in `completed/`, project `plcc-ng-demo`, +type from the legacy `**Type:**`: + +| plcc-ng-demo | CR | type | +| --- | --- | --- | +| 003 | CR-803 | docs | +| 004 | CR-804 | chore | + +**Expected counts:** `completed/` 5, `tasks/` 0, `drafts/` 0. + +## Switch-over in plcc-ng-demo + +**Devcontainer.** Set up as plcc-ng's (already done: the `../dev` clone and +mount, `BACKLOG_CWD`, `safe.directory`, the `plcc-ng-demo-claude` volume, +the pinned `backlog`, and `bin/install/claude-imports.bash`). + +**`AGENTS.md`** imports `../dev/ORG-AGENTS.md`, `../dev/CONTRIBUTING.md`, +then `CONTRIBUTING.md`, one per line. It has no repo-specific agent rules yet, +so it has no section for them. + +**`CONTRIBUTING.md`** (new) opens with the link to the org developer guide, +then: + +- "Where this repo differs": nowhere; the existing + `dev-docs/plans/2026-07-21-plcc-ng-demo.md` is a kept plan, frozen as + history, with none added. +- Development environment: the `../dev` clone and mount, the tracker + `project` value `plcc-ng-demo`, old numbers at +800 (`#3` is `CR-803`), and + why the container approves `AGENTS.md`'s imports and how to check + (`/memory`), as plcc-ng's says. +- Commands: `bin/install/claude-imports.bash`. + +**Retire the old tracker.** Delete `dev-docs/issues/`, `bin/issues/`, and +`dev-docs/roadmap.md` (it holds only the old tracker's empty "Open Issues" +list). Allow `Bash(backlog:*)` in `.claude/settings.json`. + +**Frozen documents** (`dev-docs/specs/`, `dev-docs/plans/`) mention +`dev-docs/issues/` and `bin/issues/` only as plain text, so they are not +edited. + +## Order + +1. `migrate/` gains the plcc-ng-demo driver, with tests, on its own branch in + this repo. +2. Dry run from plcc-ng-demo `main` into a scratch copy of the tracker; a + human reviews the report. +3. The plcc-ng-demo switch-over branch is prepared. +4. The conversion re-runs from plcc-ng-demo `main` (the freeze point) and + lands in the tracker. +5. Human checkpoint before merging the plcc-ng-demo branch: rebuild from it + on the host; the container starts, `backlog task list` works, and + `/memory` lists the imported files. From this point no issue is filed in + `dev-docs/issues/`. + +## Verification + +- `migrate/tests` cover the new driver and the `duplicates` table; the + plcc-ng and languages-ng tests still pass. +- `verify.py` for plcc-ng-demo: per-folder counts match the expected counts + above; `backlog task view` succeeds for 801–805; `backlog doctor` and + `bin/check.py` are clean. +- In plcc-ng-demo after switch-over, `grep -rn 'dev-docs/issues\|bin/issues\|roadmap'` + finds nothing outside `.git` and the frozen `dev-docs/plans/` and + `dev-docs/specs/`, and no relative Markdown link points at a deleted file. + +## Rejected alternatives + +| Alternative | Why not | +| --- | --- | +| Duplicates as plain `Done` in project `plcc-ng-demo` | They were closed before the move, but the languages-ng rule already settles hand-copied upstream findings; one rule for both repos | +| Generalize `plcc_ng.py` in place with command-line options | A driver per repo is how `drivers.py`, `verify`, and `citations` already select behavior | +| Skip the spec and record the decisions in the CRs | Specs hold decisions and CRs reference them; the duplicate rule overrides the central spec, which belongs in a spec | diff --git a/dev-docs/tracker.md b/dev-docs/tracker.md index 3db0419..40d2567 100644 --- a/dev-docs/tracker.md +++ b/dev-docs/tracker.md @@ -192,15 +192,20 @@ the project and type rules. ## Migration tooling `migrate/` converts legacy in-repo trackers (Python 3 standard library), with -one driver per repo: `python3 -m migrate.plcc_ng` and -`python3 -m migrate.languages_ng` (each takes `--repo`, `--tracker`, -`--triage`, `--report`). `python3 -m migrate.verify` and -`python3 -m migrate.citations` take `--driver plcc-ng|languages-ng` (default -`plcc-ng`); `python3 -m migrate.repo_links --project ` picks the driver -from the project. Decisions are in `migrate/-triage.json`, results in +one driver per repo: `python3 -m migrate.plcc_ng`, +`python3 -m migrate.languages_ng`, and `python3 -m migrate.plcc_ng_demo` +(each takes `--repo`, `--tracker`, `--triage`, `--report`). +`python3 -m migrate.verify` and `python3 -m migrate.citations` take +`--driver plcc-ng|languages-ng|plcc-ng-demo` (default `plcc-ng`); +`python3 -m migrate.repo_links --project ` picks the driver from the +project. Decisions are in `migrate/-triage.json`, results in `migrate/reports/.md`. plcc-ng's triage keys (`open`, `type_overrides`) are a legacy number (`"105"`) or, for a number two files share, the file stem (`"035-python-emitter-missing-semantics-blocks"`). +plcc-ng-demo's tracker has plcc-ng's format, so its driver runs plcc-ng's +converter at offset +800; its triage file adds `duplicates` (legacy number → +the plcc-ng CR it duplicates), per +[its migration spec](specs/2026-10-11-plcc-ng-demo-migration-design.md). languages-ng's are legacy numbers only, and its triage file adds `duplicates` (legacy number → the plcc-ng CR it duplicates), `references` (legacy number → `{"#N": replacement}` for a bare `#N` the tool cannot diff --git a/migrate/citations.py b/migrate/citations.py index 8590004..e87cf58 100644 --- a/migrate/citations.py +++ b/migrate/citations.py @@ -1,6 +1,6 @@ """Count citations of each open legacy issue, as triage evidence. -Usage: python3 -m migrate.citations --repo REPO [--driver plcc-ng|languages-ng] +Usage: python3 -m migrate.citations --repo REPO [--driver plcc-ng|languages-ng|plcc-ng-demo] """ import argparse import re diff --git a/migrate/drivers.py b/migrate/drivers.py index d75297e..67810e3 100644 --- a/migrate/drivers.py +++ b/migrate/drivers.py @@ -1,7 +1,8 @@ """The legacy-tracker drivers, one per migrated repo, by project name.""" import importlib -DRIVERS = {"plcc-ng": "migrate.plcc_ng", "languages-ng": "migrate.languages_ng"} +DRIVERS = {"plcc-ng": "migrate.plcc_ng", "languages-ng": "migrate.languages_ng", + "plcc-ng-demo": "migrate.plcc_ng_demo"} def load(project): diff --git a/migrate/emit.py b/migrate/emit.py index 7c2897d..af03658 100644 --- a/migrate/emit.py +++ b/migrate/emit.py @@ -1,11 +1,13 @@ """Render Backlog.md task files from legacy issues.""" import re +from pathlib import Path CORE_TYPES = ("fix", "feat", "docs", "test", "refactor", "chore") TYPE_MAP = {"bug": "fix", "enhancement": "feat", "feature": "feat", "perf": "fix", **{t: t for t in CORE_TYPES}} WONTDO_RE = re.compile(r"^(abandoned|superseded)\b", re.IGNORECASE) HEADING_RE = re.compile(r"^#{1,5} ") +TYPE_LINE_RE = re.compile(r"^type: (\S+)$", re.MULTILINE) def yaml_quote(s): @@ -68,3 +70,12 @@ def render_task(*, task_id, title, status, created, labels, description, if final_summary: text += "\n" + _section("Final Summary", "FINAL_SUMMARY", final_summary.strip()) return text + + +def tracker_type(backlog, cr): + """The type of CR- in the tracker, or None if the tracker has no such CR.""" + for folder in ("tasks", "completed"): + for path in (Path(backlog) / folder).glob(f"cr-{cr} - *.md"): + found = TYPE_LINE_RE.search(path.read_text(encoding="utf-8")) + return found.group(1) if found else None + return None diff --git a/migrate/languages_ng.py b/migrate/languages_ng.py index 00f6aee..68778ed 100644 --- a/migrate/languages_ng.py +++ b/migrate/languages_ng.py @@ -9,7 +9,8 @@ import sys from pathlib import Path -from migrate.emit import CORE_TYPES, TYPE_MAP, assemble_description, filename_for, render_task +from migrate.emit import (CORE_TYPES, TYPE_MAP, assemble_description, filename_for, + render_task, tracker_type) from migrate.legacy import parse_frontmatter_issue from migrate.links import IssueRef, LinkContext, rewrite_issue_body @@ -24,7 +25,6 @@ FOLDERS = ("tasks", "completed", "drafts") AS_FOLDER = {"cr": "tasks", "draft": "drafts", "wontdo": "completed"} TRIAGE_TABLES = ("open", "duplicates", "type_overrides", "references") -TYPE_LINE_RE = re.compile(r"^type: (\S+)$", re.MULTILINE) DRAFT_FILE_RE = re.compile(r"^draft-(\d+) - ") @@ -52,15 +52,6 @@ def expected_counts(issues, triage): return counts -def tracker_type(backlog, cr): - """The type of CR- in the tracker, or None if the tracker has no such CR.""" - for folder in ("tasks", "completed"): - for path in (Path(backlog) / folder).glob(f"cr-{cr} - *.md"): - found = TYPE_LINE_RE.search(path.read_text(encoding="utf-8")) - return found.group(1) if found else None - return None - - def validate(issues, triage, backlog): """Return {legacy number: type} for the duplicates; exit listing every problem instead.""" errors, dup_types = [], {} diff --git a/migrate/plcc-ng-demo-triage.json b/migrate/plcc-ng-demo-triage.json new file mode 100644 index 0000000..bf414e3 --- /dev/null +++ b/migrate/plcc-ng-demo-triage.json @@ -0,0 +1,8 @@ +{ + "open": {}, + "duplicates": { + "1": 159, + "2": 160, + "5": 161 + } +} diff --git a/migrate/plcc_ng.py b/migrate/plcc_ng.py index 86ed3d5..70a1714 100644 --- a/migrate/plcc_ng.py +++ b/migrate/plcc_ng.py @@ -1,29 +1,53 @@ """Convert plcc-ng's legacy dev-docs/issues/ tracker into Backlog.md files. Usage: python3 -m migrate.plcc_ng --repo PLCC_NG --tracker ISSUES --triage FILE --report OUT + +plcc-ng-demo's tracker has the same format; migrate.plcc_ng_demo runs this +converter with its own Profile. """ import argparse import json import posixpath import re import sys +from dataclasses import dataclass from pathlib import Path from migrate.emit import (CORE_TYPES, TYPE_MAP, WONTDO_RE, assemble_description, filename_for, - render_task) + render_task, tracker_type) from migrate.legacy import parse_issue from migrate.links import IssueRef, LinkContext, rewrite_issue_body -PROJECT = "plcc-ng" OFFSET = 0 DUP_OFFSET = 400 DUP_RANGE = range(400, 500) -OTHER_OFFSETS = {"languages-ng": 500, "plcc-ng-demo": 800} -GITHUB = "https://github.com/ourPLCC/plcc-ng" -PROVENANCE_PREFIX = f"Migrated from {PROJECT} #" -PROVENANCE_END_RE = re.compile( - rf"^{re.escape(PROVENANCE_PREFIX)}\d+\.\n$", re.MULTILINE) FOLDERS = ("tasks", "completed", "drafts") +TRIAGE_TABLES = ("open", "type_overrides", "duplicates") +UPSTREAM = "plcc-ng" + + +@dataclass(frozen=True) +class Profile: + """What differs between repos whose trackers use plcc-ng's format.""" + project: str + offset: int + dup_offset: int + dup_range: range + other_offsets: dict + github: str + + @property + def provenance_prefix(self): + return f"Migrated from {self.project} #" + + @property + def provenance_end_re(self): + return re.compile(rf"^{re.escape(self.provenance_prefix)}\d+\.\n" + r"$", re.MULTILINE) + + +PLCC_NG = Profile("plcc-ng", OFFSET, DUP_OFFSET, DUP_RANGE, + {"languages-ng": 500, "plcc-ng-demo": 800}, "https://github.com/ourPLCC/plcc-ng") def load_issues(repo_root): @@ -61,8 +85,12 @@ def build_refs(issues, ids): return refs -def issue_refs(issues): - return build_refs(issues, assign_ids(issues)[0]) +def _ids(issues, profile): + return assign_ids(issues, profile.offset, profile.dup_offset, profile.dup_range) + + +def issue_refs(issues, profile=PLCC_NG): + return build_refs(issues, _ids(issues, profile)[0]) def _stem(issue): @@ -82,7 +110,7 @@ def _triage_entry(table, issue, duplicated): def _placement(issue, triage, duplicated): - if issue.closed: + if issue.closed or _triage_entry(triage.get("duplicates", {}), issue, duplicated): return "completed" entry = _triage_entry(triage["open"], issue, duplicated) return "drafts" if entry["as"] == "draft" else "tasks" @@ -96,17 +124,30 @@ def expected_counts(issues, triage): return counts -def _validate(issues, triage): - errors = [] +def _validate(issues, triage, backlog): + """Return {rel_path: type} for the upstream duplicates; exit listing every problem instead.""" + errors, dup_types = [], {} duplicated = _duplicated(issues) open_triage = triage.get("open", {}) overrides = triage.get("type_overrides", {}) - for name, table in (("open", open_triage), ("type_overrides", overrides)): + upstream = triage.get("duplicates", {}) + known = {str(i.number) for i in issues} | {_stem(i) for i in issues} + for key in upstream: + if key not in known: + errors.append(f"duplicates key {key!r} is not a legacy issue number or file stem") + for name in TRIAGE_TABLES: + table = triage.get(name, {}) for number in sorted(duplicated): if str(number) in table: stems = ", ".join(_stem(i) for i in issues if i.number == number) errors.append(f"{name} key {str(number)!r} is ambiguous; key by file stem ({stems})") for issue in issues: + cr = _triage_entry(upstream, issue, duplicated) + if cr: + dup_types[issue.rel_path] = tracker_type(backlog, cr) + if dup_types[issue.rel_path] is None: + errors.append(f"#{issue.number:03d} duplicates CR-{cr}, which the tracker does not have") + continue if not issue.closed: entry = _triage_entry(open_triage, issue, duplicated) if not entry or entry.get("as") not in ("cr", "draft"): @@ -116,38 +157,46 @@ def _validate(issues, triage): errors.append(f"#{issue.number:03d} has type {issue.type!r}; add a type_overrides entry") if errors: raise SystemExit("cannot convert:\n " + "\n ".join(errors)) + return dup_types -def is_migrated(path): +def is_migrated(path, profile=PLCC_NG): """True if the file's description ends with this migration's provenance line.""" - return PROVENANCE_END_RE.search(path.read_text(encoding="utf-8")) is not None + return profile.provenance_end_re.search(path.read_text(encoding="utf-8")) is not None -def _remove_previous(backlog): +def _remove_previous(backlog, profile): for folder in FOLDERS: for path in (backlog / folder).glob("*.md"): - if is_migrated(path): + if is_migrated(path, profile): path.unlink() -def convert(repo_root, tracker_root, triage): +def convert(repo_root, tracker_root, triage, profile=PLCC_NG): repo_root, backlog = Path(repo_root), Path(tracker_root) / "backlog" issues = load_issues(repo_root) - _validate(issues, triage) - ids, remaps = assign_ids(issues) + dup_types = _validate(issues, triage, backlog) + ids, remaps = _ids(issues, profile) duplicated = _duplicated(issues) - ctx = LinkContext(PROJECT, repo_root, build_refs(issues, ids), OTHER_OFFSETS, GITHUB) - overrides = triage.get("type_overrides", {}) - _remove_previous(backlog) + ctx = LinkContext(profile.project, repo_root, build_refs(issues, ids), profile.other_offsets, + profile.github) + overrides, upstream = triage.get("type_overrides", {}), triage.get("duplicates", {}) + _remove_previous(backlog, profile) - type_notes, wontdo, drafts, draft_number = [], [], [], 0 + dup_notes, type_notes, wontdo, drafts, draft_number = [], [], [], [], 0 for issue in issues: folder = _placement(issue, triage, duplicated) - cr_type = _triage_entry(overrides, issue, duplicated) or TYPE_MAP[issue.type] + upstream_cr = _triage_entry(upstream, issue, duplicated) + cr_type = (dup_types.get(issue.rel_path) or _triage_entry(overrides, issue, duplicated) + or TYPE_MAP[issue.type]) if cr_type != issue.type: type_notes.append(f"#{issue.number:03d}: {issue.type!r} -> {cr_type}") - labels, final_summary, status = [], None, "To Do" - if folder == "completed": + labels, final_summary, status, project = [], None, "To Do", profile.project + if upstream_cr: + task_id, status, labels, project = f"CR-{ids[issue.rel_path]}", "Done", ["wontdo"], UPSTREAM + final_summary = f"Duplicate of CR-{upstream_cr}, filed upstream by hand." + dup_notes.append(f"#{issue.number:03d} -> {task_id}: duplicate of CR-{upstream_cr}") + elif folder == "completed": task_id, status = f"CR-{ids[issue.rel_path]}", "Done" if WONTDO_RE.match(issue.status_note): labels = ["wontdo"] @@ -162,21 +211,22 @@ def convert(repo_root, tracker_root, triage): task_id = f"CR-{ids[issue.rel_path]}" status = _triage_entry(triage["open"], issue, duplicated).get("status", "To Do") body = rewrite_issue_body(assemble_description(issue), posixpath.dirname(issue.rel_path), ctx) - description = f"{body}\n\n{PROVENANCE_PREFIX}{issue.number:03d}." + description = f"{body}\n\n{profile.provenance_prefix}{issue.number:03d}." text = render_task(task_id=task_id, title=issue.title, status=status, created=issue.date, labels=labels, description=description, type_=cr_type, - project=PROJECT, final_summary=final_summary) + project=project, final_summary=final_summary) (backlog / folder / filename_for(task_id, issue.title)).write_text(text, encoding="utf-8") counts = expected_counts(issues, triage) sections = [ + ("Duplicates of plcc-ng issues", dup_notes), ("Duplicate-number remaps", remaps), ("Type mappings (non-identity)", type_notes), ("Won't-do (abandoned or superseded)", wontdo), ("Drafts (lose their number; provenance line keeps it greppable)", drafts), ("Link findings", ctx.findings), ] - lines = [f"# {PROJECT} migration report", "", + lines = [f"# {profile.project} migration report", "", f"Converted {len(issues)} files: {counts['completed']} -> completed/, " f"{counts['tasks']} -> tasks/, {counts['drafts']} -> drafts/.", ""] for title, items in sections: @@ -184,15 +234,15 @@ def convert(repo_root, tracker_root, triage): return "\n".join(lines) -def main(argv=None): - parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) +def main(argv=None, profile=PLCC_NG, doc=__doc__): + parser = argparse.ArgumentParser(description=doc.splitlines()[0]) parser.add_argument("--repo", required=True, type=Path) parser.add_argument("--tracker", required=True, type=Path) parser.add_argument("--triage", required=True, type=Path) parser.add_argument("--report", required=True, type=Path) args = parser.parse_args(argv) triage = json.loads(args.triage.read_text(encoding="utf-8")) - report = convert(args.repo, args.tracker, triage) + report = convert(args.repo, args.tracker, triage, profile) args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(report, encoding="utf-8") print(report.splitlines()[2]) diff --git a/migrate/plcc_ng_demo.py b/migrate/plcc_ng_demo.py new file mode 100644 index 0000000..1ea735d --- /dev/null +++ b/migrate/plcc_ng_demo.py @@ -0,0 +1,36 @@ +"""Convert plcc-ng-demo's legacy dev-docs/issues/ tracker into Backlog.md files. + +Usage: python3 -m migrate.plcc_ng_demo --repo PLCC_NG_DEMO --tracker TRACKER --triage FILE --report OUT + +The demo's tracker is a copy of plcc-ng's, so this runs plcc-ng's converter with +the demo's profile. +""" +import sys + +from migrate import plcc_ng +from migrate.plcc_ng import FOLDERS, expected_counts, load_issues # noqa: F401 (driver API) + +# No legacy number repeats, so the duplicate-number range is empty: a repeat stops the run. +PROFILE = plcc_ng.Profile("plcc-ng-demo", 800, 100, range(0), + {"plcc-ng": 0, "languages-ng": 500}, + "https://github.com/ourPLCC/plcc-ng-demo") + + +def issue_refs(issues): + return plcc_ng.issue_refs(issues, PROFILE) + + +def is_migrated(path): + return plcc_ng.is_migrated(path, PROFILE) + + +def convert(repo_root, tracker_root, triage): + return plcc_ng.convert(repo_root, tracker_root, triage, PROFILE) + + +def main(argv=None): + return plcc_ng.main(argv, PROFILE, __doc__) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/migrate/repo_links.py b/migrate/repo_links.py index eac0d67..2f2942c 100644 --- a/migrate/repo_links.py +++ b/migrate/repo_links.py @@ -1,6 +1,6 @@ """Rewrite links into a code repo's retired dev-docs/issues/ tracker to CR IDs. -Usage: python3 -m migrate.repo_links --repo PATH --project plcc-ng|languages-ng [--check] +Usage: python3 -m migrate.repo_links --repo PATH --project plcc-ng|languages-ng|plcc-ng-demo [--check] """ import argparse import posixpath diff --git a/migrate/tests/test_plcc_ng_demo.py b/migrate/tests/test_plcc_ng_demo.py new file mode 100644 index 0000000..69c07a4 --- /dev/null +++ b/migrate/tests/test_plcc_ng_demo.py @@ -0,0 +1,142 @@ +import tempfile +import textwrap +import unittest +from pathlib import Path + +from migrate import drivers, plcc_ng, plcc_ng_demo, repo_links +from migrate.emit import render_task + + +def legacy(number, title, type_="fix", body="Body."): + return textwrap.dedent(f"""\ + # {number:03d} - {title} + + **Type:** {type_} + **Date:** 2026-07-21 + """) + f"\n## Description\n\n{body}\n\n## Notes\n\nN.\n" + + +class PlccNgDemoTest(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + base = Path(self._tmp.name) + self.repo, self.tracker = base / "plcc-ng-demo", base / "tracker" + done = self.repo / "dev-docs" / "issues" / "done" + done.mkdir(parents=True) + files = { + "001-migration-docs.md": legacy(1, "Migration docs", "docs"), + "002-race.md": legacy(2, "Race", "fix", body="Unlike #3, see plcc-ng #160."), + "003-div.md": legacy(3, "Div", "docs", body="See #1 and languages-ng #12."), + } + for name, text in files.items(): + (done / name).write_text(text) + (self.repo / "dev-docs" / "issues" / "TEMPLATE.md").write_text("# NNN - Title\n") + self.backlog = self.tracker / "backlog" + for folder in ("tasks", "completed", "drafts"): + (self.backlog / folder).mkdir(parents=True) + self.write_cr("completed", 159, "Migration docs upstream", "docs", "Done") + self.write_cr("tasks", 160, "Race upstream", "feat", "To Do") + self.triage = {"open": {}, "duplicates": {"1": 159, "2": 160}} + + def tearDown(self): + self._tmp.cleanup() + + def write_cr(self, folder, cr, title, type_, status): + description = f"Upstream.\n\nMigrated from plcc-ng #{cr:03d}." + (self.backlog / folder / f"cr-{cr} - {title.replace(' ', '-')}.md").write_text( + render_task(task_id=f"CR-{cr}", title=title, status=status, created="2026-07-22", + labels=[], description=description, type_=type_, project="plcc-ng")) + + def files(self, folder): + return sorted(p.name for p in (self.backlog / folder).glob("*.md")) + + def read(self, folder, name): + return (self.backlog / folder / name).read_text() + + def test_registered_driver(self): + self.assertIs(drivers.load("plcc-ng-demo"), plcc_ng_demo) + + def test_issue_refs_use_offset_800(self): + refs = plcc_ng_demo.issue_refs(plcc_ng_demo.load_issues(self.repo)) + self.assertEqual(refs[3][0].cr, 803) + + def test_expected_counts(self): + self.assertEqual(plcc_ng_demo.expected_counts(plcc_ng_demo.load_issues(self.repo), self.triage), + {"tasks": 0, "completed": 3, "drafts": 0}) + + def test_convert_places_files(self): + plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + self.assertEqual(self.files("completed"), + ["cr-159 - Migration-docs-upstream.md", "cr-801 - Migration-docs.md", + "cr-802 - Race.md", "cr-803 - Div.md"]) + self.assertEqual(self.files("tasks"), ["cr-160 - Race-upstream.md"]) + + def test_duplicates_are_wontdo_with_the_plcc_ng_type(self): + report = plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + race = self.read("completed", "cr-802 - Race.md") + for expected in ("status: Done", " - wontdo", "type: feat", "project: plcc-ng", + "Duplicate of CR-160, filed upstream by hand.", + "Migrated from plcc-ng-demo #002."): + self.assertIn(expected, race) + self.assertIn("#002 -> CR-802: duplicate of CR-160", report) + + def test_local_issue_content(self): + plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + div = self.read("completed", "cr-803 - Div.md") + for expected in ("status: Done", "type: docs", "project: plcc-ng-demo", + "See CR-801 and languages-ng CR-512.", "Migrated from plcc-ng-demo #003."): + self.assertIn(expected, div) + self.assertNotIn("wontdo", div) + race = self.read("completed", "cr-802 - Race.md") + self.assertIn("Unlike CR-803, see plcc-ng CR-160.", race) + + def test_duplicate_of_missing_cr(self): + self.triage["duplicates"]["3"] = 999 + with self.assertRaises(SystemExit) as cm: + plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + self.assertIn("CR-999", str(cm.exception)) + + def test_duplicate_key_that_is_not_an_issue(self): + self.triage["duplicates"]["7"] = 159 + with self.assertRaises(SystemExit) as cm: + plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + self.assertIn("'7'", str(cm.exception)) + + def test_provenance_is_per_repo(self): + plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + demo = self.backlog / "completed" / "cr-803 - Div.md" + upstream = self.backlog / "completed" / "cr-159 - Migration-docs-upstream.md" + self.assertTrue(plcc_ng_demo.is_migrated(demo)) + self.assertFalse(plcc_ng_demo.is_migrated(upstream)) + self.assertFalse(plcc_ng.is_migrated(demo)) + self.assertTrue(plcc_ng.is_migrated(upstream)) + + def test_rerun_keeps_plcc_ng_crs(self): + plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + self.assertIn("cr-159 - Migration-docs-upstream.md", self.files("completed")) + self.assertEqual(self.files("tasks"), ["cr-160 - Race-upstream.md"]) + + def test_report(self): + report = plcc_ng_demo.convert(self.repo, self.tracker, self.triage) + self.assertTrue(report.startswith("# plcc-ng-demo migration report")) + self.assertIn("Converted 3 files: 3 -> completed/, 0 -> tasks/, 0 -> drafts/.", report) + + def test_main_writes_report(self): + triage = self.tracker / "triage.json" + triage.write_text('{"open": {}, "duplicates": {"1": 159, "2": 160}}') + out = self.tracker / "reports" / "plcc-ng-demo.md" + self.assertEqual(plcc_ng_demo.main(["--repo", str(self.repo), "--tracker", str(self.tracker), + "--triage", str(triage), "--report", str(out)]), 0) + self.assertIn("duplicate of CR-159", out.read_text()) + + def test_repo_links_rewrite_to_demo_crs(self): + doc = self.repo / "dev-docs" / "notes.md" + doc.write_text("See [the race](issues/done/002-race.md).\n") + changed, _ = repo_links.rewrite_repo(self.repo, "plcc-ng-demo") + self.assertEqual(changed, ["dev-docs/notes.md"]) + self.assertEqual(doc.read_text(), "See the race (CR-802).\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/migrate/verify.py b/migrate/verify.py index 167bd5b..e60f038 100644 --- a/migrate/verify.py +++ b/migrate/verify.py @@ -1,6 +1,6 @@ """Verify a converted tracker against its legacy source. -Usage: python3 -m migrate.verify --repo REPO --tracker TRACKER --triage FILE [--driver plcc-ng|languages-ng] +Usage: python3 -m migrate.verify --repo REPO --tracker TRACKER --triage FILE [--driver plcc-ng|languages-ng|plcc-ng-demo] """ import argparse import json