core-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. core_cli/__init__.py +3 -0
  2. core_cli/main.py +33 -0
  3. core_cli/resources/__init__.py +3 -0
  4. core_cli/resources/code/__init__.py +22 -0
  5. core_cli/resources/code/actions.py +54 -0
  6. core_cli/resources/code/audit_duplicates.py +37 -0
  7. core_cli/resources/code/bridges.py +90 -0
  8. core_cli/resources/code/check_imports.py +75 -0
  9. core_cli/resources/code/check_ui.py +61 -0
  10. core_cli/resources/code/docstrings.py +44 -0
  11. core_cli/resources/code/fix_atomic.py +57 -0
  12. core_cli/resources/code/format.py +62 -0
  13. core_cli/resources/code/hub.py +10 -0
  14. core_cli/resources/code/integrity.py +75 -0
  15. core_cli/resources/code/lint.py +37 -0
  16. core_cli/resources/code/logging.py +55 -0
  17. core_cli/resources/code/test.py +32 -0
  18. core_cli/resources/lane/__init__.py +26 -0
  19. core_cli/resources/lane/claim.py +48 -0
  20. core_cli/resources/lane/list.py +50 -0
  21. core_cli/resources/lane/next.py +82 -0
  22. core_cli/resources/lane/propose.py +119 -0
  23. core_cli/resources/project/__init__.py +21 -0
  24. core_cli/resources/project/docs.py +41 -0
  25. core_cli/resources/project/onboard.py +110 -0
  26. core_cli/resources/project/scout.py +288 -0
  27. core_cli/resources/proposals/__init__.py +27 -0
  28. core_cli/resources/proposals/create.py +99 -0
  29. core_cli/resources/proposals/integrate.py +57 -0
  30. core_cli/resources/proposals/list.py +115 -0
  31. core_cli/resources/proposals/manage.py +183 -0
  32. core_cli/resources/secrets/__init__.py +10 -0
  33. core_cli/resources/secrets/hub.py +9 -0
  34. core_cli/resources/secrets/manage.py +160 -0
  35. core_cli/resources/symbols/__init__.py +14 -0
  36. core_cli/resources/symbols/audit.py +56 -0
  37. core_cli/resources/symbols/fix_ids.py +39 -0
  38. core_cli/resources/symbols/hub.py +11 -0
  39. core_cli/resources/symbols/resolve_duplicates.py +37 -0
  40. core_cli/resources/symbols/sync.py +39 -0
  41. core_cli/resources/vectors/__init__.py +15 -0
  42. core_cli/resources/vectors/hub.py +9 -0
  43. core_cli/resources/vectors/query.py +66 -0
  44. core_cli/resources/vectors/rebuild.py +71 -0
  45. core_cli/resources/vectors/status.py +40 -0
  46. core_cli/resources/vectors/sync.py +50 -0
  47. core_cli/resources/vectors/sync_code.py +58 -0
  48. core_cli-1.0.0.dist-info/METADATA +72 -0
  49. core_cli-1.0.0.dist-info/RECORD +52 -0
  50. core_cli-1.0.0.dist-info/WHEEL +4 -0
  51. core_cli-1.0.0.dist-info/entry_points.txt +2 -0
  52. core_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,183 @@
1
+ # src/core_cli/resources/proposals/manage.py
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+
7
+ import httpx
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from api.cli import CoreApiClient
12
+ from cli.utils import core_command
13
+
14
+
15
+ console = Console()
16
+
17
+
18
+ def _print_detailed_info(p: dict) -> None:
19
+ console.print(f"\n[bold cyan]Proposal: {p['proposal_id']}[/bold cyan]\n")
20
+ console.print(f"[bold]Goal:[/bold] {p.get('goal', '')}")
21
+ console.print(f"[bold]Status:[/bold] {p['status']}")
22
+ console.print(f"[bold]Created:[/bold] {p['created_at']}")
23
+ console.print(f"[bold]Created By:[/bold] {p.get('created_by', '')}\n")
24
+ risk = p.get("risk")
25
+ if risk:
26
+ console.print("[bold]Risk Assessment:[/bold]")
27
+ console.print(f" Overall: {risk['overall_risk']}")
28
+ approval = "Yes" if p.get("approval_required") else "No"
29
+ console.print(f" Approval Required: {approval}")
30
+ for factor in risk.get("risk_factors", []):
31
+ console.print(f" - {factor}")
32
+ console.print("")
33
+ actions = p.get("actions", [])
34
+ console.print(f"[bold]Actions ({len(actions)}):[/bold]")
35
+ for a in sorted(actions, key=lambda x: x.get("order", 0)):
36
+ ref = a.get("action_id") or a.get("flow_id") or "?"
37
+ console.print(f" {a.get('order', 0) + 1}. {ref}")
38
+ if a.get("parameters"):
39
+ console.print(f" Parameters: {a['parameters']}")
40
+ scope = p.get("scope") or {}
41
+ files = scope.get("files") or []
42
+ modules = scope.get("modules") or []
43
+ if files or modules:
44
+ console.print("\n[bold]Scope:[/bold]")
45
+ if files:
46
+ console.print(f" Files: {len(files)}")
47
+ if modules:
48
+ console.print(f" Modules: {', '.join(modules)}")
49
+ started = p.get("execution_started_at")
50
+ completed = p.get("execution_completed_at")
51
+ if started:
52
+ console.print("\n[bold]Execution:[/bold]")
53
+ console.print(f" Started: {started}")
54
+ if completed:
55
+ dur = (
56
+ datetime.fromisoformat(completed) - datetime.fromisoformat(started)
57
+ ).total_seconds()
58
+ console.print(f" Completed: {completed}")
59
+ console.print(f" Duration: {dur}s")
60
+ failure_reason = p.get("failure_reason")
61
+ if failure_reason:
62
+ console.print(f"\n[red]Failure Reason: {failure_reason}[/red]")
63
+
64
+
65
+ def _print_execution_summary(result: dict) -> None:
66
+ if not result.get("ok") and "actions_executed" not in result:
67
+ console.print(f"Error: {result.get('error', 'Unknown error')}")
68
+ return
69
+ console.print(f"Actions executed: {result.get('actions_executed', 0)}")
70
+ console.print(f"Succeeded: {result.get('actions_succeeded', 0)}")
71
+ console.print(f"Failed: {result.get('actions_failed', 0)}")
72
+ console.print(f"Duration: {result.get('duration_sec', 0)}s\n")
73
+ console.print("[bold]Action Results:[/bold]")
74
+ for action_id, res in result.get("action_results", {}).items():
75
+ mark = "[green]✓[/green]" if res["ok"] else "[red]✗[/red]"
76
+ console.print(f" {mark} {action_id}: {res['duration_sec']}s")
77
+ if not res["ok"]:
78
+ err = res.get("data", {}).get("error", "Unknown error")
79
+ console.print(f" [red]{err}[/red]")
80
+
81
+
82
+ @core_command(dangerous=False, requires_context=False)
83
+ # ID: 9bacc55b-be1d-4f71-a27e-6e83ba176e33
84
+ async def show_proposal(proposal_id: str = typer.Argument(...)) -> None:
85
+ """Show detailed breakdown and risk assessment of a proposal."""
86
+ client = CoreApiClient()
87
+ try:
88
+ proposal = await client.get_proposal(proposal_id)
89
+ except httpx.HTTPStatusError as exc:
90
+ if exc.response.status_code == 404:
91
+ console.print(f"[red]Proposal {proposal_id} not found.[/red]")
92
+ raise typer.Exit(1) from exc
93
+ raise
94
+ _print_detailed_info(proposal)
95
+
96
+
97
+ @core_command(dangerous=False, requires_context=False)
98
+ # ID: f2e065f7-c253-4c33-ae0d-5374ffdb8e23
99
+ async def approve_proposal(
100
+ proposal_id: str = typer.Argument(...),
101
+ by: str = typer.Option("cli_admin", "--by", help="Approver identity."),
102
+ authority: str = typer.Option(
103
+ "principal.governor",
104
+ "--authority",
105
+ help="Authority under which approval is granted (URS NFR.5).",
106
+ ),
107
+ ) -> None:
108
+ """Authorize a pending proposal for execution."""
109
+ client = CoreApiClient()
110
+ try:
111
+ response = await client.approve_proposal(
112
+ proposal_id, approved_by=by, approval_authority=authority
113
+ )
114
+ except httpx.HTTPStatusError as exc:
115
+ status_code = exc.response.status_code
116
+ if status_code == 404:
117
+ console.print(f"[red]Proposal {proposal_id} not found.[/red]")
118
+ raise typer.Exit(1) from exc
119
+ if status_code == 400:
120
+ try:
121
+ detail = exc.response.json().get("detail", exc.response.text)
122
+ except ValueError:
123
+ detail = exc.response.text
124
+ console.print(f"[red]{detail}[/red]")
125
+ raise typer.Exit(1) from exc
126
+ raise
127
+
128
+ console.print(
129
+ f"[green]✅ Proposal {proposal_id} APPROVED by "
130
+ f"{response['approved_by']} under {response['approval_authority']}.[/green]"
131
+ )
132
+
133
+
134
+ @core_command(dangerous=True, confirmation=True, requires_context=False)
135
+ # ID: f4cdc45a-2f42-4916-b4e3-a305b5357a9d
136
+ async def execute_proposal(
137
+ proposal_id: str = typer.Argument(...),
138
+ write: bool = typer.Option(False, "--write", help="Apply changes to the system."),
139
+ ) -> None:
140
+ """Execute an approved proposal.
141
+
142
+ Runs the atomic action sequence defined in the proposal.
143
+ """
144
+ if not write:
145
+ console.print("[yellow]💡 Dry-run: simulating execution steps...[/yellow]\n")
146
+ client = CoreApiClient()
147
+ result = await client.execute_proposal(proposal_id, write=write)
148
+ if result.get("ok"):
149
+ console.print(
150
+ f"\n[bold green]✅ Execution Successful: {proposal_id}[/bold green]"
151
+ )
152
+ else:
153
+ console.print(f"\n[bold red]❌ Execution Failed: {proposal_id}[/bold red]")
154
+ _print_execution_summary(result)
155
+
156
+
157
+ @core_command(dangerous=False, requires_context=False)
158
+ # ID: 4ac3cfc1-feae-440c-b02f-4c57a6a1147d
159
+ async def reject_proposal(
160
+ proposal_id: str = typer.Argument(...),
161
+ reason: str = typer.Option(..., "--reason", "-r"),
162
+ ) -> None:
163
+ """Reject a proposal and prevent its execution.
164
+
165
+ Revival of deferred findings (ADR-010 §7a / ADR-045) now happens
166
+ on the API side; this command only renders the result.
167
+ """
168
+ client = CoreApiClient()
169
+ try:
170
+ response = await client.reject_proposal(proposal_id, reason=reason)
171
+ except httpx.HTTPStatusError as exc:
172
+ if exc.response.status_code == 404:
173
+ console.print(f"[red]Proposal {proposal_id} not found.[/red]")
174
+ raise typer.Exit(1) from exc
175
+ raise
176
+
177
+ console.print(f"[yellow]🚫 Proposal {response['proposal_id']} REJECTED.[/yellow]")
178
+ revived_count = response.get("revived_count", 0)
179
+ if revived_count > 0:
180
+ console.print(
181
+ f"[cyan] Revived {revived_count} deferred "
182
+ f"finding(s) to awaiting_reaudit for sensor adjudication.[/cyan]"
183
+ )
@@ -0,0 +1,10 @@
1
+ # src/cli/resources/secrets/__init__.py
2
+ """Secrets resource hub."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from . import manage # noqa: F401
7
+ from .hub import app
8
+
9
+
10
+ __all__ = ["app"]
@@ -0,0 +1,9 @@
1
+ # src/cli/resources/secrets/hub.py
2
+ import typer
3
+
4
+
5
+ app = typer.Typer(
6
+ name="secrets",
7
+ help="Manage encrypted secrets in the database.",
8
+ no_args_is_help=True,
9
+ )
@@ -0,0 +1,160 @@
1
+ # src/core_cli/resources/secrets/manage.py
2
+
3
+ """Encrypted secrets management — consumer CLI over HTTP (ADR-146 D2)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from api.cli import CoreApiClient
12
+ from cli.utils import core_command
13
+
14
+ from .hub import app
15
+
16
+
17
+ console = Console()
18
+
19
+
20
+ @app.command("set")
21
+ @core_command(dangerous=True, requires_context=False)
22
+ # ID: 92bcc7e9-189f-45d0-bc18-21512bcba69f
23
+ async def set_secret(
24
+ ctx: typer.Context,
25
+ key: str = typer.Argument(..., help="Secret key (e.g., 'anthropic.api_key')"),
26
+ value: str = typer.Option(
27
+ ...,
28
+ "--value",
29
+ "-v",
30
+ prompt=True,
31
+ hide_input=True,
32
+ help="Secret value (will be encrypted at rest)",
33
+ ),
34
+ description: str | None = typer.Option(
35
+ None, "--description", "-d", help="Optional description"
36
+ ),
37
+ force: bool = typer.Option(
38
+ False, "--force", "-f", help="Overwrite without 409 error if key exists"
39
+ ),
40
+ ) -> None:
41
+ """Store an encrypted secret in the CORE installation."""
42
+ if not key.strip():
43
+ console.print("[red]Secret key cannot be empty.[/red]")
44
+ raise typer.Exit(code=1)
45
+ client = CoreApiClient()
46
+ try:
47
+ result = await client.secrets.set_secret(
48
+ key=key, value=value, description=description, force=force
49
+ )
50
+ except Exception as exc:
51
+ import httpx
52
+
53
+ if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 409:
54
+ console.print(
55
+ f"[red]Secret '{key}' already exists. Use --force to overwrite.[/red]"
56
+ )
57
+ raise typer.Exit(code=1) from exc
58
+ console.print(f"[red]Failed to store secret: {exc}[/red]")
59
+ raise typer.Exit(code=1) from exc
60
+ action = result.get("action", "stored")
61
+ console.print(f"[green]Secret '{key}' {action}.[/green]")
62
+
63
+
64
+ @app.command("get")
65
+ @core_command(dangerous=False, requires_context=False)
66
+ # ID: 01608ce2-3fb8-4f0d-ae1a-abaec72369d7
67
+ async def get(
68
+ ctx: typer.Context,
69
+ key: str = typer.Argument(..., help="Secret key to retrieve"),
70
+ show: bool = typer.Option(False, "--show", "-s", help="Display the secret value"),
71
+ ) -> None:
72
+ """Check whether a secret exists (optionally reveal value with --show)."""
73
+ client = CoreApiClient()
74
+ try:
75
+ result = await client.secrets.get_secret(key=key, show=show)
76
+ except Exception as exc:
77
+ import httpx
78
+
79
+ if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 404:
80
+ console.print(f"[red]Secret '{key}' not found.[/red]")
81
+ raise typer.Exit(code=1) from exc
82
+ raise
83
+ if show:
84
+ console.print(f"[bold]Secret '{key}':[/bold]")
85
+ console.print(result.get("value", ""))
86
+ else:
87
+ console.print(f"[green]Secret '{key}' exists.[/green]")
88
+
89
+
90
+ @app.command("list")
91
+ @core_command(dangerous=False, requires_context=False)
92
+ # ID: cf23ee88-7f5e-47e9-91de-7414ef0c36ed
93
+ async def list_secrets(ctx: typer.Context) -> None:
94
+ """List all secret keys in the CORE installation (values not shown)."""
95
+ client = CoreApiClient()
96
+ result = await client.secrets.list_secrets()
97
+ secrets = result.get("secrets") or []
98
+ if not secrets:
99
+ console.print("[yellow]No secrets found.[/yellow]")
100
+ return
101
+ table = Table(title="Encrypted Secrets")
102
+ table.add_column("Key", style="cyan", no_wrap=True)
103
+ table.add_column("Last Rotated", style="dim")
104
+ table.add_column("Created", style="dim")
105
+ for s in secrets:
106
+ table.add_row(
107
+ s["key"],
108
+ str(s.get("last_rotated_at") or "never"),
109
+ str(s.get("created_at") or "unknown"),
110
+ )
111
+ console.print(table)
112
+ console.print(f"[dim]Total: {result.get('count', len(secrets))} secrets[/dim]")
113
+
114
+
115
+ @app.command("delete")
116
+ @core_command(dangerous=True, requires_context=False)
117
+ # ID: cc76acdd-0ef9-4602-86b0-09580546dd05
118
+ async def delete(
119
+ ctx: typer.Context,
120
+ key: str = typer.Argument(..., help="Secret key to delete"),
121
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
122
+ ) -> None:
123
+ """Permanently delete a secret from the CORE installation."""
124
+ if not yes:
125
+ typer.confirm(f"Delete secret '{key}'?", abort=True)
126
+ client = CoreApiClient()
127
+ try:
128
+ await client.secrets.delete_secret(key)
129
+ except Exception as exc:
130
+ import httpx
131
+
132
+ if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 404:
133
+ console.print(f"[red]Secret '{key}' not found.[/red]")
134
+ raise typer.Exit(code=1) from exc
135
+ raise
136
+ console.print(f"[green]Secret '{key}' deleted.[/green]")
137
+
138
+
139
+ @app.command("rotate")
140
+ @core_command(dangerous=True, requires_context=False)
141
+ # ID: 634e4701-120b-4a5e-88ef-f4cb0db315a9
142
+ async def rotate(
143
+ ctx: typer.Context,
144
+ key: str = typer.Argument(..., help="Secret key to rotate"),
145
+ new_value: str = typer.Option(
146
+ ..., "--value", "-v", prompt=True, hide_input=True, help="New secret value"
147
+ ),
148
+ ) -> None:
149
+ """Rotate the value of an existing secret (updates last_rotated_at)."""
150
+ client = CoreApiClient()
151
+ try:
152
+ await client.secrets.rotate_secret(key, new_value)
153
+ except Exception as exc:
154
+ import httpx
155
+
156
+ if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 404:
157
+ console.print(f"[red]Secret '{key}' not found.[/red]")
158
+ raise typer.Exit(code=1) from exc
159
+ raise
160
+ console.print(f"[green]Secret '{key}' rotated.[/green]")
@@ -0,0 +1,14 @@
1
+ """Consumer symbol-registry commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import ( # noqa: F401
6
+ audit,
7
+ fix_ids,
8
+ resolve_duplicates,
9
+ sync,
10
+ )
11
+ from .hub import app
12
+
13
+
14
+ __all__ = ["app"]
@@ -0,0 +1,56 @@
1
+ # src/core_cli/resources/symbols/audit.py
2
+
3
+ """Symbol audit command — consumer CLI over HTTP (ADR-146 D2)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from api.cli import CoreApiClient
11
+ from cli.utils import core_command
12
+
13
+ from .hub import app
14
+
15
+
16
+ console = Console()
17
+
18
+
19
+ @app.command("audit")
20
+ @core_command(dangerous=False, requires_context=False)
21
+ # ID: 2c32679a-ad9d-48e3-8cb0-b4cec7adea29
22
+ async def audit_symbols(ctx: typer.Context) -> None:
23
+ """Audit symbol integrity: drift summary and unassigned ID report."""
24
+ client = CoreApiClient()
25
+
26
+ console.print("[bold cyan]1. Checking for Symbol Drift (Disk vs DB)...[/bold cyan]")
27
+ drift = await client.symbols.get_drift()
28
+ if not drift.get("available", True):
29
+ console.print(f"[yellow]Drift data unavailable: {drift.get('error')}[/yellow]")
30
+ else:
31
+ violations = drift.get("anchor_violations", 0)
32
+ pending = drift.get("pending_symbols", 0)
33
+ last_sync = drift.get("last_sync_at") or "never"
34
+ if violations == 0 and pending == 0:
35
+ console.print("[green]No drift detected.[/green]")
36
+ else:
37
+ if violations:
38
+ console.print(f"[yellow]{violations} open anchor violation(s).[/yellow]")
39
+ if pending:
40
+ console.print(f"[yellow]{pending} symbol(s) pending classification.[/yellow]")
41
+ console.print(f"[dim]Last sync: {last_sync}[/dim]")
42
+
43
+ console.print("\n[bold cyan]2. Checking for Unassigned IDs...[/bold cyan]")
44
+ result = await client.symbols.get_unassigned()
45
+ unassigned = result.get("unassigned", [])
46
+ if not unassigned:
47
+ console.print("[green]All public symbols have assigned IDs.[/green]")
48
+ else:
49
+ console.print(f"[yellow]{len(unassigned)} symbol(s) with no ID tag.[/yellow]")
50
+ for item in unassigned[:10]:
51
+ console.print(f" - {item.get('name')} ({item.get('file_path')})")
52
+ if len(unassigned) > 10:
53
+ console.print(f" ... and {len(unassigned) - 10} more.")
54
+ console.print(
55
+ "\n[dim]Tip: Run 'core-admin symbols fix-ids --write' to fix this.[/dim]"
56
+ )
@@ -0,0 +1,39 @@
1
+ # src/core_cli/resources/symbols/fix_ids.py
2
+
3
+ """Assign stable # ID: anchors to untagged symbols — consumer CLI over HTTP (ADR-146 D2)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from api.cli import CoreApiClient
11
+ from cli.utils import core_command
12
+
13
+ from .hub import app
14
+
15
+
16
+ console = Console()
17
+
18
+
19
+ @app.command("fix-ids")
20
+ @core_command(dangerous=True, requires_context=False, confirmation=True)
21
+ # ID: 37ac33b4-76d3-40c4-8955-3b81a2a4ccf2
22
+ async def fix_ids_command(
23
+ ctx: typer.Context,
24
+ write: bool = typer.Option(
25
+ False, "--write", help="Inject missing UUIDs into source files."
26
+ ),
27
+ ) -> None:
28
+ """Assign stable '# ID:' anchors to all untagged public symbols.
29
+
30
+ Scans 'src/' and modifies files to ensure knowledge graph stability.
31
+ Pass --write to apply changes (default is dry-run).
32
+ """
33
+ mode = "WRITE" if write else "DRY-RUN"
34
+ console.print(f"[bold cyan]Fixing symbol IDs ({mode})...[/bold cyan]")
35
+ client = CoreApiClient()
36
+ result = await client.fix.run_fix("fix.ids", write=write)
37
+ run_id = result.get("run_id")
38
+ console.print(f"[green]Dispatched fix.ids run {run_id}.[/green]")
39
+ console.print(f"[dim]Poll with: core-admin fix status {run_id}[/dim]")
@@ -0,0 +1,11 @@
1
+ # src/cli/resources/symbols/hub.py
2
+
3
+ import typer
4
+
5
+
6
+ # Stable hub for symbols resource commands
7
+ app = typer.Typer(
8
+ name="symbols",
9
+ help="Operations for the symbol registry and Knowledge Graph identification.",
10
+ no_args_is_help=True,
11
+ )
@@ -0,0 +1,37 @@
1
+ # src/core_cli/resources/symbols/resolve_duplicates.py
2
+
3
+ """Resolve duplicate symbol IDs — consumer CLI over HTTP (ADR-146 D2)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from api.cli import CoreApiClient
11
+ from cli.utils import core_command
12
+
13
+ from .hub import app
14
+
15
+
16
+ console = Console()
17
+
18
+
19
+ @app.command("resolve-duplicates")
20
+ @core_command(dangerous=True, requires_context=False, confirmation=True)
21
+ # ID: c9ca3aa7-a542-4f3c-bbf3-d8dc97d4400a
22
+ async def resolve_symbol_duplicates(
23
+ ctx: typer.Context,
24
+ write: bool = typer.Option(False, "--write", help="Regenerate conflicting UUIDs."),
25
+ ) -> None:
26
+ """Find and resolve duplicate '# ID:' anchors in the codebase.
27
+
28
+ The older entry is preserved; colliding symbols receive fresh identifiers.
29
+ Pass --write to apply changes (default is dry-run).
30
+ """
31
+ mode = "RESOLVING" if write else "ANALYZING"
32
+ console.print(f"[bold cyan]{mode} duplicate ID collisions...[/bold cyan]")
33
+ client = CoreApiClient()
34
+ result = await client.fix.run_fix("fix.duplicate_ids", write=write)
35
+ run_id = result.get("run_id")
36
+ console.print(f"[green]Dispatched fix.duplicate_ids run {run_id}.[/green]")
37
+ console.print(f"[dim]Poll with: core-admin fix status {run_id}[/dim]")
@@ -0,0 +1,39 @@
1
+ # src/core_cli/resources/symbols/sync.py
2
+
3
+ """Synchronize filesystem symbols with the DB — consumer CLI over HTTP (ADR-146 D2)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from api.cli import CoreApiClient
11
+ from cli.utils import core_command
12
+
13
+ from .hub import app
14
+
15
+
16
+ console = Console()
17
+
18
+
19
+ @app.command("sync")
20
+ @core_command(dangerous=True, requires_context=False)
21
+ # ID: 7de43597-32ac-4e0f-9e55-af90f4c716f6
22
+ async def sync_symbols(
23
+ ctx: typer.Context,
24
+ write: bool = typer.Option(
25
+ False, "--write", help="Apply synchronization to the database."
26
+ ),
27
+ ) -> None:
28
+ """Synchronize filesystem symbols with the PostgreSQL Knowledge Graph.
29
+
30
+ Scans 'src/' and updates the 'core.symbols' table.
31
+ Pass --write to apply changes (default is dry-run).
32
+ """
33
+ mode = "WRITE" if write else "DRY-RUN"
34
+ console.print(f"[bold cyan]Synchronizing Symbols to DB ({mode})...[/bold cyan]")
35
+ client = CoreApiClient()
36
+ result = await client.fix.run_fix("sync.db", write=write)
37
+ run_id = result.get("run_id")
38
+ console.print(f"[green]Dispatched sync.db run {run_id}.[/green]")
39
+ console.print(f"[dim]Poll with: core-admin fix status {run_id}[/dim]")
@@ -0,0 +1,15 @@
1
+ """Consumer vector-store commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import ( # noqa: F401
6
+ query,
7
+ rebuild,
8
+ status,
9
+ sync,
10
+ sync_code,
11
+ )
12
+ from .hub import app
13
+
14
+
15
+ __all__ = ["app"]
@@ -0,0 +1,9 @@
1
+ # src/cli/resources/vectors/hub.py
2
+ import typer
3
+
4
+
5
+ app = typer.Typer(
6
+ name="vectors",
7
+ help="Vector store operations (Qdrant).",
8
+ no_args_is_help=True,
9
+ )
@@ -0,0 +1,66 @@
1
+ # src/core_cli/resources/vectors/query.py
2
+
3
+ """Semantic vector search command — consumer CLI over HTTP (ADR-146 D2)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from api.cli import CoreApiClient
11
+ from cli.utils import core_command
12
+
13
+ from .hub import app
14
+
15
+
16
+ console = Console()
17
+
18
+
19
+ @app.command("query")
20
+ @core_command(requires_context=False)
21
+ # ID: 44056798-de41-4934-8bba-97e6f88ce1f0
22
+ async def query_vectors(
23
+ ctx: typer.Context,
24
+ query: str = typer.Argument(..., help="Natural language query"),
25
+ collection: str = typer.Option(
26
+ "policies",
27
+ "--collection",
28
+ "-c",
29
+ help="Collection to query: 'policies', 'patterns', 'specs', or 'code'",
30
+ ),
31
+ limit: int = typer.Option(5, "--limit", "-n", help="Max results to return"),
32
+ ) -> None:
33
+ """Semantic search in vector collections.
34
+
35
+ Search constitutional documents using natural language queries.
36
+
37
+ Examples:
38
+ core-admin vectors query "file access rules"
39
+ core-admin vectors query "atomic actions" --collection patterns --limit 3
40
+ """
41
+ console.print(f"[bold cyan]Querying {collection}[/bold cyan]")
42
+ console.print(f"Query: {query}")
43
+ console.print()
44
+ client = CoreApiClient()
45
+ try:
46
+ result = await client.vectors.query(query, collection=collection, limit=limit)
47
+ except Exception as exc:
48
+ console.print(f"[red]Error: {exc}[/red]")
49
+ raise typer.Exit(1) from exc
50
+
51
+ results = result.get("results", [])
52
+ if not results:
53
+ console.print("[yellow]No results found[/yellow]")
54
+ return
55
+
56
+ console.print(f"[bold]Top {len(results)} results:[/bold]")
57
+ console.print("")
58
+ for i, hit in enumerate(results, 1):
59
+ score = hit.get("score", 0.0)
60
+ payload = hit.get("payload") or {}
61
+ content = payload.get("content") or payload.get("text") or ""
62
+ doc_id = payload.get("doc_id") or payload.get("chunk_id") or "Unknown"
63
+ content_preview = content[:200] if content else "[No content available]"
64
+ console.print(f"[bold cyan]{i}. {doc_id}[/bold cyan] (score: {score:.4f})")
65
+ console.print(f" {content_preview}...")
66
+ console.print("")