oc_chat
Loading...
Searching...
No Matches
migrate.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/cli/migrate.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_migration.py"
6"""
7Migration operator commands for the orchestration registry.
8
9Purpose
10-------
11Expose the legacy Markdown-state migration lifecycle beneath
12``python -m oc_chat.registry migrate``.
13
14Responsibilities
15----------------
16- Wire import, verify, rollback, and cutover subcommands.
17- Resolve registry/workspace paths through the standard discovery logic.
18- Format migration results as human text or JSON.
19
20Diagnostics
21-----------
22Domain: OC_CHAT.MAIN
23Levels:
24 L2 - lifecycle events
25 L3 - semantic details
26 L4 - deep tracing
27
28Contracts
29---------
30- ``cutover`` is a v0.2.x stub and exits 2.
31- ``verify`` exits 1 when the registry differs from the legacy projection.
32
33Dependencies
34------------
35- oc_diagnostics (structured debug logging via _dbg)
36- oc_chat.registry.migrate (migration command implementations)
37- oc_chat.registry.discovery (registry path discovery)
38- oc_chat.registry (Registry facade passed to the migration commands)
39- oc_chat.registry.backends.sqlite (SQLiteBackend opened at the discovered registry path)
40"""
41from __future__ import annotations
42
43import argparse
44import json
45import sys
46from pathlib import Path
47from typing import Mapping
48
49from oc_diagnostics import _dbg
50
51from oc_chat.registry import Registry
52from oc_chat.registry.backends.sqlite import SQLiteBackend
53from oc_chat.registry.discovery import discover_registry_location
54from oc_chat.registry.migrate import (
55 MigrationOptions,
56 run_cutover_stub,
57 run_import,
58 run_rollback,
59 run_verify,
60)
61
62
63_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
64
65
66def add_migrate_subcommands(migrate_parser: argparse.ArgumentParser) -> None:
67 """Attach the M11.2 migration subcommands."""
68
69 migrate_parser.set_defaults(handler=_cmd_migrate_group, migrate_parser=migrate_parser)
70 subcommands = migrate_parser.add_subparsers(dest="migrate_command")
71
72 import_parser = subcommands.add_parser(
73 "import",
74 help="Import legacy queue/status files into the registry.",
75 )
76 import_parser.add_argument(
77 "registry",
78 nargs="?",
79 help="Registry SQLite path. Defaults to the discovery algorithm.",
80 )
81 import_parser.add_argument(
82 "--dry-run",
83 action="store_true",
84 help="Build and report the projection without writing.",
85 )
86 import_parser.add_argument(
87 "--json",
88 action="store_true",
89 help="Emit the structured import report as JSON.",
90 )
91 import_parser.set_defaults(handler=_cmd_import)
92
93 verify_parser = subcommands.add_parser(
94 "verify",
95 help="Compare the registry with the current legacy projection.",
96 )
97 verify_parser.add_argument(
98 "registry",
99 nargs="?",
100 help="Registry SQLite path. Defaults to the discovery algorithm.",
101 )
102 verify_parser.add_argument(
103 "--json",
104 action="store_true",
105 help="Emit the structured verify report as JSON.",
106 )
107 verify_parser.set_defaults(handler=_cmd_verify)
108
109 rollback_parser = subcommands.add_parser(
110 "rollback",
111 help="Restore the most recent pre-migration registry backup.",
112 )
113 rollback_parser.add_argument(
114 "registry",
115 nargs="?",
116 help="Registry SQLite path. Defaults to the discovery algorithm.",
117 )
118 rollback_parser.add_argument(
119 "--json",
120 action="store_true",
121 help="Emit the structured rollback report as JSON.",
122 )
123 rollback_parser.set_defaults(handler=_cmd_rollback)
124
125 cutover_parser = subcommands.add_parser(
126 "cutover",
127 help="Reserved for the M11.8 cutover phase.",
128 )
129 cutover_parser.add_argument(
130 "--json",
131 action="store_true",
132 help="Emit the structured cutover report as JSON.",
133 )
134 cutover_parser.set_defaults(handler=_cmd_cutover)
135
136
137def _cmd_migrate_group(args: argparse.Namespace) -> int:
138 parser = args.migrate_parser
139 parser.print_help()
140 return 1
141
142
143def _cmd_import(args: argparse.Namespace) -> int:
144 registry, options = _registry_and_options(args.registry)
145 result = run_import(registry, options, dry_run=args.dry_run)
146 _print_result(result.to_dict(), args.json, "migration import")
147 return 0 if result.ok else 1
148
149
150def _cmd_verify(args: argparse.Namespace) -> int:
151 registry, options = _registry_and_options(args.registry)
152 result = run_verify(registry, options)
153 _print_result(result.to_dict(), args.json, "migration verify")
154 return 0 if result.ok else 1
155
156
157def _cmd_rollback(args: argparse.Namespace) -> int:
158 registry, options = _registry_and_options(args.registry)
159 result = run_rollback(registry, options)
160 _print_result(result.to_dict(), args.json, "migration rollback")
161 return 0 if result.ok else 1
162
163
164def _cmd_cutover(args: argparse.Namespace) -> int:
165 result = run_cutover_stub()
166 if args.json:
167 print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
168 else:
169 print(result.message, file=sys.stderr)
170 return result.exit_code
171
172
174 registry_path: str | None,
175) -> tuple[Registry, MigrationOptions]:
176 location = discover_registry_location(registry_path=registry_path)
177 backend = SQLiteBackend(location.registry_path)
178 return (
179 Registry(backend),
180 MigrationOptions(workspace_root=Path(location.workspace_root)),
181 )
182
183
185 result: Mapping[str, object],
186 as_json: bool,
187 label: str,
188) -> None:
189 if as_json:
190 print(json.dumps(result, indent=2, sort_keys=True))
191 return
192 print(format_migrate_report(result, label))
193
194
195def format_migrate_report(result: Mapping[str, object], label: str) -> str:
196 """Return a compact human-readable migration report."""
197
198 ok = result.get("ok") is True
199 summary = result.get("summary", {})
200 lines = [f"{label} {'ok' if ok else 'failed'}"]
201 if isinstance(summary, Mapping):
202 counts = ", ".join(
203 f"{key}={value}"
204 for key, value in summary.items()
205 if key != "status_counts"
206 )
207 if counts:
208 lines.append(f" {counts}")
209 status_counts = summary.get("status_counts")
210 if isinstance(status_counts, Mapping):
211 rendered = ", ".join(f"{key}={value}" for key, value in status_counts.items())
212 lines.append(f" statuses: {rendered}")
213 backup_path = result.get("backup_path") or result.get("restored_from")
214 if backup_path:
215 lines.append(f" backup: {backup_path}")
216 for field in ("warnings", "errors"):
217 values = result.get(field, [])
218 if isinstance(values, list) and values:
219 lines.append(f" {field}:")
220 lines.extend(f" - {value}" for value in values)
221 if result.get("diff"):
222 lines.append(" diff: present")
223 return "\n".join(lines)
None add_migrate_subcommands(argparse.ArgumentParser migrate_parser)
Definition migrate.py:66
tuple[Registry, MigrationOptions] _registry_and_options(str|None registry_path)
Definition migrate.py:175
str format_migrate_report(Mapping[str, object] result, str label)
Definition migrate.py:195
int _cmd_cutover(argparse.Namespace args)
Definition migrate.py:164
None _print_result(Mapping[str, object] result, bool as_json, str label)
Definition migrate.py:188
int _cmd_import(argparse.Namespace args)
Definition migrate.py:143
int _cmd_migrate_group(argparse.Namespace args)
Definition migrate.py:137
int _cmd_rollback(argparse.Namespace args)
Definition migrate.py:157
int _cmd_verify(argparse.Namespace args)
Definition migrate.py:150