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.
- core_cli/__init__.py +3 -0
- core_cli/main.py +33 -0
- core_cli/resources/__init__.py +3 -0
- core_cli/resources/code/__init__.py +22 -0
- core_cli/resources/code/actions.py +54 -0
- core_cli/resources/code/audit_duplicates.py +37 -0
- core_cli/resources/code/bridges.py +90 -0
- core_cli/resources/code/check_imports.py +75 -0
- core_cli/resources/code/check_ui.py +61 -0
- core_cli/resources/code/docstrings.py +44 -0
- core_cli/resources/code/fix_atomic.py +57 -0
- core_cli/resources/code/format.py +62 -0
- core_cli/resources/code/hub.py +10 -0
- core_cli/resources/code/integrity.py +75 -0
- core_cli/resources/code/lint.py +37 -0
- core_cli/resources/code/logging.py +55 -0
- core_cli/resources/code/test.py +32 -0
- core_cli/resources/lane/__init__.py +26 -0
- core_cli/resources/lane/claim.py +48 -0
- core_cli/resources/lane/list.py +50 -0
- core_cli/resources/lane/next.py +82 -0
- core_cli/resources/lane/propose.py +119 -0
- core_cli/resources/project/__init__.py +21 -0
- core_cli/resources/project/docs.py +41 -0
- core_cli/resources/project/onboard.py +110 -0
- core_cli/resources/project/scout.py +288 -0
- core_cli/resources/proposals/__init__.py +27 -0
- core_cli/resources/proposals/create.py +99 -0
- core_cli/resources/proposals/integrate.py +57 -0
- core_cli/resources/proposals/list.py +115 -0
- core_cli/resources/proposals/manage.py +183 -0
- core_cli/resources/secrets/__init__.py +10 -0
- core_cli/resources/secrets/hub.py +9 -0
- core_cli/resources/secrets/manage.py +160 -0
- core_cli/resources/symbols/__init__.py +14 -0
- core_cli/resources/symbols/audit.py +56 -0
- core_cli/resources/symbols/fix_ids.py +39 -0
- core_cli/resources/symbols/hub.py +11 -0
- core_cli/resources/symbols/resolve_duplicates.py +37 -0
- core_cli/resources/symbols/sync.py +39 -0
- core_cli/resources/vectors/__init__.py +15 -0
- core_cli/resources/vectors/hub.py +9 -0
- core_cli/resources/vectors/query.py +66 -0
- core_cli/resources/vectors/rebuild.py +71 -0
- core_cli/resources/vectors/status.py +40 -0
- core_cli/resources/vectors/sync.py +50 -0
- core_cli/resources/vectors/sync_code.py +58 -0
- core_cli-1.0.0.dist-info/METADATA +72 -0
- core_cli-1.0.0.dist-info/RECORD +52 -0
- core_cli-1.0.0.dist-info/WHEEL +4 -0
- core_cli-1.0.0.dist-info/entry_points.txt +2 -0
- 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,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,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,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,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("")
|