code-atlas-forge 1.1.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.
- code_atlas/__init__.py +6 -0
- code_atlas/__main__.py +6 -0
- code_atlas/api/__init__.py +5 -0
- code_atlas/api/handlers.py +1514 -0
- code_atlas/api/server.py +394 -0
- code_atlas/cli.py +2084 -0
- code_atlas/config.py +149 -0
- code_atlas/context/__init__.py +66 -0
- code_atlas/context/contracts.py +277 -0
- code_atlas/context/domain.py +760 -0
- code_atlas/context/engine.py +442 -0
- code_atlas/context/expand.py +133 -0
- code_atlas/context/renderers.py +71 -0
- code_atlas/context/tokenizer.py +37 -0
- code_atlas/contracts/__init__.py +38 -0
- code_atlas/contracts/enums.py +67 -0
- code_atlas/contracts/models.py +175 -0
- code_atlas/contracts/ports.py +70 -0
- code_atlas/discovery/__init__.py +25 -0
- code_atlas/discovery/gitignore.py +119 -0
- code_atlas/discovery/identity.py +52 -0
- code_atlas/discovery/models.py +204 -0
- code_atlas/discovery/scanner.py +495 -0
- code_atlas/domains/__init__.py +107 -0
- code_atlas/domains/api/__init__.py +28 -0
- code_atlas/domains/api/adapter.py +405 -0
- code_atlas/domains/api/common.py +308 -0
- code_atlas/domains/api/consumers.py +224 -0
- code_atlas/domains/api/emit.py +231 -0
- code_atlas/domains/api/openapi.py +398 -0
- code_atlas/domains/api/routes.py +582 -0
- code_atlas/domains/api/yaml_subset.py +683 -0
- code_atlas/domains/aws/__init__.py +28 -0
- code_atlas/domains/aws/adapter.py +382 -0
- code_atlas/domains/aws/common.py +261 -0
- code_atlas/domains/aws/configs.py +1164 -0
- code_atlas/domains/aws/emit.py +411 -0
- code_atlas/domains/aws/pyfiles.py +2220 -0
- code_atlas/domains/aws/sqlbridge.py +448 -0
- code_atlas/domains/cicd/__init__.py +50 -0
- code_atlas/domains/cicd/adapter.py +762 -0
- code_atlas/domains/cicd/classify.py +136 -0
- code_atlas/domains/cicd/workflow.py +504 -0
- code_atlas/domains/cicd/yamlmini.py +946 -0
- code_atlas/domains/cli_surface.py +739 -0
- code_atlas/domains/contracts.py +274 -0
- code_atlas/domains/crossresolve.py +1208 -0
- code_atlas/domains/evidence.py +57 -0
- code_atlas/domains/identity.py +149 -0
- code_atlas/domains/model.py +202 -0
- code_atlas/domains/registry.py +250 -0
- code_atlas/domains/resolution.py +65 -0
- code_atlas/domains/spark/__init__.py +31 -0
- code_atlas/domains/spark/adapter.py +146 -0
- code_atlas/domains/spark/emit.py +344 -0
- code_atlas/domains/spark/extract.py +108 -0
- code_atlas/domains/spark/sqllit.py +192 -0
- code_atlas/domains/spark/walk.py +1943 -0
- code_atlas/domains/sql/__init__.py +30 -0
- code_atlas/domains/sql/adapter.py +382 -0
- code_atlas/domains/sql/extract.py +1271 -0
- code_atlas/domains/sql/nodes.py +264 -0
- code_atlas/domains/sql/parser.py +1510 -0
- code_atlas/domains/sql/tokens.py +318 -0
- code_atlas/domains/summarize.py +126 -0
- code_atlas/domains/terraform/__init__.py +20 -0
- code_atlas/domains/terraform/adapter.py +127 -0
- code_atlas/domains/terraform/catalog.py +43 -0
- code_atlas/domains/terraform/extract.py +1277 -0
- code_atlas/domains/terraform/hcl.py +538 -0
- code_atlas/domains/terraform/refs.py +181 -0
- code_atlas/domains/terraform/tfjson.py +163 -0
- code_atlas/domains/terraform/tfvars.py +98 -0
- code_atlas/domains/validation.py +120 -0
- code_atlas/env_override.py +27 -0
- code_atlas/eval/__init__.py +26 -0
- code_atlas/eval/domain_eval.py +681 -0
- code_atlas/eval/ground_truth.py +688 -0
- code_atlas/eval/impact_truth.py +758 -0
- code_atlas/eval/metrics.py +178 -0
- code_atlas/eval/pack_value.py +610 -0
- code_atlas/federation/__init__.py +72 -0
- code_atlas/federation/federation.py +676 -0
- code_atlas/federation/model.py +355 -0
- code_atlas/federation/store.py +194 -0
- code_atlas/gitbin.py +48 -0
- code_atlas/graph/__init__.py +20 -0
- code_atlas/graph/builder.py +378 -0
- code_atlas/graph/identity.py +54 -0
- code_atlas/graph/queries.py +47 -0
- code_atlas/graph/validation.py +92 -0
- code_atlas/impact/__init__.py +37 -0
- code_atlas/impact/blast_radius.py +171 -0
- code_atlas/impact/contract_analyzer.py +104 -0
- code_atlas/impact/contracts.py +315 -0
- code_atlas/impact/diff_reader.py +142 -0
- code_atlas/impact/domain_traversal.py +768 -0
- code_atlas/impact/engine.py +392 -0
- code_atlas/impact/packs.py +128 -0
- code_atlas/impact/report.py +136 -0
- code_atlas/impact/revision_binding.py +220 -0
- code_atlas/impact/risk_classifier.py +83 -0
- code_atlas/impact/snapshot_resolver.py +136 -0
- code_atlas/impact/symbol_diff.py +210 -0
- code_atlas/impact/test_selector.py +166 -0
- code_atlas/indexing/__init__.py +5 -0
- code_atlas/indexing/cache.py +396 -0
- code_atlas/indexing/change_detector.py +278 -0
- code_atlas/indexing/coordinator.py +1032 -0
- code_atlas/indexing/dependency_planner.py +268 -0
- code_atlas/indexing/persist.py +436 -0
- code_atlas/indexing/reuse.py +85 -0
- code_atlas/install/__init__.py +7 -0
- code_atlas/install/cli.py +168 -0
- code_atlas/install/lifecycle.py +286 -0
- code_atlas/mcp.py +674 -0
- code_atlas/observability/__init__.py +19 -0
- code_atlas/observability/logging.py +199 -0
- code_atlas/parsers/__init__.py +43 -0
- code_atlas/parsers/contracts.py +209 -0
- code_atlas/parsers/engine.py +125 -0
- code_atlas/parsers/languages/__init__.py +1 -0
- code_atlas/parsers/languages/python.py +391 -0
- code_atlas/parsers/languages/tsx.py +28 -0
- code_atlas/parsers/languages/typescript.py +909 -0
- code_atlas/parsers/registry.py +51 -0
- code_atlas/parsers/tree_sitter_runtime.py +118 -0
- code_atlas/resolvers/__init__.py +33 -0
- code_atlas/resolvers/bootstrap.py +246 -0
- code_atlas/resolvers/capabilities.py +33 -0
- code_atlas/resolvers/contracts.py +133 -0
- code_atlas/resolvers/coordinator.py +96 -0
- code_atlas/resolvers/enrichment.py +318 -0
- code_atlas/resolvers/lsp/__init__.py +26 -0
- code_atlas/resolvers/lsp/adapter.py +256 -0
- code_atlas/resolvers/lsp/lifecycle.py +251 -0
- code_atlas/resolvers/lsp/spawn.py +92 -0
- code_atlas/resolvers/lsp/transport.py +240 -0
- code_atlas/resolvers/registry.py +33 -0
- code_atlas/resolvers/scip/__init__.py +49 -0
- code_atlas/resolvers/scip/decoder.py +216 -0
- code_atlas/resolvers/scip/importer.py +248 -0
- code_atlas/resolvers/scip/loader.py +90 -0
- code_atlas/resolvers/scip/mapper.py +271 -0
- code_atlas/resolvers/scip/provenance.py +211 -0
- code_atlas/resolvers/scip/resolver.py +289 -0
- code_atlas/resolvers/scip/validation.py +101 -0
- code_atlas/resolvers/syntactic/__init__.py +8 -0
- code_atlas/resolvers/syntactic/resolver.py +254 -0
- code_atlas/search/__init__.py +32 -0
- code_atlas/search/contracts.py +176 -0
- code_atlas/search/domain.py +561 -0
- code_atlas/search/engine.py +530 -0
- code_atlas/search/federated.py +276 -0
- code_atlas/search/graph.py +282 -0
- code_atlas/search/hybrid.py +151 -0
- code_atlas/search/intent.py +48 -0
- code_atlas/search/lexical.py +176 -0
- code_atlas/search/refs.py +359 -0
- code_atlas/search/structural.py +505 -0
- code_atlas/search/tokenizer.py +43 -0
- code_atlas/search/vector/__init__.py +35 -0
- code_atlas/search/vector/contracts.py +76 -0
- code_atlas/search/vector/embeddings.py +96 -0
- code_atlas/search/vector/index.py +169 -0
- code_atlas/search/vector/provider.py +50 -0
- code_atlas/storage/__init__.py +14 -0
- code_atlas/storage/db.py +47 -0
- code_atlas/storage/migrations.py +552 -0
- code_atlas/storage/store.py +1045 -0
- code_atlas/ui/__init__.py +29 -0
- code_atlas/ui/i18n.py +143 -0
- code_atlas/ui/kit.py +689 -0
- code_atlas/ui/wizard.py +317 -0
- code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
- code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
- code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
- code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
- code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
- code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
- code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
code_atlas/cli.py
ADDED
|
@@ -0,0 +1,2084 @@
|
|
|
1
|
+
"""Interface de linha de comando do Code Atlas.
|
|
2
|
+
|
|
3
|
+
Cycle 1: ``--version``, ``doctor``, ``init``, ``index``, ``status``.
|
|
4
|
+
Códigos de saída consistentes:
|
|
5
|
+
|
|
6
|
+
- 0: sucesso
|
|
7
|
+
- 1: erro de execução (inclui índice falho)
|
|
8
|
+
- 2: erro de uso (argparse)
|
|
9
|
+
- 3: bloqueado/unsafe (path inválido, config ausente para ação exigida)
|
|
10
|
+
|
|
11
|
+
``--json`` emite um único objeto JSON em stdout; erros vão para stderr.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sqlite3
|
|
20
|
+
import sys
|
|
21
|
+
from collections.abc import Mapping, Sequence
|
|
22
|
+
from dataclasses import asdict, dataclass, replace
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from code_atlas.domains.crossresolve import CrossDomainResolver
|
|
28
|
+
from code_atlas.domains.registry import DomainRegistry
|
|
29
|
+
from code_atlas.federation import Federation
|
|
30
|
+
|
|
31
|
+
from code_atlas import __version__, gitbin
|
|
32
|
+
from code_atlas.config import (
|
|
33
|
+
ConfigError,
|
|
34
|
+
config_file,
|
|
35
|
+
data_dir,
|
|
36
|
+
resolve_root,
|
|
37
|
+
write_config,
|
|
38
|
+
)
|
|
39
|
+
from code_atlas.discovery.identity import identity_key_for_root
|
|
40
|
+
from code_atlas.discovery.scanner import probe_git
|
|
41
|
+
from code_atlas.indexing import IndexCoordinator, IndexOutcome
|
|
42
|
+
from code_atlas.observability.logging import escape_control
|
|
43
|
+
from code_atlas.search.structural import (
|
|
44
|
+
DEFAULT_LIMIT,
|
|
45
|
+
MAX_DEPTH,
|
|
46
|
+
MAX_LIMIT,
|
|
47
|
+
SCHEMA_VERSION,
|
|
48
|
+
SearchFilters,
|
|
49
|
+
StructuralSearch,
|
|
50
|
+
clamp_limit,
|
|
51
|
+
)
|
|
52
|
+
from code_atlas.storage import connect, migrate, schema_version
|
|
53
|
+
from code_atlas.storage.store import Store
|
|
54
|
+
|
|
55
|
+
EXIT_OK = 0
|
|
56
|
+
EXIT_ERROR = 1
|
|
57
|
+
EXIT_USAGE = 2
|
|
58
|
+
EXIT_UNSAFE = 3
|
|
59
|
+
|
|
60
|
+
DB_FILENAME = "atlas.db"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def db_path() -> Path:
|
|
64
|
+
"""Banco do workspace (fora do repositório indexado)."""
|
|
65
|
+
return data_dir() / DB_FILENAME
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _open_db() -> sqlite3.Connection:
|
|
69
|
+
conn = connect(db_path())
|
|
70
|
+
migrate(conn)
|
|
71
|
+
return conn
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _resolver_factories(
|
|
75
|
+
target: Path,
|
|
76
|
+
*,
|
|
77
|
+
scip_path: str | None = None,
|
|
78
|
+
only: Sequence[str] | None = None,
|
|
79
|
+
) -> tuple[Sequence[Any], list[str], dict[str, str], int]:
|
|
80
|
+
"""Resolvers opt-in + ``retain_snapshots`` do ``config.toml``.
|
|
81
|
+
|
|
82
|
+
``[resolvers]`` (lf-0027): ausência de config/tabela → zero
|
|
83
|
+
factories (syntactic only). Erros de bootstrap são devolvidos
|
|
84
|
+
como strings de diagnóstico — nunca levantam.
|
|
85
|
+
``[indexing] retain_snapshots`` (lf-0031): 0 = ilimitado.
|
|
86
|
+
lf-0032: ``scip_path`` sobrescreve ``[resolvers.scip].index_path``;
|
|
87
|
+
``only`` restringe os resolvers ativos a ``scip`` / ``lsp`` /
|
|
88
|
+
``lsp:<nome>`` (``syntactic`` é sempre o baseline).
|
|
89
|
+
"""
|
|
90
|
+
from code_atlas.resolvers.bootstrap import resolver_factories
|
|
91
|
+
|
|
92
|
+
cfg_path = config_file()
|
|
93
|
+
resolvers: Any = None
|
|
94
|
+
retain = 0
|
|
95
|
+
errors: list[str] = []
|
|
96
|
+
if cfg_path.exists():
|
|
97
|
+
from code_atlas.config import WorkspaceConfig
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
cfg = WorkspaceConfig.load(cfg_path)
|
|
101
|
+
resolvers = cfg.resolvers
|
|
102
|
+
retain = cfg.retain_snapshots
|
|
103
|
+
except ConfigError as exc:
|
|
104
|
+
return (), [f"config load failed: {exc}"], {}, 0
|
|
105
|
+
table: dict[str, Any] = dict(resolvers or {})
|
|
106
|
+
if scip_path:
|
|
107
|
+
scip = dict(table.get("scip") or {})
|
|
108
|
+
scip["index_path"] = scip_path
|
|
109
|
+
table["scip"] = scip
|
|
110
|
+
if only:
|
|
111
|
+
names = {n.strip() for n in only if n.strip()}
|
|
112
|
+
known_lsp = set(
|
|
113
|
+
((resolvers or {}).get("lsp", {}) or {}).get("servers", {})
|
|
114
|
+
or {}
|
|
115
|
+
) if isinstance((resolvers or {}).get("lsp"), dict) else set()
|
|
116
|
+
unknown = sorted(
|
|
117
|
+
n for n in names
|
|
118
|
+
if n not in ("syntactic", "scip", "lsp")
|
|
119
|
+
and not (n.startswith("lsp:") and n[4:] in known_lsp)
|
|
120
|
+
)
|
|
121
|
+
for u in unknown:
|
|
122
|
+
errors.append(f"--resolver: resolver desconhecido '{u}'")
|
|
123
|
+
if "scip" not in names:
|
|
124
|
+
table.pop("scip", None)
|
|
125
|
+
lsp = table.get("lsp")
|
|
126
|
+
if isinstance(lsp, dict) and "lsp" not in names:
|
|
127
|
+
wanted = {n[4:] for n in names if n.startswith("lsp:")}
|
|
128
|
+
servers = dict(lsp.get("servers") or {})
|
|
129
|
+
if wanted:
|
|
130
|
+
lsp = dict(lsp)
|
|
131
|
+
lsp["servers"] = {
|
|
132
|
+
k: v for k, v in servers.items() if k in wanted
|
|
133
|
+
}
|
|
134
|
+
table["lsp"] = lsp
|
|
135
|
+
else:
|
|
136
|
+
table.pop("lsp", None)
|
|
137
|
+
versions: dict[str, str] = {}
|
|
138
|
+
factories, boot_errors = resolver_factories(table or None, target, versions)
|
|
139
|
+
errors.extend(boot_errors)
|
|
140
|
+
return tuple(factories), errors, versions, retain
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class Check:
|
|
145
|
+
"""Uma verificação do ``doctor``."""
|
|
146
|
+
|
|
147
|
+
name: str
|
|
148
|
+
value: str
|
|
149
|
+
ok: bool
|
|
150
|
+
detail: str = ""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _probe_git() -> Check:
|
|
154
|
+
if gitbin.git_binary() is None:
|
|
155
|
+
return Check("git", "absent", True, "git binary not found; degraded discovery mode")
|
|
156
|
+
version = gitbin.git_version()
|
|
157
|
+
if not version:
|
|
158
|
+
return Check("git", "error", True, "git --version failed")
|
|
159
|
+
return Check("git", version, True)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _probe_fts5() -> bool:
|
|
163
|
+
try:
|
|
164
|
+
con = sqlite3.connect(":memory:")
|
|
165
|
+
try:
|
|
166
|
+
con.execute("CREATE VIRTUAL TABLE t USING fts5(x)")
|
|
167
|
+
finally:
|
|
168
|
+
con.close()
|
|
169
|
+
return True
|
|
170
|
+
except sqlite3.Error:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _first_writable_ancestor(path: Path) -> tuple[Path, bool]:
|
|
175
|
+
"""Primeiro ancestral existente de ``path`` e se é gravável."""
|
|
176
|
+
current = path
|
|
177
|
+
while not current.exists() and current != current.parent:
|
|
178
|
+
current = current.parent
|
|
179
|
+
return current, current.exists() and os.access(current, os.W_OK)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def run_doctor(args: argparse.Namespace) -> int:
|
|
183
|
+
"""Executa verificações reais de ambiente. Sai 0 se não houver falhas."""
|
|
184
|
+
checks: list[Check] = []
|
|
185
|
+
|
|
186
|
+
py_ok = sys.version_info >= (3, 12)
|
|
187
|
+
checks.append(
|
|
188
|
+
Check(
|
|
189
|
+
"python",
|
|
190
|
+
sys.version.split()[0],
|
|
191
|
+
py_ok,
|
|
192
|
+
"" if py_ok else "requires >= 3.12",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
checks.append(Check("sqlite", sqlite3.sqlite_version, True))
|
|
196
|
+
checks.append(Check("fts5", "available" if _probe_fts5() else "unavailable", _probe_fts5()))
|
|
197
|
+
checks.append(_probe_git())
|
|
198
|
+
|
|
199
|
+
cfg = config_file()
|
|
200
|
+
checks.append(
|
|
201
|
+
Check(
|
|
202
|
+
"config",
|
|
203
|
+
str(cfg),
|
|
204
|
+
True,
|
|
205
|
+
"exists" if cfg.exists() else "missing (run `code-atlas init`)",
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
dd = data_dir()
|
|
209
|
+
ancestor, writable = _first_writable_ancestor(dd)
|
|
210
|
+
checks.append(
|
|
211
|
+
Check(
|
|
212
|
+
"data_dir",
|
|
213
|
+
str(dd),
|
|
214
|
+
writable,
|
|
215
|
+
f"writable via {ancestor}" if writable else f"not writable (checked {ancestor})",
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
failed = [c for c in checks if not c.ok]
|
|
220
|
+
if args.json:
|
|
221
|
+
payload: dict[str, Any] = {
|
|
222
|
+
"tool": "code-atlas",
|
|
223
|
+
"version": __version__,
|
|
224
|
+
"healthy": not failed,
|
|
225
|
+
"checks": [c.__dict__ for c in checks],
|
|
226
|
+
}
|
|
227
|
+
print(json.dumps(payload, indent=2))
|
|
228
|
+
else:
|
|
229
|
+
width = max(len(c.name) for c in checks)
|
|
230
|
+
for c in checks:
|
|
231
|
+
mark = "ok" if c.ok else "FAIL"
|
|
232
|
+
suffix = f" {c.detail}" if c.detail else ""
|
|
233
|
+
print(escape_control(
|
|
234
|
+
f"{c.name:<{width}} {mark:<4} {c.value}{suffix}"))
|
|
235
|
+
if failed:
|
|
236
|
+
print(f"\n{len(failed)} check(s) failed", file=sys.stderr)
|
|
237
|
+
return EXIT_ERROR if failed else EXIT_OK
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def run_init(args: argparse.Namespace) -> int:
|
|
241
|
+
"""Cria config, data dir e registra o repositório no índice local."""
|
|
242
|
+
root = resolve_root(args.root)
|
|
243
|
+
if not root.is_dir():
|
|
244
|
+
print(f"error: root {root} is not a directory", file=sys.stderr)
|
|
245
|
+
return EXIT_UNSAFE
|
|
246
|
+
try:
|
|
247
|
+
cfg = write_config(root, force=args.force)
|
|
248
|
+
except ConfigError as exc:
|
|
249
|
+
print(f"error: {escape_control(str(exc))}", file=sys.stderr)
|
|
250
|
+
return EXIT_ERROR
|
|
251
|
+
conn = _open_db()
|
|
252
|
+
try:
|
|
253
|
+
# lf-0021: identidade lógica via probe git — sem scan completo.
|
|
254
|
+
repo_id = Store(conn).get_or_create_repository(
|
|
255
|
+
identity_key_for_root(root), str(root)
|
|
256
|
+
)
|
|
257
|
+
conn.commit()
|
|
258
|
+
finally:
|
|
259
|
+
conn.close()
|
|
260
|
+
payload = {
|
|
261
|
+
"config": str(cfg),
|
|
262
|
+
"root": str(root),
|
|
263
|
+
"data_dir": str(data_dir()),
|
|
264
|
+
"database": str(db_path()),
|
|
265
|
+
"repository_id": repo_id,
|
|
266
|
+
}
|
|
267
|
+
if args.json:
|
|
268
|
+
print(json.dumps(payload))
|
|
269
|
+
else:
|
|
270
|
+
for k, v in payload.items():
|
|
271
|
+
print(f"{k}: {escape_control(str(v))}")
|
|
272
|
+
return EXIT_OK
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def run_status(args: argparse.Namespace) -> int:
|
|
276
|
+
"""Relata estado do índice para a raiz resolvida (funciona pré-index)."""
|
|
277
|
+
root = resolve_root(args.root)
|
|
278
|
+
cfg = config_file()
|
|
279
|
+
db_file = db_path()
|
|
280
|
+
initialized = db_file.exists()
|
|
281
|
+
|
|
282
|
+
payload: dict[str, object] = {
|
|
283
|
+
"tool": "code-atlas",
|
|
284
|
+
"version": __version__,
|
|
285
|
+
"root": str(root),
|
|
286
|
+
"config": str(cfg),
|
|
287
|
+
"config_exists": cfg.exists(),
|
|
288
|
+
"database": str(db_file),
|
|
289
|
+
"initialized": initialized,
|
|
290
|
+
}
|
|
291
|
+
git_info = probe_git(root) if root.is_dir() else None
|
|
292
|
+
git_payload: dict[str, object] | None = None
|
|
293
|
+
if git_info is not None:
|
|
294
|
+
git_payload = {
|
|
295
|
+
"available": git_info.available,
|
|
296
|
+
"is_repo": git_info.is_repo,
|
|
297
|
+
"branch": git_info.branch,
|
|
298
|
+
"head_sha": git_info.head_sha,
|
|
299
|
+
"dirty": git_info.dirty,
|
|
300
|
+
"is_worktree": git_info.is_worktree,
|
|
301
|
+
}
|
|
302
|
+
payload["git"] = git_payload
|
|
303
|
+
|
|
304
|
+
if not initialized:
|
|
305
|
+
payload["snapshot"] = None
|
|
306
|
+
payload["counts"] = {"source_files": 0, "snapshots": 0, "index_jobs": 0}
|
|
307
|
+
payload["schema_version"] = 0
|
|
308
|
+
if not args.json:
|
|
309
|
+
print(f"root: {root}")
|
|
310
|
+
print("state: not initialized — run `code-atlas init` or `code-atlas index`")
|
|
311
|
+
if git_info and git_info.is_repo:
|
|
312
|
+
print(f"git: {git_info.branch or 'detached'}@{git_info.head_sha[:12]}")
|
|
313
|
+
else:
|
|
314
|
+
print(json.dumps(payload))
|
|
315
|
+
return EXIT_OK
|
|
316
|
+
|
|
317
|
+
conn = _open_db()
|
|
318
|
+
try:
|
|
319
|
+
store = Store(conn)
|
|
320
|
+
payload["schema_version"] = schema_version(conn)
|
|
321
|
+
row = conn.execute(
|
|
322
|
+
"SELECT id FROM repositories WHERE root_handle = ?", (str(root),)
|
|
323
|
+
).fetchone()
|
|
324
|
+
repo_id: int | None = int(row["id"]) if row else None
|
|
325
|
+
payload["repository_id"] = repo_id
|
|
326
|
+
count_queries = {
|
|
327
|
+
"source_files": "SELECT COUNT(*) AS c FROM source_files",
|
|
328
|
+
"snapshots": "SELECT COUNT(*) AS c FROM snapshots",
|
|
329
|
+
"index_jobs": "SELECT COUNT(*) AS c FROM index_jobs",
|
|
330
|
+
}
|
|
331
|
+
payload["counts"] = {
|
|
332
|
+
name: int(conn.execute(sql).fetchone()["c"])
|
|
333
|
+
for name, sql in count_queries.items()
|
|
334
|
+
}
|
|
335
|
+
snap = store.latest_ready_snapshot(repo_id) if repo_id else None
|
|
336
|
+
snap_payload: dict[str, object] | None = None
|
|
337
|
+
if snap is not None:
|
|
338
|
+
snap_payload = {
|
|
339
|
+
"id": snap.id,
|
|
340
|
+
"status": str(snap.status),
|
|
341
|
+
"worktree_digest": snap.worktree_digest,
|
|
342
|
+
"git_ref": snap.git_ref,
|
|
343
|
+
"commit_sha": snap.commit_sha,
|
|
344
|
+
"parent_snapshot_id": snap.parent_snapshot_id,
|
|
345
|
+
"created_at": snap.created_at,
|
|
346
|
+
"files": store.inventory_count(snap.id) if snap.id else 0,
|
|
347
|
+
}
|
|
348
|
+
payload["snapshot"] = snap_payload
|
|
349
|
+
finally:
|
|
350
|
+
conn.close()
|
|
351
|
+
|
|
352
|
+
if args.json:
|
|
353
|
+
print(json.dumps(payload))
|
|
354
|
+
else:
|
|
355
|
+
print(f"root: {root}")
|
|
356
|
+
print(f"schema: v{payload['schema_version']}")
|
|
357
|
+
if git_payload:
|
|
358
|
+
print(f"git: {git_payload['branch'] or 'detached'}"
|
|
359
|
+
f"@{str(git_payload['head_sha'])[:12]} "
|
|
360
|
+
f"{'(dirty)' if git_payload['dirty'] else '(clean)'}")
|
|
361
|
+
if snap_payload:
|
|
362
|
+
print(f"snapshot: #{snap_payload['id']} {snap_payload['status']}"
|
|
363
|
+
f" — {snap_payload['files']} files")
|
|
364
|
+
else:
|
|
365
|
+
print("snapshot: none (run `code-atlas index`)")
|
|
366
|
+
return EXIT_OK
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def run_index(args: argparse.Namespace) -> int:
|
|
370
|
+
"""Indexa o inventário do diretório-alvo via IndexCoordinator.
|
|
371
|
+
|
|
372
|
+
Path arg com segmento ``..`` é rejeitado (hardening): para indexar um
|
|
373
|
+
diretório pai, passe o caminho absoluto diretamente.
|
|
374
|
+
"""
|
|
375
|
+
if args.path and ".." in Path(args.path).parts:
|
|
376
|
+
print("error: '..' traversal is not allowed in PATH", file=sys.stderr)
|
|
377
|
+
return EXIT_UNSAFE
|
|
378
|
+
target = Path(args.path).expanduser().resolve() if args.path else resolve_root(args.root)
|
|
379
|
+
if target == target.parent:
|
|
380
|
+
print("error: refusing to index filesystem root", file=sys.stderr)
|
|
381
|
+
return EXIT_UNSAFE
|
|
382
|
+
if not target.is_dir():
|
|
383
|
+
print(f"error: {escape_control(str(target))} is not a directory",
|
|
384
|
+
file=sys.stderr)
|
|
385
|
+
return EXIT_UNSAFE
|
|
386
|
+
conn = _open_db()
|
|
387
|
+
try:
|
|
388
|
+
# Resolvers opt-in (lf-0027): [resolvers] do config.toml —
|
|
389
|
+
# erros de bootstrap viram diagnóstico no payload, não crash.
|
|
390
|
+
# lf-0032: --scip sobrescreve o index_path; --resolver restringe
|
|
391
|
+
# os resolvers ativos — nome desconhecido é erro tipado (nunca
|
|
392
|
+
# uma flag que silenciosamente não faz nada).
|
|
393
|
+
factories, boot_errors, res_versions, retain, fatal = (
|
|
394
|
+
_index_resolvers(target, args))
|
|
395
|
+
if fatal:
|
|
396
|
+
conn.close()
|
|
397
|
+
return _emit_error(args, fatal)
|
|
398
|
+
# Domínio opt-in (lf-0123, Cycle 7 Phase 15): ``--domains``
|
|
399
|
+
# monta o registry de adapters built-in; ``--cross-domain``
|
|
400
|
+
# injeta o resolver. Default = flags ausentes → ``None`` →
|
|
401
|
+
# comportamento byte a byte anterior (adapters OFF).
|
|
402
|
+
dom_reg, cross_res, dom_err = _index_domains(target, args)
|
|
403
|
+
if dom_err:
|
|
404
|
+
conn.close()
|
|
405
|
+
return _emit_error(args, dom_err)
|
|
406
|
+
coord = IndexCoordinator(
|
|
407
|
+
conn, resolver_factories=factories,
|
|
408
|
+
resolver_versions=res_versions,
|
|
409
|
+
snapshot_retention=retain,
|
|
410
|
+
domain_registry=dom_reg,
|
|
411
|
+
cross_resolver=cross_res)
|
|
412
|
+
# lf-0022: incremental é o default — ``--full`` é o escape
|
|
413
|
+
# (force desliga o caminho incremental internamente).
|
|
414
|
+
# lf-0141: ``--verbose`` liga o callback de progresso — linhas
|
|
415
|
+
# ``[index]`` em stderr, stdout fica livre para ``--json``.
|
|
416
|
+
outcome: IndexOutcome = coord.index(
|
|
417
|
+
target, force=args.full, incremental=True,
|
|
418
|
+
progress=_index_progress
|
|
419
|
+
if getattr(args, "verbose", False) else None)
|
|
420
|
+
plan_counts = (
|
|
421
|
+
coord.last_plan.counts() if coord.last_plan is not None else {}
|
|
422
|
+
)
|
|
423
|
+
finally:
|
|
424
|
+
conn.close()
|
|
425
|
+
payload = {
|
|
426
|
+
"tool": "code-atlas",
|
|
427
|
+
"version": __version__,
|
|
428
|
+
"schema_version": SCHEMA_VERSION,
|
|
429
|
+
"root": str(target),
|
|
430
|
+
"snapshot_id": outcome.snapshot_id,
|
|
431
|
+
"job_id": outcome.job_id,
|
|
432
|
+
"status": outcome.status,
|
|
433
|
+
"files_indexed": outcome.files_indexed,
|
|
434
|
+
"secrets_skipped": outcome.secrets_skipped,
|
|
435
|
+
"warnings": list(outcome.warnings),
|
|
436
|
+
"noop": outcome.noop,
|
|
437
|
+
"error": outcome.error,
|
|
438
|
+
"stats": outcome.stats,
|
|
439
|
+
"plan": plan_counts,
|
|
440
|
+
"diagnostics": boot_errors,
|
|
441
|
+
"domains": (
|
|
442
|
+
sorted(d.domain_id for d in dom_reg.enabled())
|
|
443
|
+
if dom_reg is not None else []
|
|
444
|
+
),
|
|
445
|
+
"cross_domain": cross_res is not None,
|
|
446
|
+
}
|
|
447
|
+
if args.json:
|
|
448
|
+
print(json.dumps(payload))
|
|
449
|
+
else:
|
|
450
|
+
_print_index_human(outcome)
|
|
451
|
+
return EXIT_OK if outcome.status in ("success", "noop") else EXIT_ERROR
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _index_progress(event: str, fields: Mapping[str, Any]) -> None:
|
|
455
|
+
"""``index --verbose``: uma linha ``[index] <evento> k=v …`` em stderr.
|
|
456
|
+
|
|
457
|
+
stdout fica reservado ao payload — ``--json`` continua parseável
|
|
458
|
+
com o progresso ligado. Nenhum campo carrega paths nem segredos —
|
|
459
|
+
e o escape de controles (lf-0153) garante que isso continue
|
|
460
|
+
verdade mesmo se um campo futuro carregar texto não-confiável:
|
|
461
|
+
``\\n``/ANSI nunca forjam linhas ou sequências de terminal.
|
|
462
|
+
"""
|
|
463
|
+
parts = " ".join(f"{k}={v}" for k, v in fields.items())
|
|
464
|
+
line = escape_control(f"[index] {event} {parts}").rstrip()
|
|
465
|
+
print(line, file=sys.stderr)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _index_domains(
|
|
469
|
+
target: Path, args: argparse.Namespace,
|
|
470
|
+
) -> tuple[DomainRegistry | None, CrossDomainResolver | None, str]:
|
|
471
|
+
"""Flags de domínio do ``index`` → (registry, cross_resolver, fatal).
|
|
472
|
+
|
|
473
|
+
``--domains`` aceita ids separados por vírgula ou ``all``;
|
|
474
|
+
``--cross-domain`` exige ``--domains`` (sem rows de domínio o plano
|
|
475
|
+
do resolver sai vazio — recusar é mais honesto que indexar uma
|
|
476
|
+
versão de analisador que não faz nada).
|
|
477
|
+
"""
|
|
478
|
+
spec = getattr(args, "domains", None)
|
|
479
|
+
cross = bool(getattr(args, "cross_domain", False))
|
|
480
|
+
if not spec:
|
|
481
|
+
if cross:
|
|
482
|
+
return None, None, "--cross-domain requires --domains"
|
|
483
|
+
return None, None, ""
|
|
484
|
+
from code_atlas.domains.cli_surface import select_domain_registry
|
|
485
|
+
|
|
486
|
+
reg, err = select_domain_registry(spec, target)
|
|
487
|
+
if err or reg is None:
|
|
488
|
+
return None, None, err or "--domains: registry build failed"
|
|
489
|
+
resolver = None
|
|
490
|
+
if cross:
|
|
491
|
+
from code_atlas.domains.crossresolve import CrossDomainResolver
|
|
492
|
+
|
|
493
|
+
resolver = CrossDomainResolver()
|
|
494
|
+
return reg, resolver, ""
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _print_index_human(outcome: IndexOutcome) -> None:
|
|
498
|
+
if outcome.status == "success":
|
|
499
|
+
print(f"snapshot #{outcome.snapshot_id} READY — "
|
|
500
|
+
f"{outcome.files_indexed} inventory files")
|
|
501
|
+
s = outcome.stats
|
|
502
|
+
if s.get("occurrences") or s.get("relations"):
|
|
503
|
+
print(f"graph: {s.get('symbols_new', 0)} symbols, "
|
|
504
|
+
f"{s.get('occurrences', 0)} occurrences, "
|
|
505
|
+
f"{s.get('relations', 0)} relations "
|
|
506
|
+
f"({s.get('files_failed', 0)} files failed)")
|
|
507
|
+
if s.get("domain_adapters"):
|
|
508
|
+
dom = {k: v for k, v in s.items()
|
|
509
|
+
if k.startswith("domain_") and k != "domain_adapters"}
|
|
510
|
+
print(f"domains: {s['domain_adapters']} adapter(s) — "
|
|
511
|
+
+ ", ".join(f"{k}={v}" for k, v in sorted(dom.items())))
|
|
512
|
+
elif outcome.status == "noop":
|
|
513
|
+
print(f"snapshot #{outcome.snapshot_id} unchanged — no-op index")
|
|
514
|
+
else:
|
|
515
|
+
print(f"index failed: {escape_control(outcome.error)}",
|
|
516
|
+
file=sys.stderr)
|
|
517
|
+
if outcome.secrets_skipped:
|
|
518
|
+
print(f"secrets excluded: {outcome.secrets_skipped}")
|
|
519
|
+
for w in outcome.warnings:
|
|
520
|
+
print(f"warning: {w}", file=sys.stderr)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _index_resolvers(
|
|
524
|
+
target: Path, args: argparse.Namespace,
|
|
525
|
+
) -> tuple[Sequence[Any], list[str], dict[str, str], int, str]:
|
|
526
|
+
"""Flags de resolver do ``index`` → factories/versions/retain.
|
|
527
|
+
|
|
528
|
+
``fatal`` não-vazio = erro tipado de flag (resolver desconhecido)
|
|
529
|
+
— o caller aborta sem indexar.
|
|
530
|
+
"""
|
|
531
|
+
only: list[str] = []
|
|
532
|
+
for r in args.resolver or ():
|
|
533
|
+
only.extend(p for p in r.split(",") if p.strip())
|
|
534
|
+
factories, errors, versions, retain = _resolver_factories(
|
|
535
|
+
target, scip_path=args.scip, only=only or None)
|
|
536
|
+
bad = "; ".join(
|
|
537
|
+
e for e in errors if e.startswith("--resolver:"))
|
|
538
|
+
return factories, errors, versions, retain, bad
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def run_semantic_status(args: argparse.Namespace) -> int:
|
|
542
|
+
"""lf-0032: resolvers installed/enabled/used + cobertura do READY.
|
|
543
|
+
|
|
544
|
+
Lê só o snapshot READY persistido — nunca inventa cobertura.
|
|
545
|
+
``installed``/``enabled`` vêm de ``describe_resolvers`` (read-only:
|
|
546
|
+
nenhum server LSP é spawnado); ``used`` vem de
|
|
547
|
+
``relations.resolver`` — a única fonte autoritativa de uso real.
|
|
548
|
+
"""
|
|
549
|
+
target = (
|
|
550
|
+
Path(args.path).expanduser().resolve()
|
|
551
|
+
if args.path else resolve_root(args.root)
|
|
552
|
+
)
|
|
553
|
+
conn = _open_db()
|
|
554
|
+
try:
|
|
555
|
+
payload = _semantic_payload(conn, target)
|
|
556
|
+
finally:
|
|
557
|
+
conn.close()
|
|
558
|
+
return _emit_page(args, payload)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _semantic_payload(
|
|
562
|
+
conn: sqlite3.Connection, target: Path
|
|
563
|
+
) -> dict[str, Any]:
|
|
564
|
+
"""Monta o payload do ``semantic-status`` lendo só o READY."""
|
|
565
|
+
search = StructuralSearch(conn)
|
|
566
|
+
repo_id = search.repo_for_root(str(target))
|
|
567
|
+
snap = (
|
|
568
|
+
search.resolve_snapshot(repo_id, None)[0]
|
|
569
|
+
if repo_id is not None else None
|
|
570
|
+
)
|
|
571
|
+
coverage, used = _resolver_usage(conn, snap)
|
|
572
|
+
reports, boot_errors = _resolver_reports(target, used)
|
|
573
|
+
analyzer_versions = dict(snap.analyzer_versions) if snap else {}
|
|
574
|
+
last_job: dict[str, Any] = {}
|
|
575
|
+
if repo_id is not None:
|
|
576
|
+
row = conn.execute(
|
|
577
|
+
"SELECT counts FROM index_jobs WHERE repository_id = ? "
|
|
578
|
+
"AND status IN ('success','noop','failed') "
|
|
579
|
+
"ORDER BY id DESC LIMIT 1",
|
|
580
|
+
(repo_id,),
|
|
581
|
+
).fetchone()
|
|
582
|
+
if row is not None:
|
|
583
|
+
last_job = json.loads(row["counts"] or "{}")
|
|
584
|
+
sem_keys = ("semantic_attempted", "semantic_enriched",
|
|
585
|
+
"semantic_diagnostics")
|
|
586
|
+
payload: dict[str, Any] = {
|
|
587
|
+
"tool": "code-atlas",
|
|
588
|
+
"version": __version__,
|
|
589
|
+
"schema_version": SCHEMA_VERSION,
|
|
590
|
+
"root": str(target),
|
|
591
|
+
"snapshot_id": snap.id if snap else None,
|
|
592
|
+
"indexed": snap is not None,
|
|
593
|
+
"resolvers": reports,
|
|
594
|
+
"coverage": {
|
|
595
|
+
"resolved": coverage.get("resolved", 0),
|
|
596
|
+
"ambiguous": coverage.get("ambiguous", 0),
|
|
597
|
+
"unresolved": coverage.get("unresolved", 0),
|
|
598
|
+
"external": coverage.get("external", 0),
|
|
599
|
+
"total": sum(coverage.values()),
|
|
600
|
+
},
|
|
601
|
+
"semantic": {k: last_job.get(k, 0) for k in sem_keys},
|
|
602
|
+
"revision": {
|
|
603
|
+
"commit_sha": snap.commit_sha if snap else None,
|
|
604
|
+
"scip_index_sha": analyzer_versions.get("resolver:scip"),
|
|
605
|
+
},
|
|
606
|
+
"diagnostics": boot_errors,
|
|
607
|
+
"_human": [],
|
|
608
|
+
}
|
|
609
|
+
for rep in reports:
|
|
610
|
+
flags = "/".join(
|
|
611
|
+
k for k in ("installed", "enabled", "used") if rep[k])
|
|
612
|
+
payload["_human"].append(
|
|
613
|
+
f"{rep['id']}: {flags or 'none'} edges={rep['edges']}")
|
|
614
|
+
cov = payload["coverage"]
|
|
615
|
+
payload["_human"].append(
|
|
616
|
+
f"coverage: resolved={cov['resolved']} "
|
|
617
|
+
f"ambiguous={cov['ambiguous']} "
|
|
618
|
+
f"unresolved={cov['unresolved']} external={cov['external']}")
|
|
619
|
+
for e in boot_errors:
|
|
620
|
+
payload["_human"].append(f"diagnostic: {e}")
|
|
621
|
+
return payload
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _resolver_usage(
|
|
625
|
+
conn: sqlite3.Connection, snap: Any
|
|
626
|
+
) -> tuple[dict[str, int], dict[str, dict[str, int]]]:
|
|
627
|
+
"""Cobertura por status + uso por resolver no snapshot READY."""
|
|
628
|
+
coverage: dict[str, int] = {}
|
|
629
|
+
used: dict[str, dict[str, int]] = {}
|
|
630
|
+
if snap is None or snap.id is None:
|
|
631
|
+
return coverage, used
|
|
632
|
+
for r in conn.execute(
|
|
633
|
+
"SELECT resolver, resolution_status, COUNT(*) AS c "
|
|
634
|
+
"FROM relations WHERE snapshot_id = ? "
|
|
635
|
+
"GROUP BY resolver, resolution_status",
|
|
636
|
+
(snap.id,),
|
|
637
|
+
):
|
|
638
|
+
coverage[r["resolution_status"]] = (
|
|
639
|
+
coverage.get(r["resolution_status"], 0) + r["c"])
|
|
640
|
+
used.setdefault(r["resolver"], {}).setdefault(
|
|
641
|
+
r["resolution_status"], 0)
|
|
642
|
+
used[r["resolver"]][r["resolution_status"]] += r["c"]
|
|
643
|
+
return coverage, used
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _resolver_reports(
|
|
647
|
+
target: Path, used: dict[str, dict[str, int]]
|
|
648
|
+
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
649
|
+
"""``describe_resolvers`` + marcação de uso real por resolver."""
|
|
650
|
+
from code_atlas.resolvers.bootstrap import describe_resolvers
|
|
651
|
+
|
|
652
|
+
table: Any = None
|
|
653
|
+
cfg_path = config_file()
|
|
654
|
+
if cfg_path.exists():
|
|
655
|
+
from code_atlas.config import WorkspaceConfig
|
|
656
|
+
|
|
657
|
+
try:
|
|
658
|
+
table = WorkspaceConfig.load(cfg_path).resolvers
|
|
659
|
+
except ConfigError:
|
|
660
|
+
table = None
|
|
661
|
+
reports, errors = describe_resolvers(table, target)
|
|
662
|
+
for rep in reports:
|
|
663
|
+
rid = str(rep["id"])
|
|
664
|
+
by_status = dict(used.get(rid, {}))
|
|
665
|
+
if rid == "syntactic-scope-v1":
|
|
666
|
+
# arestas sintáticas carregam o parser-id como resolver
|
|
667
|
+
# (ex.: tree-sitter-python) — tudo que não é resolver
|
|
668
|
+
# externo pertence ao tier sintático baseline.
|
|
669
|
+
for k, v in used.items():
|
|
670
|
+
if k == rid:
|
|
671
|
+
continue
|
|
672
|
+
if not k.startswith("scip") and not k.startswith("lsp:"):
|
|
673
|
+
for st, c in v.items():
|
|
674
|
+
by_status[st] = by_status.get(st, 0) + c
|
|
675
|
+
rep["used"] = bool(by_status)
|
|
676
|
+
rep["edges"] = sum(by_status.values())
|
|
677
|
+
rep["by_status"] = by_status
|
|
678
|
+
return reports, errors
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _search_context(
|
|
682
|
+
args: argparse.Namespace,
|
|
683
|
+
) -> tuple[
|
|
684
|
+
sqlite3.Connection | None,
|
|
685
|
+
int | None,
|
|
686
|
+
int | None,
|
|
687
|
+
dict[str, Any] | None,
|
|
688
|
+
str,
|
|
689
|
+
]:
|
|
690
|
+
"""Resolve (conn, repo_id, snapshot, repo_info) para read-only.
|
|
691
|
+
|
|
692
|
+
Erros são impressos em stderr + JSON de erro; retorna tupla com
|
|
693
|
+
``None``s e mensagem quando indisponível — caller sai EXIT_ERROR.
|
|
694
|
+
"""
|
|
695
|
+
root = resolve_root(args.root)
|
|
696
|
+
conn = _open_db()
|
|
697
|
+
search = StructuralSearch(conn)
|
|
698
|
+
root_handle = str(root)
|
|
699
|
+
repo_id = search.repo_for_root(root_handle)
|
|
700
|
+
if repo_id is None:
|
|
701
|
+
conn.close()
|
|
702
|
+
return None, None, None, None, f"no indexed repository at {root_handle}"
|
|
703
|
+
snap, err = search.resolve_snapshot(repo_id, args.snapshot)
|
|
704
|
+
if err or snap is None or snap.id is None:
|
|
705
|
+
conn.close()
|
|
706
|
+
return None, None, None, None, err or "snapshot missing id"
|
|
707
|
+
repo_info = search.repository_info(repo_id) or {"id": repo_id}
|
|
708
|
+
return conn, repo_id, snap.id, repo_info, ""
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _emit_error(args: argparse.Namespace, msg: str) -> int:
|
|
712
|
+
if args.json:
|
|
713
|
+
print(json.dumps({"tool": "code-atlas", "version": __version__,
|
|
714
|
+
"error": msg, "schema_version": SCHEMA_VERSION}))
|
|
715
|
+
else:
|
|
716
|
+
print(f"error: {escape_control(msg)}", file=sys.stderr)
|
|
717
|
+
return EXIT_ERROR
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _emit_page(args: argparse.Namespace, payload: dict[str, Any]) -> int:
|
|
721
|
+
"""--json: objeto único parseável. Humano: linhas pré-formatadas."""
|
|
722
|
+
if args.json:
|
|
723
|
+
payload.pop("_human", None)
|
|
724
|
+
print(json.dumps(payload))
|
|
725
|
+
else:
|
|
726
|
+
for line in payload.pop("_human", []):
|
|
727
|
+
print(escape_control(line))
|
|
728
|
+
if "total" in payload:
|
|
729
|
+
print(
|
|
730
|
+
f"-- total={payload['total']} "
|
|
731
|
+
f"truncated={payload.get('truncated', False)}"
|
|
732
|
+
)
|
|
733
|
+
return EXIT_OK
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def run_search(args: argparse.Namespace) -> int:
|
|
737
|
+
conn, repo_id, snap_id, repo_info, err = _search_context(args)
|
|
738
|
+
if err:
|
|
739
|
+
return _emit_error(args, err)
|
|
740
|
+
assert (
|
|
741
|
+
conn is not None and repo_id is not None and snap_id is not None
|
|
742
|
+
)
|
|
743
|
+
strategy = getattr(args, "strategy", "structural") or "structural"
|
|
744
|
+
if strategy != "structural":
|
|
745
|
+
if repo_info is None:
|
|
746
|
+
return _emit_error(args, "repository info unavailable")
|
|
747
|
+
return _run_search_engine(args, conn, snap_id, repo_info, strategy)
|
|
748
|
+
try:
|
|
749
|
+
search = StructuralSearch(conn)
|
|
750
|
+
filters = SearchFilters(
|
|
751
|
+
language=args.language, kind=args.kind, path=args.path_filter,
|
|
752
|
+
)
|
|
753
|
+
limit = clamp_limit(args.limit)
|
|
754
|
+
page = search.search_symbols(
|
|
755
|
+
repo_id, snap_id, args.query, filters, limit, max(0, args.offset)
|
|
756
|
+
)
|
|
757
|
+
results = [dict(x) for x in page.items]
|
|
758
|
+
payload: dict[str, Any] = {
|
|
759
|
+
"tool": "code-atlas", "version": __version__,
|
|
760
|
+
"schema_version": SCHEMA_VERSION, "command": "search",
|
|
761
|
+
"query": args.query, "snapshot_id": snap_id,
|
|
762
|
+
"repository": repo_info,
|
|
763
|
+
"diagnostics": [],
|
|
764
|
+
"filters": {"language": args.language, "kind": args.kind,
|
|
765
|
+
"path": args.path_filter},
|
|
766
|
+
"limit": limit, "offset": args.offset,
|
|
767
|
+
"total": page.total, "truncated": page.truncated,
|
|
768
|
+
"results": results,
|
|
769
|
+
"_human": [
|
|
770
|
+
f"{r['symbol_id']:>5} {r['kind']:<9} {r['language']:<10} "
|
|
771
|
+
f"{r['qualified_name']}" for r in results
|
|
772
|
+
],
|
|
773
|
+
}
|
|
774
|
+
finally:
|
|
775
|
+
conn.close()
|
|
776
|
+
return _emit_page(args, payload)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _run_search_engine(
|
|
780
|
+
args: argparse.Namespace,
|
|
781
|
+
conn: sqlite3.Connection,
|
|
782
|
+
snap_id: int,
|
|
783
|
+
repo_info: dict[str, Any],
|
|
784
|
+
strategy: str,
|
|
785
|
+
) -> int:
|
|
786
|
+
"""Estratégias do SearchEngine (lexical|graph|hybrid) com envelope v2."""
|
|
787
|
+
from code_atlas.search import SearchEngine, SearchRequest
|
|
788
|
+
from code_atlas.search.contracts import encode_cursor, query_fingerprint
|
|
789
|
+
|
|
790
|
+
offset = max(0, args.offset)
|
|
791
|
+
cursor = None
|
|
792
|
+
if offset:
|
|
793
|
+
fp = query_fingerprint(args.query, {
|
|
794
|
+
"language": args.language, "kind": args.kind,
|
|
795
|
+
"path": args.path_filter})
|
|
796
|
+
cursor = encode_cursor(offset, fp)
|
|
797
|
+
try:
|
|
798
|
+
res = SearchEngine(conn).search(
|
|
799
|
+
SearchRequest(
|
|
800
|
+
query=args.query,
|
|
801
|
+
strategy=strategy,
|
|
802
|
+
language=args.language,
|
|
803
|
+
kind=args.kind,
|
|
804
|
+
path=args.path_filter,
|
|
805
|
+
repository_root=str(resolve_root(args.root)),
|
|
806
|
+
snapshot_id=snap_id,
|
|
807
|
+
limit=clamp_limit(args.limit),
|
|
808
|
+
cursor=cursor,
|
|
809
|
+
)
|
|
810
|
+
)
|
|
811
|
+
finally:
|
|
812
|
+
conn.close()
|
|
813
|
+
items = [asdict(h) for h in res.items]
|
|
814
|
+
payload: dict[str, Any] = res.to_dict()
|
|
815
|
+
payload["tool"] = "code-atlas"
|
|
816
|
+
payload["version"] = __version__
|
|
817
|
+
payload["command"] = "search"
|
|
818
|
+
payload["repository"] = repo_info
|
|
819
|
+
payload["_human"] = [
|
|
820
|
+
f"{h['qualified_name']:<40} {h['path']} "
|
|
821
|
+
f"[{','.join(h['matched_fields']) or 'structural'} "
|
|
822
|
+
f"score={h['score']:.4f}]" for h in items
|
|
823
|
+
]
|
|
824
|
+
return _emit_page(args, payload)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def run_context(args: argparse.Namespace) -> int:
|
|
828
|
+
"""`code-atlas context TASK` — pack de contexto sob budget (lf-0040)."""
|
|
829
|
+
conn, _repo_id, snap_id, _info, err = _search_context(args)
|
|
830
|
+
if err:
|
|
831
|
+
return _emit_error(args, err)
|
|
832
|
+
assert conn is not None and snap_id is not None
|
|
833
|
+
from code_atlas.context import (
|
|
834
|
+
ContextEngine,
|
|
835
|
+
ContextRequest,
|
|
836
|
+
render_json,
|
|
837
|
+
render_markdown,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
try:
|
|
841
|
+
pack = ContextEngine(conn).build(ContextRequest(
|
|
842
|
+
task=args.task,
|
|
843
|
+
repository_root=str(resolve_root(args.root)),
|
|
844
|
+
snapshot_id=snap_id,
|
|
845
|
+
level=args.level,
|
|
846
|
+
budget_tokens=args.budget,
|
|
847
|
+
max_symbols=clamp_limit(args.limit),
|
|
848
|
+
))
|
|
849
|
+
finally:
|
|
850
|
+
conn.close()
|
|
851
|
+
if args.format == "markdown":
|
|
852
|
+
print(render_markdown(pack))
|
|
853
|
+
return EXIT_OK
|
|
854
|
+
if args.json or args.format == "json":
|
|
855
|
+
print(render_json(pack))
|
|
856
|
+
return EXIT_OK
|
|
857
|
+
# humano: linhas resumidas
|
|
858
|
+
m = pack.manifest
|
|
859
|
+
print(f"# pack snapshot={m.snapshot_id} level={m.level} "
|
|
860
|
+
f"selected={m.selected} omitted={m.omitted} "
|
|
861
|
+
f"truncated={str(m.truncated).lower()}")
|
|
862
|
+
for i in pack.items:
|
|
863
|
+
print(escape_control(
|
|
864
|
+
f" [{i.priority}] {i.kind:<10} {i.title} "
|
|
865
|
+
f"(~{i.est_tokens} tok)"))
|
|
866
|
+
if pack.diagnostics.codes:
|
|
867
|
+
print(f"diagnostics: {', '.join(pack.diagnostics.codes)}")
|
|
868
|
+
return EXIT_OK
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def run_context_expand(args: argparse.Namespace) -> int:
|
|
872
|
+
"""`code-atlas context-expand --symbol X` — expansão autocontida."""
|
|
873
|
+
conn, _repo_id, snap_id, _info, err = _search_context(args)
|
|
874
|
+
if err:
|
|
875
|
+
return _emit_error(args, err)
|
|
876
|
+
assert conn is not None and snap_id is not None
|
|
877
|
+
from code_atlas.context import ContextExpander, expansion_to_json
|
|
878
|
+
|
|
879
|
+
delivered = frozenset(
|
|
880
|
+
s for s in (args.delivered or "").split(",") if s)
|
|
881
|
+
try:
|
|
882
|
+
res = ContextExpander(conn).expand(
|
|
883
|
+
snap_id, args.symbol, delivered=delivered,
|
|
884
|
+
budget_tokens=args.budget)
|
|
885
|
+
finally:
|
|
886
|
+
conn.close()
|
|
887
|
+
if res.error:
|
|
888
|
+
return _emit_error(args, res.error)
|
|
889
|
+
if args.json:
|
|
890
|
+
print(expansion_to_json(res))
|
|
891
|
+
else:
|
|
892
|
+
for i in res.added:
|
|
893
|
+
print(escape_control(
|
|
894
|
+
f" + {i.symbol_key} (~{i.est_tokens} tok) {i.path}"))
|
|
895
|
+
for r in res.remaining_refs:
|
|
896
|
+
print(escape_control(f" … {r.ref_id} ({r.reason})"))
|
|
897
|
+
return EXIT_OK
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
_MCP_MANIFESTS: dict[str, dict[str, Any]] = {
|
|
901
|
+
"claude-code": {"mcpServers": {"code-atlas": {
|
|
902
|
+
"command": "code-atlas", "args": ["mcp"]}}},
|
|
903
|
+
"codex-cli": {"mcp_servers": {"code-atlas": {
|
|
904
|
+
"command": "code-atlas", "args": ["mcp"]}}},
|
|
905
|
+
"generic": {"mcpServers": {"code-atlas": {
|
|
906
|
+
"command": "code-atlas", "args": ["mcp"]}}},
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def run_api(args: argparse.Namespace) -> int:
|
|
911
|
+
"""`code-atlas api serve` — API HTTP local loopback (Cycle 6)."""
|
|
912
|
+
from code_atlas.api.server import serve
|
|
913
|
+
|
|
914
|
+
db_file = db_path()
|
|
915
|
+
if not db_file.exists():
|
|
916
|
+
print("banco não existe — rode `code-atlas index` antes",
|
|
917
|
+
file=sys.stderr)
|
|
918
|
+
return EXIT_ERROR
|
|
919
|
+
server = serve(str(db_file), host=args.host, port=args.port,
|
|
920
|
+
verbose=args.verbose)
|
|
921
|
+
print(f"API Explorer em {server.url} — Ctrl+C para parar")
|
|
922
|
+
try:
|
|
923
|
+
server.serve_forever()
|
|
924
|
+
except KeyboardInterrupt:
|
|
925
|
+
pass
|
|
926
|
+
finally:
|
|
927
|
+
server.shutdown()
|
|
928
|
+
return EXIT_OK
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def run_explorer(args: argparse.Namespace) -> int:
|
|
932
|
+
"""`code-atlas explorer` — serve API + assets compilados do Explorer.
|
|
933
|
+
|
|
934
|
+
Nunca abre o navegador sem `--open`; bind sempre loopback; se a dist
|
|
935
|
+
do Angular não existir (nem empacotada nem no checkout), serve só a
|
|
936
|
+
API e avisa.
|
|
937
|
+
"""
|
|
938
|
+
from code_atlas.api.server import explorer_static_root, serve
|
|
939
|
+
|
|
940
|
+
db_file = db_path()
|
|
941
|
+
if not db_file.exists():
|
|
942
|
+
print("banco não existe — rode `code-atlas index` antes",
|
|
943
|
+
file=sys.stderr)
|
|
944
|
+
return EXIT_ERROR
|
|
945
|
+
static = explorer_static_root(Path(__file__).resolve().parents[2])
|
|
946
|
+
if static is None:
|
|
947
|
+
print("Explorer não compilado — servindo apenas a API "
|
|
948
|
+
"(build: cd explorer && npm run build)", file=sys.stderr)
|
|
949
|
+
try:
|
|
950
|
+
server = serve(str(db_file), host=args.host, port=args.port,
|
|
951
|
+
verbose=args.verbose, static_root=static)
|
|
952
|
+
except OSError as e:
|
|
953
|
+
print(f"porta indisponível: {escape_control(str(e))}",
|
|
954
|
+
file=sys.stderr)
|
|
955
|
+
return EXIT_ERROR
|
|
956
|
+
url = f"http://{server.host}:{server.port}/"
|
|
957
|
+
print(f"Explorer em {url} — API em {server.url}")
|
|
958
|
+
if args.open:
|
|
959
|
+
import webbrowser
|
|
960
|
+
webbrowser.open(url)
|
|
961
|
+
try:
|
|
962
|
+
server.serve_forever()
|
|
963
|
+
except KeyboardInterrupt:
|
|
964
|
+
pass
|
|
965
|
+
finally:
|
|
966
|
+
server.shutdown()
|
|
967
|
+
return EXIT_OK
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def run_mcp(args: argparse.Namespace) -> int:
|
|
971
|
+
"""`code-atlas mcp [doctor|manifest] ` — servidor stdio + host UX.
|
|
972
|
+
|
|
973
|
+
`doctor` diagnostica instalação/capabilities; `manifest` EMITE o
|
|
974
|
+
manifest do host (nunca edita config global — só instrui).
|
|
975
|
+
"""
|
|
976
|
+
from code_atlas.mcp import main_stdio
|
|
977
|
+
|
|
978
|
+
cmd = getattr(args, "cmd", "serve") or "serve"
|
|
979
|
+
if cmd == "manifest":
|
|
980
|
+
host = getattr(args, "host", "generic") or "generic"
|
|
981
|
+
manifest = _MCP_MANIFESTS.get(host, _MCP_MANIFESTS["generic"])
|
|
982
|
+
print(json.dumps(manifest, indent=2))
|
|
983
|
+
print("# cole o JSON acima no config do host — Atlas nunca "
|
|
984
|
+
"escreve nele", file=sys.stderr)
|
|
985
|
+
return EXIT_OK
|
|
986
|
+
if cmd == "doctor":
|
|
987
|
+
return _mcp_doctor(args)
|
|
988
|
+
return main_stdio()
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def _mcp_doctor(args: argparse.Namespace) -> int:
|
|
992
|
+
"""Diagnóstico de instalação: executável, DB, capabilities, ndjson."""
|
|
993
|
+
import shutil
|
|
994
|
+
|
|
995
|
+
from code_atlas.search.vector import sqlite_vec_available
|
|
996
|
+
from code_atlas.storage import schema_version
|
|
997
|
+
|
|
998
|
+
exe = shutil.which("code-atlas") or "<não no PATH — use python -m>"
|
|
999
|
+
checks: dict[str, Any] = {
|
|
1000
|
+
"executable": exe,
|
|
1001
|
+
"python": sys.version.split()[0],
|
|
1002
|
+
"db_path": str(db_path()),
|
|
1003
|
+
"db_exists": db_path().exists(),
|
|
1004
|
+
"sqlite_vec": str(sqlite_vec_available()),
|
|
1005
|
+
}
|
|
1006
|
+
if checks["db_exists"]:
|
|
1007
|
+
conn = _open_db()
|
|
1008
|
+
checks["schema_version"] = schema_version(conn)
|
|
1009
|
+
conn.close()
|
|
1010
|
+
if args.json:
|
|
1011
|
+
print(json.dumps({"tool": "code-atlas",
|
|
1012
|
+
"command": "mcp doctor", **checks}))
|
|
1013
|
+
else:
|
|
1014
|
+
for k, v in checks.items():
|
|
1015
|
+
print(f"{k:<16} {escape_control(str(v))}")
|
|
1016
|
+
return EXIT_OK
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def run_symbol(args: argparse.Namespace) -> int:
|
|
1020
|
+
conn, repo_id, snap_id, repo_info, err = _search_context(args)
|
|
1021
|
+
if err:
|
|
1022
|
+
return _emit_error(args, err)
|
|
1023
|
+
assert (
|
|
1024
|
+
conn is not None and repo_id is not None and snap_id is not None
|
|
1025
|
+
)
|
|
1026
|
+
try:
|
|
1027
|
+
search = StructuralSearch(conn)
|
|
1028
|
+
detail = search.symbol_detail(repo_id, snap_id, args.ident)
|
|
1029
|
+
if detail is None:
|
|
1030
|
+
return _emit_error(args, f"symbol {args.ident!r} not found")
|
|
1031
|
+
payload: dict[str, Any] = {
|
|
1032
|
+
"tool": "code-atlas", "version": __version__,
|
|
1033
|
+
"schema_version": SCHEMA_VERSION, "command": "symbol",
|
|
1034
|
+
"snapshot_id": snap_id, "result": detail,
|
|
1035
|
+
"repository": repo_info, "diagnostics": [],
|
|
1036
|
+
"_human": [
|
|
1037
|
+
f"{detail['kind']} {detail['qualified_name']} "
|
|
1038
|
+
f"({detail['language']})"
|
|
1039
|
+
] + [
|
|
1040
|
+
f" {o['path']}:{o['line']} [{o['parse_status']}]"
|
|
1041
|
+
for o in detail["occurrences"]
|
|
1042
|
+
],
|
|
1043
|
+
}
|
|
1044
|
+
finally:
|
|
1045
|
+
conn.close()
|
|
1046
|
+
return _emit_page(args, payload)
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def run_references(args: argparse.Namespace) -> int:
|
|
1050
|
+
conn, repo_id, snap_id, repo_info, err = _search_context(args)
|
|
1051
|
+
if err:
|
|
1052
|
+
return _emit_error(args, err)
|
|
1053
|
+
assert (
|
|
1054
|
+
conn is not None and repo_id is not None and snap_id is not None
|
|
1055
|
+
)
|
|
1056
|
+
try:
|
|
1057
|
+
search = StructuralSearch(conn)
|
|
1058
|
+
page, rerr = search.references(
|
|
1059
|
+
repo_id, snap_id, args.ident, args.relation_type,
|
|
1060
|
+
clamp_limit(args.limit), max(0, args.offset),
|
|
1061
|
+
)
|
|
1062
|
+
if page is None:
|
|
1063
|
+
return _emit_error(args, rerr)
|
|
1064
|
+
results = [dict(x) for x in page.items]
|
|
1065
|
+
payload: dict[str, Any] = {
|
|
1066
|
+
"tool": "code-atlas", "version": __version__,
|
|
1067
|
+
"schema_version": SCHEMA_VERSION, "command": "references",
|
|
1068
|
+
"symbol": args.ident, "snapshot_id": snap_id,
|
|
1069
|
+
"repository": repo_info, "diagnostics": [],
|
|
1070
|
+
"limit": clamp_limit(args.limit), "offset": args.offset,
|
|
1071
|
+
"total": page.total, "truncated": page.truncated,
|
|
1072
|
+
"results": results,
|
|
1073
|
+
"_human": [
|
|
1074
|
+
f"#{r['relation_id']} {r['type']} "
|
|
1075
|
+
f"{r['from']['symbol']}@{r['from']['path']}:{r['from']['line']} "
|
|
1076
|
+
f"-> {r['to']['symbol'] if r['to'] else r['to_unresolved']} "
|
|
1077
|
+
f"[{r['resolution_status']}]"
|
|
1078
|
+
for r in results
|
|
1079
|
+
],
|
|
1080
|
+
}
|
|
1081
|
+
finally:
|
|
1082
|
+
conn.close()
|
|
1083
|
+
return _emit_page(args, payload)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def run_graph(args: argparse.Namespace) -> int:
|
|
1087
|
+
conn, repo_id, snap_id, repo_info, err = _search_context(args)
|
|
1088
|
+
if err:
|
|
1089
|
+
return _emit_error(args, err)
|
|
1090
|
+
assert (
|
|
1091
|
+
conn is not None and repo_id is not None and snap_id is not None
|
|
1092
|
+
)
|
|
1093
|
+
try:
|
|
1094
|
+
search = StructuralSearch(conn)
|
|
1095
|
+
filters = SearchFilters(
|
|
1096
|
+
language=args.language, kind=args.kind,
|
|
1097
|
+
path=args.path_filter, relation_type=args.relation_type,
|
|
1098
|
+
)
|
|
1099
|
+
dump = search.graph_dump(snap_id, filters, clamp_limit(args.limit))
|
|
1100
|
+
payload: dict[str, Any] = {
|
|
1101
|
+
"tool": "code-atlas", "version": __version__,
|
|
1102
|
+
"schema_version": SCHEMA_VERSION, "command": "graph",
|
|
1103
|
+
"snapshot_id": snap_id,
|
|
1104
|
+
"repository": repo_info, "diagnostics": [], "filters": {
|
|
1105
|
+
"language": args.language, "kind": args.kind,
|
|
1106
|
+
"path": args.path_filter, "relation_type": args.relation_type,
|
|
1107
|
+
},
|
|
1108
|
+
"limit": clamp_limit(args.limit),
|
|
1109
|
+
**dump,
|
|
1110
|
+
"_human": [
|
|
1111
|
+
f"nodes={dump['counts']['nodes']} edges={dump['counts']['edges']}",
|
|
1112
|
+
],
|
|
1113
|
+
}
|
|
1114
|
+
finally:
|
|
1115
|
+
conn.close()
|
|
1116
|
+
return _emit_page(args, payload)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def run_index_plan(args: argparse.Namespace) -> int:
|
|
1120
|
+
"""``index-plan`` (lf-0022): dry-run — plano JSON, zero writes no DB."""
|
|
1121
|
+
if args.path and ".." in Path(args.path).parts:
|
|
1122
|
+
print("error: '..' traversal is not allowed in PATH", file=sys.stderr)
|
|
1123
|
+
return EXIT_UNSAFE
|
|
1124
|
+
target = (
|
|
1125
|
+
Path(args.path).expanduser().resolve()
|
|
1126
|
+
if args.path
|
|
1127
|
+
else resolve_root(args.root)
|
|
1128
|
+
)
|
|
1129
|
+
if not target.is_dir():
|
|
1130
|
+
print(f"error: {escape_control(str(target))} is not a directory",
|
|
1131
|
+
file=sys.stderr)
|
|
1132
|
+
return EXIT_UNSAFE
|
|
1133
|
+
conn = _open_db()
|
|
1134
|
+
try:
|
|
1135
|
+
plan, diag = IndexCoordinator(conn).plan_dry_run(target)
|
|
1136
|
+
finally:
|
|
1137
|
+
conn.close()
|
|
1138
|
+
assert plan is not None # dry-run sempre materializa um plano
|
|
1139
|
+
payload: dict[str, Any] = {
|
|
1140
|
+
"tool": "code-atlas", "version": __version__,
|
|
1141
|
+
"schema_version": SCHEMA_VERSION, "command": "index-plan",
|
|
1142
|
+
"root": str(target), "diagnostics": list(diag.values()),
|
|
1143
|
+
"plan": plan.to_dict(),
|
|
1144
|
+
}
|
|
1145
|
+
if args.json:
|
|
1146
|
+
print(json.dumps(payload))
|
|
1147
|
+
else:
|
|
1148
|
+
counts = plan.counts()
|
|
1149
|
+
print("index-plan (dry-run): " + ", ".join(
|
|
1150
|
+
f"{k}={v}" for k, v in sorted(counts.items())
|
|
1151
|
+
))
|
|
1152
|
+
if diag:
|
|
1153
|
+
for d in diag.values():
|
|
1154
|
+
print(f"note: {escape_control(str(d))}")
|
|
1155
|
+
for f in plan.files:
|
|
1156
|
+
print(escape_control(
|
|
1157
|
+
f" {f.action:<22} {f.path} ({f.reason})"))
|
|
1158
|
+
return EXIT_OK
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def run_definitions(args: argparse.Namespace) -> int:
|
|
1162
|
+
"""``definitions`` (lf-0022): sites de definição do símbolo."""
|
|
1163
|
+
conn, repo_id, snap_id, repo_info, err = _search_context(args)
|
|
1164
|
+
if err:
|
|
1165
|
+
return _emit_error(args, err)
|
|
1166
|
+
assert (
|
|
1167
|
+
conn is not None and repo_id is not None and snap_id is not None
|
|
1168
|
+
)
|
|
1169
|
+
try:
|
|
1170
|
+
search = StructuralSearch(conn)
|
|
1171
|
+
out = search.definitions(repo_id, snap_id, args.ident)
|
|
1172
|
+
if out is None:
|
|
1173
|
+
return _emit_error(args, f"symbol {args.ident!r} not found")
|
|
1174
|
+
defs = out["definitions"]
|
|
1175
|
+
payload: dict[str, Any] = {
|
|
1176
|
+
"tool": "code-atlas", "version": __version__,
|
|
1177
|
+
"schema_version": SCHEMA_VERSION, "command": "definitions",
|
|
1178
|
+
"query": args.ident, "snapshot_id": snap_id,
|
|
1179
|
+
"repository": repo_info, "diagnostics": [],
|
|
1180
|
+
"symbol_id": out["symbol_id"],
|
|
1181
|
+
"qualified_name": out["qualified_name"],
|
|
1182
|
+
"total": len(defs), "truncated": False,
|
|
1183
|
+
"results": defs,
|
|
1184
|
+
"_human": [
|
|
1185
|
+
f"{out['qualified_name']} ({out['kind']})",
|
|
1186
|
+
*(
|
|
1187
|
+
f" {d['path']}:{d['line']}:{d['col']}"
|
|
1188
|
+
f" [{d['parse_status']}]"
|
|
1189
|
+
for d in defs
|
|
1190
|
+
),
|
|
1191
|
+
],
|
|
1192
|
+
}
|
|
1193
|
+
finally:
|
|
1194
|
+
conn.close()
|
|
1195
|
+
return _emit_page(args, payload)
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def run_impact(args: argparse.Namespace) -> int:
|
|
1199
|
+
"""``impact`` (Cycle 5): análise de impacto base e target (snapshots)."""
|
|
1200
|
+
from code_atlas.impact.contracts import (
|
|
1201
|
+
ImpactLimits,
|
|
1202
|
+
ImpactRequest,
|
|
1203
|
+
)
|
|
1204
|
+
from code_atlas.impact.engine import ImpactEngine
|
|
1205
|
+
from code_atlas.impact.report import to_json, to_markdown
|
|
1206
|
+
|
|
1207
|
+
root = resolve_root(args.root)
|
|
1208
|
+
conn = _open_db()
|
|
1209
|
+
try:
|
|
1210
|
+
base = args.base or ("HEAD" if args.cmd in ("worktree", "diff")
|
|
1211
|
+
else "")
|
|
1212
|
+
req = ImpactRequest(
|
|
1213
|
+
repository_root=str(root),
|
|
1214
|
+
base_revision=base,
|
|
1215
|
+
target_revision=(args.target if args.cmd != "worktree"
|
|
1216
|
+
else ""),
|
|
1217
|
+
symbol=args.ident if args.cmd in ("symbol", "contract")
|
|
1218
|
+
else None,
|
|
1219
|
+
known_only=args.known_only,
|
|
1220
|
+
limits=ImpactLimits(
|
|
1221
|
+
max_depth=args.depth, max_nodes=args.max_nodes,
|
|
1222
|
+
max_paths=args.max_paths))
|
|
1223
|
+
res = ImpactEngine(conn).analyze(req)
|
|
1224
|
+
if args.cmd == "tests":
|
|
1225
|
+
# relatório focado em candidatos a teste
|
|
1226
|
+
res = replace(
|
|
1227
|
+
res, symbol_changes=(), contract_changes=(),
|
|
1228
|
+
impacts=())
|
|
1229
|
+
if args.format == "json":
|
|
1230
|
+
print(to_json(res))
|
|
1231
|
+
return 0
|
|
1232
|
+
if args.format == "markdown":
|
|
1233
|
+
print(to_markdown(res))
|
|
1234
|
+
return 0
|
|
1235
|
+
# humano: resumo determinístico
|
|
1236
|
+
print("impact: "
|
|
1237
|
+
+ escape_control(f"{res.base_revision} → {res.target_revision}"))
|
|
1238
|
+
print(f" snapshots: {res.base_snapshot_id} → "
|
|
1239
|
+
f"{res.target_snapshot_id}")
|
|
1240
|
+
print(f" changes: {len(res.symbol_changes)} símbolo(s), "
|
|
1241
|
+
f"{len(res.contract_changes)} contrato(s)")
|
|
1242
|
+
print(f" impacts: {len(res.impacts)} "
|
|
1243
|
+
f"(diretos: {sum(1 for i in res.impacts if i.impact.value == 'direct')})")
|
|
1244
|
+
print(f" tests: {len(res.test_candidates)} candidato(s)")
|
|
1245
|
+
print(f" risk: {res.risk.category.value} "
|
|
1246
|
+
f"(uncertainty={res.risk.uncertainty})")
|
|
1247
|
+
for r in res.risk.reasons:
|
|
1248
|
+
print(escape_control(f" - {r}"))
|
|
1249
|
+
for d in res.diagnostics:
|
|
1250
|
+
print(escape_control(f" diagnostic {d.code}: {d.message}"),
|
|
1251
|
+
file=sys.stderr)
|
|
1252
|
+
if res.truncated:
|
|
1253
|
+
# lf-0151: paridade com JSON/Markdown — as razões do
|
|
1254
|
+
# corte fazem parte do estado reportado.
|
|
1255
|
+
why = escape_control(", ".join(res.truncation_reasons))
|
|
1256
|
+
print(" TRUNCATED — limites atingidos"
|
|
1257
|
+
+ (f" ({why})" if why else ""))
|
|
1258
|
+
return 0
|
|
1259
|
+
finally:
|
|
1260
|
+
conn.close()
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _fed_cmd_create(fed: Federation, args: argparse.Namespace) -> dict[str, Any]:
|
|
1264
|
+
ws = fed.create_workspace(args.name)
|
|
1265
|
+
return {"workspace_id": ws.id, "workspace": ws.name, "created": True}
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def _fed_cmd_add(fed: Federation, args: argparse.Namespace) -> dict[str, Any]:
|
|
1269
|
+
ws = fed.get_workspace(args.name)
|
|
1270
|
+
repo_id = fed.resolve_repository(args.repo)
|
|
1271
|
+
member = fed.add_member(ws, repo_id)
|
|
1272
|
+
return {
|
|
1273
|
+
"workspace_id": ws.id, "workspace": ws.name,
|
|
1274
|
+
"repository_id": member.repository_id, "member_id": member.id,
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def _fed_cmd_pin(fed: Federation, args: argparse.Namespace) -> dict[str, Any]:
|
|
1279
|
+
from code_atlas.federation import InvalidPinError
|
|
1280
|
+
|
|
1281
|
+
ws = fed.get_workspace(args.name)
|
|
1282
|
+
repo_id = fed.resolve_repository(args.repo)
|
|
1283
|
+
if args.snapshot == "latest":
|
|
1284
|
+
member = fed.unpin_member(ws, repo_id)
|
|
1285
|
+
else:
|
|
1286
|
+
try:
|
|
1287
|
+
snap_id = int(args.snapshot)
|
|
1288
|
+
except ValueError as exc:
|
|
1289
|
+
raise InvalidPinError(
|
|
1290
|
+
f"snapshot must be an id or 'latest': {args.snapshot!r}"
|
|
1291
|
+
) from exc
|
|
1292
|
+
member = fed.pin_member(ws, repo_id, snap_id)
|
|
1293
|
+
return {
|
|
1294
|
+
"workspace_id": ws.id, "workspace": ws.name,
|
|
1295
|
+
"repository_id": member.repository_id,
|
|
1296
|
+
"pinned_snapshot_id": member.pinned_snapshot_id,
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _fed_cmd_view(fed: Federation, args: argparse.Namespace) -> dict[str, Any]:
|
|
1301
|
+
from code_atlas.federation import InvalidPinError
|
|
1302
|
+
|
|
1303
|
+
ws = fed.get_workspace(args.name)
|
|
1304
|
+
pins: dict[int, int] = {}
|
|
1305
|
+
for seg in (args.pin or "").split(","):
|
|
1306
|
+
seg = seg.strip()
|
|
1307
|
+
if not seg:
|
|
1308
|
+
continue
|
|
1309
|
+
repo, sep, snap = seg.partition(":")
|
|
1310
|
+
if not sep or not repo.isdigit() or not snap.isdigit():
|
|
1311
|
+
raise InvalidPinError(
|
|
1312
|
+
"pin must be repo:snapshot[,repo:snapshot…]"
|
|
1313
|
+
)
|
|
1314
|
+
pins[int(repo)] = int(snap)
|
|
1315
|
+
return fed.resolve_view(ws, pins).to_dict()
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
_FED_COMMANDS = {
|
|
1319
|
+
"create": _fed_cmd_create,
|
|
1320
|
+
"add": _fed_cmd_add,
|
|
1321
|
+
"pin": _fed_cmd_pin,
|
|
1322
|
+
"view": _fed_cmd_view,
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
_FED_REQUIRED = {
|
|
1326
|
+
"create": ("name",), "add": ("name", "repo"),
|
|
1327
|
+
"pin": ("name", "repo", "snapshot"), "view": ("name",),
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def run_federation(args: argparse.Namespace) -> int:
|
|
1332
|
+
"""``federation`` (lf-0118, Phase 10): agrupamentos multi-repo.
|
|
1333
|
+
|
|
1334
|
+
A mutação (create/add/pin) é superfície exclusiva da CLI — a API HTTP
|
|
1335
|
+
expõe somente views/lookups/graphs GET. Membros só entram de repos
|
|
1336
|
+
**já indexados**; nada de auto-discovery de diretórios.
|
|
1337
|
+
"""
|
|
1338
|
+
from code_atlas.federation import Federation, FederationError
|
|
1339
|
+
|
|
1340
|
+
cmd = getattr(args, "fed_cmd", "view") or "view"
|
|
1341
|
+
missing = [k for k in _FED_REQUIRED.get(cmd, ())
|
|
1342
|
+
if not getattr(args, k, None)]
|
|
1343
|
+
if missing:
|
|
1344
|
+
print(f"error: federation {cmd} requires "
|
|
1345
|
+
f"{', '.join(missing)}", file=sys.stderr)
|
|
1346
|
+
return EXIT_USAGE
|
|
1347
|
+
conn = _open_db()
|
|
1348
|
+
try:
|
|
1349
|
+
try:
|
|
1350
|
+
payload = _FED_COMMANDS[cmd](Federation(conn), args)
|
|
1351
|
+
except FederationError as exc:
|
|
1352
|
+
return _emit_error(args, str(exc))
|
|
1353
|
+
finally:
|
|
1354
|
+
conn.close()
|
|
1355
|
+
payload["tool"] = "code-atlas"
|
|
1356
|
+
payload["version"] = __version__
|
|
1357
|
+
payload["command"] = f"federation {cmd}"
|
|
1358
|
+
if args.json:
|
|
1359
|
+
print(json.dumps(payload))
|
|
1360
|
+
elif cmd == "view":
|
|
1361
|
+
print(f"view {escape_control(str(payload['view_id']))}")
|
|
1362
|
+
for m in payload["members"]:
|
|
1363
|
+
print(
|
|
1364
|
+
f" repo={m['repository_id']} "
|
|
1365
|
+
f"snapshot={m['snapshot_id']} "
|
|
1366
|
+
f"[{m['resolution']}] {m['snapshot_status']}"
|
|
1367
|
+
)
|
|
1368
|
+
else:
|
|
1369
|
+
for k, v in payload.items():
|
|
1370
|
+
if not k.startswith("_") and k not in ("tool", "version",
|
|
1371
|
+
"command"):
|
|
1372
|
+
print(f"{k}: {escape_control(str(v))}")
|
|
1373
|
+
return EXIT_OK
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
# --- domains (lf-0123, Cycle 7 Phase 15) ------------------------------------
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def _domain_repo(
|
|
1380
|
+
conn: sqlite3.Connection, args: argparse.Namespace, ident: str | None
|
|
1381
|
+
) -> tuple[int | None, str]:
|
|
1382
|
+
"""Escopo do repo: ``--repo`` > ``--root`` > derivado do ``ident``.
|
|
1383
|
+
|
|
1384
|
+
``--repo`` aceita id | identity_key | root_handle | path (mesma
|
|
1385
|
+
resolução de ``federation add`` — reusa ``Federation.resolve_repository``).
|
|
1386
|
+
``ident`` numérico/``dom:``/nome único deriva o repo do próprio dado
|
|
1387
|
+
— ``domains entity`` funciona sem flags quando a chave é exata.
|
|
1388
|
+
"""
|
|
1389
|
+
repo_ref = getattr(args, "repo", None)
|
|
1390
|
+
root_arg = getattr(args, "root", None)
|
|
1391
|
+
if repo_ref:
|
|
1392
|
+
from code_atlas.federation import Federation, FederationError
|
|
1393
|
+
|
|
1394
|
+
try:
|
|
1395
|
+
return Federation(conn).resolve_repository(str(repo_ref)), ""
|
|
1396
|
+
except FederationError as exc:
|
|
1397
|
+
return None, str(exc)
|
|
1398
|
+
if root_arg:
|
|
1399
|
+
root = resolve_root(root_arg)
|
|
1400
|
+
repo_id = StructuralSearch(conn).repo_for_root(str(root))
|
|
1401
|
+
if repo_id is None:
|
|
1402
|
+
return None, f"no indexed repository at {root}"
|
|
1403
|
+
return repo_id, ""
|
|
1404
|
+
if ident:
|
|
1405
|
+
from code_atlas.domains.cli_surface import repo_for_ident
|
|
1406
|
+
|
|
1407
|
+
repo_id, derr = repo_for_ident(conn, ident)
|
|
1408
|
+
if derr:
|
|
1409
|
+
return None, derr
|
|
1410
|
+
if repo_id is not None:
|
|
1411
|
+
return repo_id, ""
|
|
1412
|
+
root = resolve_root(root_arg)
|
|
1413
|
+
repo_id = StructuralSearch(conn).repo_for_root(str(root))
|
|
1414
|
+
if repo_id is None:
|
|
1415
|
+
return None, f"no indexed repository at {root}"
|
|
1416
|
+
return repo_id, ""
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def _domain_context(
|
|
1420
|
+
args: argparse.Namespace, ident: str | None = None
|
|
1421
|
+
) -> tuple[
|
|
1422
|
+
sqlite3.Connection | None,
|
|
1423
|
+
int | None,
|
|
1424
|
+
int | None,
|
|
1425
|
+
dict[str, Any] | None,
|
|
1426
|
+
str,
|
|
1427
|
+
]:
|
|
1428
|
+
"""(conn, repo_id, snapshot_id, repo_info, err) — mesmo contrato de
|
|
1429
|
+
``_search_context``, mas o repo pode vir de ``--repo``/``ident``."""
|
|
1430
|
+
conn = _open_db()
|
|
1431
|
+
repo_id, err = _domain_repo(conn, args, ident)
|
|
1432
|
+
if err or repo_id is None:
|
|
1433
|
+
conn.close()
|
|
1434
|
+
return None, None, None, None, err or "repository unresolved"
|
|
1435
|
+
search = StructuralSearch(conn)
|
|
1436
|
+
snap, serr = search.resolve_snapshot(
|
|
1437
|
+
repo_id, getattr(args, "snapshot", None))
|
|
1438
|
+
if serr or snap is None or snap.id is None:
|
|
1439
|
+
conn.close()
|
|
1440
|
+
return None, None, None, None, serr or "snapshot missing id"
|
|
1441
|
+
repo_info = search.repository_info(repo_id) or {"id": repo_id}
|
|
1442
|
+
return conn, repo_id, int(snap.id), repo_info, ""
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def _dom_cmd_list(
|
|
1446
|
+
args: argparse.Namespace,
|
|
1447
|
+
) -> tuple[dict[str, Any] | None, str]:
|
|
1448
|
+
"""``domains list`` — adapters registrados (não extrai nada)."""
|
|
1449
|
+
from code_atlas.domains.cli_surface import adapters_report
|
|
1450
|
+
|
|
1451
|
+
adapters = adapters_report()
|
|
1452
|
+
width = max((len(a["domain_id"]) for a in adapters), default=0)
|
|
1453
|
+
return {
|
|
1454
|
+
"command": "domains list",
|
|
1455
|
+
"adapters": adapters,
|
|
1456
|
+
"notes": [
|
|
1457
|
+
"registered adapters are opt-in: extraction only runs when "
|
|
1458
|
+
"`code-atlas index --domains` selects them",
|
|
1459
|
+
],
|
|
1460
|
+
"_human": [
|
|
1461
|
+
"domain".ljust(width) + " version enabled artifacts",
|
|
1462
|
+
*(
|
|
1463
|
+
f"{a['domain_id']:<{width}} {a['adapter_version']:<8} "
|
|
1464
|
+
f"{'yes' if a['enabled'] else 'no':<7} "
|
|
1465
|
+
+ ",".join(a["artifact_support"]["extensions"] or ["-"])
|
|
1466
|
+
+ (f"; globs={a['artifact_support']['globs']}"
|
|
1467
|
+
if a["artifact_support"]["globs"] else "")
|
|
1468
|
+
for a in adapters
|
|
1469
|
+
),
|
|
1470
|
+
"note: opt-in — `index --domains <ids|all>` enables extraction",
|
|
1471
|
+
],
|
|
1472
|
+
}, ""
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def _dom_cmd_entities(
|
|
1476
|
+
args: argparse.Namespace,
|
|
1477
|
+
) -> tuple[dict[str, Any] | None, str]:
|
|
1478
|
+
"""``domains entities`` — listagem determinística do snapshot."""
|
|
1479
|
+
from code_atlas.domains.cli_surface import (
|
|
1480
|
+
NOTE_OPT_IN,
|
|
1481
|
+
list_entities,
|
|
1482
|
+
)
|
|
1483
|
+
from code_atlas.domains.model import DomainEntityKind
|
|
1484
|
+
|
|
1485
|
+
kind = (getattr(args, "kind", None) or "").strip()
|
|
1486
|
+
if kind and kind not in {str(k) for k in DomainEntityKind}:
|
|
1487
|
+
return None, (
|
|
1488
|
+
f"unknown --kind {kind!r} (valid: "
|
|
1489
|
+
f"{', '.join(sorted(str(k) for k in DomainEntityKind))})"
|
|
1490
|
+
)
|
|
1491
|
+
conn, repo_id, snap_id, repo_info, err = _domain_context(args)
|
|
1492
|
+
if err:
|
|
1493
|
+
return None, err
|
|
1494
|
+
assert conn is not None and repo_id is not None and snap_id is not None
|
|
1495
|
+
try:
|
|
1496
|
+
limit = clamp_limit(args.limit)
|
|
1497
|
+
offset = max(0, getattr(args, "offset", 0) or 0)
|
|
1498
|
+
domain = (getattr(args, "domain", None) or "").strip()
|
|
1499
|
+
q = (getattr(args, "q", None) or "").strip()
|
|
1500
|
+
out = list_entities(
|
|
1501
|
+
conn, repo_id, snap_id,
|
|
1502
|
+
domain=domain, kind=kind, q=q, limit=limit, offset=offset)
|
|
1503
|
+
finally:
|
|
1504
|
+
conn.close()
|
|
1505
|
+
notes = [NOTE_OPT_IN] if out["total"] == 0 else []
|
|
1506
|
+
items = out["items"]
|
|
1507
|
+
return {
|
|
1508
|
+
"command": "domains entities",
|
|
1509
|
+
"repository": repo_info,
|
|
1510
|
+
"snapshot_id": snap_id,
|
|
1511
|
+
"filters": {"domain": domain, "kind": kind, "q": q},
|
|
1512
|
+
"limit": limit,
|
|
1513
|
+
"offset": offset,
|
|
1514
|
+
"total": out["total"],
|
|
1515
|
+
"truncated": out["truncated"],
|
|
1516
|
+
"items": items,
|
|
1517
|
+
"notes": notes,
|
|
1518
|
+
"_human": [
|
|
1519
|
+
*(
|
|
1520
|
+
f"{i['entity_id']:>4} {i['domain']:<10} {i['kind']:<20} "
|
|
1521
|
+
f"{i['display_name']} x{i['occurrences']}"
|
|
1522
|
+
f"{' ' + i['path'] if i['path'] else ''}"
|
|
1523
|
+
for i in items
|
|
1524
|
+
),
|
|
1525
|
+
*notes,
|
|
1526
|
+
],
|
|
1527
|
+
}, ""
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def _dom_cmd_entity(
|
|
1531
|
+
args: argparse.Namespace,
|
|
1532
|
+
) -> tuple[dict[str, Any] | None, str]:
|
|
1533
|
+
"""``domains entity <key>`` — cartão de identidade + proveniência."""
|
|
1534
|
+
from code_atlas.domains.cli_surface import (
|
|
1535
|
+
entity_detail,
|
|
1536
|
+
resolve_single_entity,
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
ident = str(args.ident)
|
|
1540
|
+
conn, repo_id, snap_id, repo_info, err = _domain_context(args, ident)
|
|
1541
|
+
if err:
|
|
1542
|
+
return None, err
|
|
1543
|
+
assert conn is not None and repo_id is not None and snap_id is not None
|
|
1544
|
+
try:
|
|
1545
|
+
entity, eerr = resolve_single_entity(conn, repo_id, ident)
|
|
1546
|
+
if entity is None:
|
|
1547
|
+
return None, eerr
|
|
1548
|
+
detail = entity_detail(conn, entity, snap_id)
|
|
1549
|
+
finally:
|
|
1550
|
+
conn.close()
|
|
1551
|
+
identd = detail["identity"]
|
|
1552
|
+
notes: list[str] = []
|
|
1553
|
+
if not detail["occurrences"]:
|
|
1554
|
+
notes.append(
|
|
1555
|
+
f"no occurrences in snapshot {snap_id} — entity was "
|
|
1556
|
+
"extracted under another snapshot or a different "
|
|
1557
|
+
"enablement set"
|
|
1558
|
+
)
|
|
1559
|
+
human = [
|
|
1560
|
+
f"entity #{detail['entity_id']} {identd['display_name']} "
|
|
1561
|
+
f"({identd['kind']}) — domain={identd['domain']}",
|
|
1562
|
+
f"identity_key: {identd['identity_key']}",
|
|
1563
|
+
f" catalog={identd['catalog'] or '-'} "
|
|
1564
|
+
f"namespace={identd['namespace'] or '-'} "
|
|
1565
|
+
f"environment={identd['environment'] or '-'} "
|
|
1566
|
+
f"name={identd['name']}",
|
|
1567
|
+
f"occurrences (snapshot {snap_id}): "
|
|
1568
|
+
f"{len(detail['occurrences'])}",
|
|
1569
|
+
*(
|
|
1570
|
+
f" {o['path'] or '<synthesized>'}:{o['line'] or '-'}:"
|
|
1571
|
+
f"{o['col'] or '-'} [{o['extraction_status']}]"
|
|
1572
|
+
for o in detail["occurrences"]
|
|
1573
|
+
),
|
|
1574
|
+
f"relations: {sum(r['count'] for r in detail['relations_summary'])}",
|
|
1575
|
+
*(
|
|
1576
|
+
f" {r['direction']:<4} {r['type']:<20} {r['status']:<11} "
|
|
1577
|
+
f"x{r['count']}"
|
|
1578
|
+
for r in detail["relations_summary"]
|
|
1579
|
+
),
|
|
1580
|
+
f"evidence: {detail['evidence_count']} distinct row(s)",
|
|
1581
|
+
*notes,
|
|
1582
|
+
]
|
|
1583
|
+
return {
|
|
1584
|
+
"command": "domains entity",
|
|
1585
|
+
"repository": repo_info,
|
|
1586
|
+
"snapshot_id": snap_id,
|
|
1587
|
+
"entity": detail,
|
|
1588
|
+
"notes": notes,
|
|
1589
|
+
"_human": human,
|
|
1590
|
+
}, ""
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _dom_cmd_lineage(
|
|
1594
|
+
args: argparse.Namespace,
|
|
1595
|
+
) -> tuple[dict[str, Any] | None, str]:
|
|
1596
|
+
"""``domains lineage <table>`` — traversal de fluxo bounded."""
|
|
1597
|
+
from code_atlas.domains.cli_surface import (
|
|
1598
|
+
DATA_FLOW_TYPES,
|
|
1599
|
+
lineage,
|
|
1600
|
+
resolve_single_entity,
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
ident = str(args.ident)
|
|
1604
|
+
conn, repo_id, snap_id, repo_info, err = _domain_context(args, ident)
|
|
1605
|
+
if err:
|
|
1606
|
+
return None, err
|
|
1607
|
+
assert conn is not None and repo_id is not None and snap_id is not None
|
|
1608
|
+
try:
|
|
1609
|
+
entity, eerr = resolve_single_entity(conn, repo_id, ident)
|
|
1610
|
+
if entity is None:
|
|
1611
|
+
return None, eerr
|
|
1612
|
+
out = lineage(
|
|
1613
|
+
conn, entity, snap_id,
|
|
1614
|
+
direction=getattr(args, "direction", "both"),
|
|
1615
|
+
depth=clamp_depth(getattr(args, "depth", 2)),
|
|
1616
|
+
limit=clamp_limit(getattr(args, "limit", None)))
|
|
1617
|
+
finally:
|
|
1618
|
+
conn.close()
|
|
1619
|
+
types = ", ".join(sorted(DATA_FLOW_TYPES))
|
|
1620
|
+
notes = (
|
|
1621
|
+
[f"no data-flow relations ({types}) touch this entity in "
|
|
1622
|
+
f"snapshot {snap_id}"]
|
|
1623
|
+
if not out["edges"] else []
|
|
1624
|
+
)
|
|
1625
|
+
ent = out["entity"]
|
|
1626
|
+
human = [
|
|
1627
|
+
f"lineage of {ent['display_name']} ({ent['kind']}) — "
|
|
1628
|
+
f"direction={out['direction']} depth<={out['depth']} "
|
|
1629
|
+
f"limit={out['limit']} truncated={out['truncated']}",
|
|
1630
|
+
]
|
|
1631
|
+
if out["direction"] in ("up", "both"):
|
|
1632
|
+
human.append(f"upstream (feeds {ent['display_name']}):")
|
|
1633
|
+
human += [
|
|
1634
|
+
f" d{e['depth']} {e['from']} → {e['to']} "
|
|
1635
|
+
f"[{e['type']} {e['resolution_status']}]"
|
|
1636
|
+
for e in out["edges"] if e["via"] == "up"
|
|
1637
|
+
]
|
|
1638
|
+
if out["direction"] in ("down", "both"):
|
|
1639
|
+
human.append(f"downstream (fed by {ent['display_name']}):")
|
|
1640
|
+
human += [
|
|
1641
|
+
f" d{e['depth']} {e['from']} → {e['to']} "
|
|
1642
|
+
f"[{e['type']} {e['resolution_status']}]"
|
|
1643
|
+
for e in out["edges"] if e["via"] == "down"
|
|
1644
|
+
]
|
|
1645
|
+
human += notes
|
|
1646
|
+
payload = {
|
|
1647
|
+
"command": "domains lineage",
|
|
1648
|
+
"repository": repo_info,
|
|
1649
|
+
"snapshot_id": snap_id,
|
|
1650
|
+
"notes": notes,
|
|
1651
|
+
"_human": human,
|
|
1652
|
+
}
|
|
1653
|
+
payload.update(out)
|
|
1654
|
+
return payload, ""
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
_DOM_COMMANDS = {
|
|
1658
|
+
"list": _dom_cmd_list,
|
|
1659
|
+
"entities": _dom_cmd_entities,
|
|
1660
|
+
"entity": _dom_cmd_entity,
|
|
1661
|
+
"lineage": _dom_cmd_lineage,
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
_DOM_REQUIRED = {
|
|
1665
|
+
"entity": ("ident",),
|
|
1666
|
+
"lineage": ("ident",),
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
def run_domains(args: argparse.Namespace) -> int:
|
|
1671
|
+
"""``domains`` (lf-0123, Phase 15): superfície read-only de domínio.
|
|
1672
|
+
|
|
1673
|
+
Todos os subcomandos são somente leitura e ancorados no snapshot
|
|
1674
|
+
resolvido — adapters desabilitados produzem resposta vazia com
|
|
1675
|
+
nota, nunca erro.
|
|
1676
|
+
"""
|
|
1677
|
+
cmd = getattr(args, "dom_cmd", "list") or "list"
|
|
1678
|
+
missing = [k for k in _DOM_REQUIRED.get(cmd, ())
|
|
1679
|
+
if not getattr(args, k, None)]
|
|
1680
|
+
if missing:
|
|
1681
|
+
print(f"error: domains {cmd} requires "
|
|
1682
|
+
f"{', '.join(missing)}", file=sys.stderr)
|
|
1683
|
+
return EXIT_USAGE
|
|
1684
|
+
try:
|
|
1685
|
+
payload, err = _DOM_COMMANDS[cmd](args)
|
|
1686
|
+
except KeyError:
|
|
1687
|
+
print(f"error: unknown domains command {cmd!r}", file=sys.stderr)
|
|
1688
|
+
return EXIT_USAGE
|
|
1689
|
+
if err or payload is None:
|
|
1690
|
+
return _emit_error(args, err or "domains command failed")
|
|
1691
|
+
payload["tool"] = "code-atlas"
|
|
1692
|
+
payload["version"] = __version__
|
|
1693
|
+
payload["schema_version"] = SCHEMA_VERSION
|
|
1694
|
+
return _emit_page(args, payload)
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def run_dependencies(args: argparse.Namespace) -> int:
|
|
1698
|
+
"""``dependencies`` (lf-0022): arestas saíntes, BFS limitado."""
|
|
1699
|
+
conn, repo_id, snap_id, repo_info, err = _search_context(args)
|
|
1700
|
+
if err:
|
|
1701
|
+
return _emit_error(args, err)
|
|
1702
|
+
assert (
|
|
1703
|
+
conn is not None and repo_id is not None and snap_id is not None
|
|
1704
|
+
)
|
|
1705
|
+
depth = clamp_depth(args.depth)
|
|
1706
|
+
try:
|
|
1707
|
+
search = StructuralSearch(conn)
|
|
1708
|
+
out, derr = search.dependencies(
|
|
1709
|
+
repo_id, snap_id, args.ident, depth,
|
|
1710
|
+
clamp_limit(args.limit), max(0, args.offset),
|
|
1711
|
+
)
|
|
1712
|
+
if out is None:
|
|
1713
|
+
return _emit_error(args, derr)
|
|
1714
|
+
results = out["results"]
|
|
1715
|
+
payload: dict[str, Any] = {
|
|
1716
|
+
"tool": "code-atlas", "version": __version__,
|
|
1717
|
+
"schema_version": SCHEMA_VERSION, "command": "dependencies",
|
|
1718
|
+
"query": args.ident, "snapshot_id": snap_id,
|
|
1719
|
+
"repository": repo_info, "diagnostics": [],
|
|
1720
|
+
"symbol_id": out["symbol_id"],
|
|
1721
|
+
"qualified_name": out["qualified_name"],
|
|
1722
|
+
"depth": depth, "limit": clamp_limit(args.limit),
|
|
1723
|
+
"offset": max(0, args.offset),
|
|
1724
|
+
"total": out["total"], "truncated": out["truncated"],
|
|
1725
|
+
"visited_symbols": out["visited_symbols"],
|
|
1726
|
+
"results": results,
|
|
1727
|
+
"_human": [
|
|
1728
|
+
f"deps of {out['qualified_name']} "
|
|
1729
|
+
f"(depth={depth}, visited={out['visited_symbols']})",
|
|
1730
|
+
*(
|
|
1731
|
+
f" d{r['depth']} {r['type']} -> "
|
|
1732
|
+
f"{r['to']['symbol'] if r['to'] else r['to_unresolved']}"
|
|
1733
|
+
f" [{r['resolution_status']}]"
|
|
1734
|
+
for r in results
|
|
1735
|
+
),
|
|
1736
|
+
],
|
|
1737
|
+
}
|
|
1738
|
+
finally:
|
|
1739
|
+
conn.close()
|
|
1740
|
+
return _emit_page(args, payload)
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
def clamp_depth(depth: int | None) -> int:
|
|
1744
|
+
"""Sane default + teto duro de traversal (lf-0022)."""
|
|
1745
|
+
if depth is None:
|
|
1746
|
+
return 1
|
|
1747
|
+
return max(1, min(int(depth), MAX_DEPTH))
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
1751
|
+
parser = argparse.ArgumentParser(
|
|
1752
|
+
prog="code-atlas",
|
|
1753
|
+
description="Code Atlas — inteligência estrutural de código local-first (Cycle 1).",
|
|
1754
|
+
epilog="exit codes: 0 ok | 1 error | 2 usage | 3 unsafe/blocked",
|
|
1755
|
+
)
|
|
1756
|
+
parser.add_argument("--version", action="version", version=f"code-atlas {__version__}")
|
|
1757
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
1758
|
+
|
|
1759
|
+
p_doctor = sub.add_parser("doctor", help="verifica ambiente, config e dependências")
|
|
1760
|
+
p_doctor.add_argument("--json", action="store_true", help="saída em JSON")
|
|
1761
|
+
p_doctor.set_defaults(func=run_doctor)
|
|
1762
|
+
|
|
1763
|
+
p_init = sub.add_parser("init", help="inicializa config, data dir e registro do repo")
|
|
1764
|
+
p_init.add_argument("--root", metavar="PATH", help="raiz do workspace (default: cwd)")
|
|
1765
|
+
p_init.add_argument("--force", action="store_true", help="sobrescreve config existente")
|
|
1766
|
+
p_init.add_argument("--json", action="store_true", help="saída em JSON")
|
|
1767
|
+
p_init.set_defaults(func=run_init)
|
|
1768
|
+
|
|
1769
|
+
p_inst = sub.add_parser(
|
|
1770
|
+
"install",
|
|
1771
|
+
help="provisiona ambiente local (data dir, config, registro) + receipt")
|
|
1772
|
+
p_inst.add_argument(
|
|
1773
|
+
"action", nargs="?", choices=("install", "status", "doctor"),
|
|
1774
|
+
default="install",
|
|
1775
|
+
help="install=plano/aplica; status=recibo gravado; doctor=saúde")
|
|
1776
|
+
p_inst.add_argument("--scope", choices=("project", "user"),
|
|
1777
|
+
default="project",
|
|
1778
|
+
help="project=workspace atual; user=só data dir global")
|
|
1779
|
+
p_inst.add_argument("--root", metavar="PATH",
|
|
1780
|
+
help="raiz do workspace (default: cwd)")
|
|
1781
|
+
p_inst.add_argument("--profile",
|
|
1782
|
+
choices=("minimal", "recommended", "full"),
|
|
1783
|
+
default="recommended")
|
|
1784
|
+
p_inst.add_argument("--components", metavar="CSV", default=None,
|
|
1785
|
+
help="csv de componentes: config,data-dir,repository")
|
|
1786
|
+
p_inst.add_argument("--dry-run", action="store_true",
|
|
1787
|
+
help="mostra o plano; não escreve nada")
|
|
1788
|
+
p_inst.add_argument("--yes", "-y", action="store_true",
|
|
1789
|
+
help="aprovação explícita para aplicar")
|
|
1790
|
+
p_inst.add_argument("--json", action="store_true", help="saída em JSON")
|
|
1791
|
+
from code_atlas.install.cli import run_install
|
|
1792
|
+
|
|
1793
|
+
p_inst.set_defaults(func=run_install)
|
|
1794
|
+
|
|
1795
|
+
p_index = sub.add_parser("index", help="indexa inventário de um diretório (snapshot)")
|
|
1796
|
+
p_index.add_argument("path", nargs="?", metavar="PATH", help="diretório a indexar")
|
|
1797
|
+
p_index.add_argument("--root", metavar="PATH", help="raiz alternativa (se PATH ausente)")
|
|
1798
|
+
p_index.add_argument(
|
|
1799
|
+
"--full", action="store_true", help="força re-scan mesmo sem mudanças"
|
|
1800
|
+
)
|
|
1801
|
+
p_index.add_argument(
|
|
1802
|
+
"--incremental", action="store_true", default=True,
|
|
1803
|
+
help="indexação incremental (default ativo; --full desativa)",
|
|
1804
|
+
)
|
|
1805
|
+
p_index.add_argument(
|
|
1806
|
+
"--scip", metavar="PATH", default=None,
|
|
1807
|
+
help="índice SCIP ad-hoc (sobrescreve [resolvers.scip].index_path)",
|
|
1808
|
+
)
|
|
1809
|
+
p_index.add_argument(
|
|
1810
|
+
"--resolver", metavar="NAME", action="append", default=None,
|
|
1811
|
+
help="restringe resolvers ativos (syntactic|scip|lsp|lsp:<nome>); "
|
|
1812
|
+
"repetível ou separado por vírgula",
|
|
1813
|
+
)
|
|
1814
|
+
p_index.add_argument(
|
|
1815
|
+
"--domains", metavar="LIST", default=None,
|
|
1816
|
+
help="adapters de domínio opt-in: ids separados por vírgula "
|
|
1817
|
+
"(sql,pyspark,aws-data,terraform,cicd,api) ou 'all' — "
|
|
1818
|
+
"default OFF (ver `code-atlas domains list`)",
|
|
1819
|
+
)
|
|
1820
|
+
p_index.add_argument(
|
|
1821
|
+
"--cross-domain", action="store_true",
|
|
1822
|
+
help="resolução cross-domain no snapshot (requer --domains)",
|
|
1823
|
+
)
|
|
1824
|
+
p_index.add_argument(
|
|
1825
|
+
"--verbose", action="store_true",
|
|
1826
|
+
help="progresso por estágio em stderr: ms por etapa + files/s "
|
|
1827
|
+
"no parse (stdout e --json não são tocados)",
|
|
1828
|
+
)
|
|
1829
|
+
p_index.add_argument("--json", action="store_true", help="saída em JSON")
|
|
1830
|
+
p_index.set_defaults(func=run_index)
|
|
1831
|
+
|
|
1832
|
+
p_sem = sub.add_parser(
|
|
1833
|
+
"semantic-status",
|
|
1834
|
+
help="resolvers instalados/habilitados/usados + cobertura semântica",
|
|
1835
|
+
)
|
|
1836
|
+
p_sem.add_argument("path", nargs="?", metavar="PATH",
|
|
1837
|
+
help="diretório do workspace")
|
|
1838
|
+
p_sem.add_argument("--root", metavar="PATH",
|
|
1839
|
+
help="raiz alternativa (se PATH ausente)")
|
|
1840
|
+
p_sem.add_argument("--json", action="store_true", help="saída em JSON")
|
|
1841
|
+
p_sem.set_defaults(func=run_semantic_status)
|
|
1842
|
+
|
|
1843
|
+
p_plan = sub.add_parser(
|
|
1844
|
+
"index-plan", help="dry-run: plano de indexação em JSON (zero writes)"
|
|
1845
|
+
)
|
|
1846
|
+
p_plan.add_argument("path", nargs="?", metavar="PATH",
|
|
1847
|
+
help="diretório a planejar")
|
|
1848
|
+
p_plan.add_argument("--root", metavar="PATH",
|
|
1849
|
+
help="raiz alternativa (se PATH ausente)")
|
|
1850
|
+
p_plan.add_argument("--json", action="store_true", help="saída em JSON")
|
|
1851
|
+
p_plan.set_defaults(func=run_index_plan)
|
|
1852
|
+
|
|
1853
|
+
p_status = sub.add_parser("status", help="estado do índice (git, snapshot, migrações)")
|
|
1854
|
+
p_status.add_argument("--root", metavar="PATH", help="raiz do workspace (default: cwd)")
|
|
1855
|
+
p_status.add_argument("--json", action="store_true", help="saída em JSON")
|
|
1856
|
+
p_status.set_defaults(func=run_status)
|
|
1857
|
+
|
|
1858
|
+
p_search = sub.add_parser(
|
|
1859
|
+
"search", help="busca estrutural por símbolo (substring)"
|
|
1860
|
+
)
|
|
1861
|
+
p_search.add_argument("query", metavar="QUERY", help="substring do qualified_name")
|
|
1862
|
+
p_search.add_argument(
|
|
1863
|
+
"--strategy", metavar="NAME", default="structural",
|
|
1864
|
+
help="structural|lexical|graph|hybrid (default structural)")
|
|
1865
|
+
_add_query_args(p_search, p_path=True)
|
|
1866
|
+
p_search.set_defaults(func=run_search)
|
|
1867
|
+
|
|
1868
|
+
p_ctx = sub.add_parser(
|
|
1869
|
+
"context", help="context pack sob budget de tokens (lf-0040)")
|
|
1870
|
+
p_ctx.add_argument("task", metavar="TASK", help="descrição da tarefa")
|
|
1871
|
+
p_ctx.add_argument("--level", choices=("orientation", "focus", "deep"),
|
|
1872
|
+
default="focus", help="nível de detalhe")
|
|
1873
|
+
p_ctx.add_argument("--budget", metavar="TOKENS", type=int, default=4000,
|
|
1874
|
+
help="orçamento estimado (default 4000)")
|
|
1875
|
+
p_ctx.add_argument("--format", choices=("json", "markdown", "human"),
|
|
1876
|
+
default="human", help="renderer de saída")
|
|
1877
|
+
_add_query_args(p_ctx, filters=False)
|
|
1878
|
+
p_ctx.set_defaults(func=run_context)
|
|
1879
|
+
|
|
1880
|
+
p_exp = sub.add_parser(
|
|
1881
|
+
"context-expand",
|
|
1882
|
+
help="expande símbolo no MESMO snapshot do pack (lf-0041)")
|
|
1883
|
+
p_exp.add_argument("--symbol", metavar="SYMBOL", required=True,
|
|
1884
|
+
help="qualified_name (ou sufixo) a expandir")
|
|
1885
|
+
p_exp.add_argument("--delivered", metavar="IDS", default="",
|
|
1886
|
+
help="item_ids já entregues (csv) — dedup")
|
|
1887
|
+
p_exp.add_argument("--budget", metavar="TOKENS", type=int,
|
|
1888
|
+
default=4000)
|
|
1889
|
+
_add_query_args(p_exp, filters=False)
|
|
1890
|
+
p_exp.set_defaults(func=run_context_expand)
|
|
1891
|
+
|
|
1892
|
+
p_mcp = sub.add_parser(
|
|
1893
|
+
"mcp", help="servidor MCP stdio — JSON-RPC ndjson (lf-0042)")
|
|
1894
|
+
p_mcp.add_argument("cmd", nargs="?", default="serve",
|
|
1895
|
+
choices=("serve", "doctor", "manifest"))
|
|
1896
|
+
p_mcp.add_argument("--host", default="generic",
|
|
1897
|
+
choices=("claude-code", "codex-cli", "generic"))
|
|
1898
|
+
p_mcp.add_argument("--json", action="store_true")
|
|
1899
|
+
p_mcp.set_defaults(func=run_mcp)
|
|
1900
|
+
|
|
1901
|
+
p_symbol = sub.add_parser("symbol", help="detalhe de um símbolo + ocorrências")
|
|
1902
|
+
p_symbol.add_argument("ident", metavar="SYMBOL", help="id numérico ou qualified_name")
|
|
1903
|
+
_add_query_args(p_symbol, filters=False)
|
|
1904
|
+
p_symbol.set_defaults(func=run_symbol)
|
|
1905
|
+
|
|
1906
|
+
p_refs = sub.add_parser("references", help="relações que tocam um símbolo")
|
|
1907
|
+
p_refs.add_argument("ident", metavar="SYMBOL", help="id numérico ou qualified_name")
|
|
1908
|
+
p_refs.add_argument("--relation-type", metavar="TYPE", help="filtra por EdgeKind")
|
|
1909
|
+
_add_query_args(p_refs, filters=False)
|
|
1910
|
+
p_refs.set_defaults(func=run_references)
|
|
1911
|
+
|
|
1912
|
+
p_graph = sub.add_parser("graph", help="dump de nós+arestas do snapshot (JSON)")
|
|
1913
|
+
p_graph.add_argument("--relation-type", metavar="TYPE", help="filtra arestas")
|
|
1914
|
+
_add_query_args(p_graph, p_path=True)
|
|
1915
|
+
p_graph.set_defaults(func=run_graph)
|
|
1916
|
+
|
|
1917
|
+
p_defs = sub.add_parser(
|
|
1918
|
+
"definitions", help="sites de definição de um símbolo (path:span)"
|
|
1919
|
+
)
|
|
1920
|
+
p_defs.add_argument("ident", metavar="SYMBOL",
|
|
1921
|
+
help="id numérico ou qualified_name")
|
|
1922
|
+
_add_query_args(p_defs, filters=False)
|
|
1923
|
+
p_defs.set_defaults(func=run_definitions)
|
|
1924
|
+
|
|
1925
|
+
p_deps = sub.add_parser(
|
|
1926
|
+
"dependencies", help="arestas saíntes de um símbolo (BFS limitado)"
|
|
1927
|
+
)
|
|
1928
|
+
p_deps.add_argument("ident", metavar="SYMBOL",
|
|
1929
|
+
help="id numérico ou qualified_name")
|
|
1930
|
+
p_deps.add_argument(
|
|
1931
|
+
"--depth", metavar="N", type=int, default=1,
|
|
1932
|
+
help=f"profundidade de traversal (default 1, teto {MAX_DEPTH})",
|
|
1933
|
+
)
|
|
1934
|
+
_add_query_args(p_deps, filters=False)
|
|
1935
|
+
p_deps.set_defaults(func=run_dependencies)
|
|
1936
|
+
|
|
1937
|
+
p_impact = sub.add_parser(
|
|
1938
|
+
"impact", help="análise de impacto entre revisões (Cycle 5)")
|
|
1939
|
+
p_impact.add_argument(
|
|
1940
|
+
"cmd", nargs="?", default="report",
|
|
1941
|
+
choices=("diff", "worktree", "symbol", "tests", "contract",
|
|
1942
|
+
"report"))
|
|
1943
|
+
p_impact.add_argument("ident", nargs="?", metavar="SYMBOL",
|
|
1944
|
+
help="qualified_name para symbol/contract")
|
|
1945
|
+
p_impact.add_argument("--base", metavar="REV", default="",
|
|
1946
|
+
help="revisão base (HEAD~1, sha, branch)")
|
|
1947
|
+
p_impact.add_argument("--target", metavar="REV", default="",
|
|
1948
|
+
help="revisão alvo (vazio = worktree)")
|
|
1949
|
+
p_impact.add_argument("--depth", metavar="N", type=int, default=4)
|
|
1950
|
+
p_impact.add_argument("--max-nodes", metavar="N", type=int,
|
|
1951
|
+
default=500)
|
|
1952
|
+
p_impact.add_argument("--max-paths", metavar="N", type=int,
|
|
1953
|
+
default=50)
|
|
1954
|
+
p_impact.add_argument("--known-only", action="store_true")
|
|
1955
|
+
p_impact.add_argument("--format",
|
|
1956
|
+
choices=("json", "markdown", "human"),
|
|
1957
|
+
default="human")
|
|
1958
|
+
_add_query_args(p_impact, filters=False)
|
|
1959
|
+
p_impact.set_defaults(func=run_impact)
|
|
1960
|
+
|
|
1961
|
+
p_api = sub.add_parser(
|
|
1962
|
+
"api", help="API HTTP local para o Explorer (loopback, read-only)")
|
|
1963
|
+
p_api.add_argument("--host", metavar="HOST", default="127.0.0.1",
|
|
1964
|
+
help="bind (default 127.0.0.1 — público proibido)")
|
|
1965
|
+
p_api.add_argument("--port", metavar="N", type=int, default=8741)
|
|
1966
|
+
p_api.add_argument("--verbose", action="store_true")
|
|
1967
|
+
p_api.set_defaults(func=run_api)
|
|
1968
|
+
|
|
1969
|
+
p_expl = sub.add_parser(
|
|
1970
|
+
"explorer",
|
|
1971
|
+
help="inicia o Explorer visual (API + assets compilados)")
|
|
1972
|
+
p_expl.add_argument("--host", metavar="HOST", default="127.0.0.1")
|
|
1973
|
+
p_expl.add_argument("--port", metavar="N", type=int, default=8741)
|
|
1974
|
+
p_expl.add_argument("--open", action="store_true",
|
|
1975
|
+
help="abre o navegador (somente quando pedido)")
|
|
1976
|
+
p_expl.add_argument("--verbose", action="store_true")
|
|
1977
|
+
p_expl.set_defaults(func=run_explorer)
|
|
1978
|
+
|
|
1979
|
+
p_fed = sub.add_parser(
|
|
1980
|
+
"federation",
|
|
1981
|
+
help="agrupa repos indexados numa view federada "
|
|
1982
|
+
"(lf-0118; mutação via CLI)")
|
|
1983
|
+
p_fed.add_argument(
|
|
1984
|
+
"fed_cmd", metavar="CMD",
|
|
1985
|
+
choices=("create", "add", "pin", "view"),
|
|
1986
|
+
help="create <name> | add <ws> <repo> | pin <ws> <repo> <snap|latest>"
|
|
1987
|
+
" | view <ws>")
|
|
1988
|
+
p_fed.add_argument("name", nargs="?", metavar="NAME",
|
|
1989
|
+
help="nome a criar (create) ou ref <ws> (demais)")
|
|
1990
|
+
p_fed.add_argument("repo", nargs="?", metavar="REPO",
|
|
1991
|
+
help="repo id | identity_key | root_handle | path "
|
|
1992
|
+
"(add/pin)")
|
|
1993
|
+
p_fed.add_argument("snapshot", nargs="?", metavar="SNAPSHOT",
|
|
1994
|
+
help="snapshot id a pinar, ou 'latest' para desfazer "
|
|
1995
|
+
"o pin (pin)")
|
|
1996
|
+
p_fed.add_argument("--pin", metavar="REPO:SNAP,…", default="",
|
|
1997
|
+
help="overrides de pin na view (view)")
|
|
1998
|
+
p_fed.add_argument("--json", action="store_true", help="saída em JSON")
|
|
1999
|
+
p_fed.set_defaults(func=run_federation)
|
|
2000
|
+
|
|
2001
|
+
p_dom = sub.add_parser(
|
|
2002
|
+
"domains",
|
|
2003
|
+
help="inteligência de domínio read-only (lf-0123): adapters, "
|
|
2004
|
+
"entidades e linhagem — dados existem só quando o índice "
|
|
2005
|
+
"rodou com `index --domains`")
|
|
2006
|
+
p_dom.add_argument(
|
|
2007
|
+
"dom_cmd", metavar="CMD",
|
|
2008
|
+
choices=("list", "entities", "entity", "lineage"),
|
|
2009
|
+
help="list | entities | entity <key> | lineage <table>")
|
|
2010
|
+
p_dom.add_argument(
|
|
2011
|
+
"ident", nargs="?", metavar="KEY",
|
|
2012
|
+
help="identity_key 'dom:…' | id numérico | qualified_name | "
|
|
2013
|
+
"namespace.qualified_name (entity/lineage)")
|
|
2014
|
+
p_dom.add_argument(
|
|
2015
|
+
"--repo", metavar="REF",
|
|
2016
|
+
help="repo id | identity_key | root_handle | path "
|
|
2017
|
+
"(como `federation add`)")
|
|
2018
|
+
p_dom.add_argument("--domain", metavar="D",
|
|
2019
|
+
help="filtra por domain_id (entities)")
|
|
2020
|
+
p_dom.add_argument("--kind", metavar="K",
|
|
2021
|
+
help="filtra por DomainEntityKind (entities)")
|
|
2022
|
+
p_dom.add_argument("--q", metavar="SUBSTR",
|
|
2023
|
+
help="substring de qualified_name/name (entities)")
|
|
2024
|
+
p_dom.add_argument(
|
|
2025
|
+
"--direction", choices=("up", "down", "both"), default="both",
|
|
2026
|
+
help="up=produtores, down=consumidores (lineage; default both)")
|
|
2027
|
+
p_dom.add_argument(
|
|
2028
|
+
"--depth", metavar="N", type=int, default=2,
|
|
2029
|
+
help=f"hops do traversal (lineage; default 2, teto {MAX_DEPTH})")
|
|
2030
|
+
p_dom.add_argument(
|
|
2031
|
+
"--limit", metavar="N", type=int,
|
|
2032
|
+
help=f"máx. de resultados (default {DEFAULT_LIMIT}, teto {MAX_LIMIT})")
|
|
2033
|
+
p_dom.add_argument("--offset", metavar="N", type=int, default=0,
|
|
2034
|
+
help="deslocamento de paginação (entities)")
|
|
2035
|
+
p_dom.add_argument("--snapshot", metavar="ID", type=int,
|
|
2036
|
+
help="snapshot alvo (default: READY mais recente)")
|
|
2037
|
+
p_dom.add_argument("--root", metavar="PATH",
|
|
2038
|
+
help="raiz do workspace (default: cwd)")
|
|
2039
|
+
p_dom.add_argument("--json", action="store_true", help="saída em JSON")
|
|
2040
|
+
p_dom.set_defaults(func=run_domains)
|
|
2041
|
+
|
|
2042
|
+
return parser
|
|
2043
|
+
|
|
2044
|
+
|
|
2045
|
+
def _add_query_args(
|
|
2046
|
+
p: argparse.ArgumentParser, *, filters: bool = True, p_path: bool = False
|
|
2047
|
+
) -> None:
|
|
2048
|
+
"""Args comuns dos comandos read-only de consulta."""
|
|
2049
|
+
p.add_argument("--root", metavar="PATH", help="raiz do workspace (default: cwd)")
|
|
2050
|
+
p.add_argument("--snapshot", metavar="ID", type=int,
|
|
2051
|
+
help="snapshot alvo (default: READY mais recente)")
|
|
2052
|
+
p.add_argument("--limit", metavar="N", type=int,
|
|
2053
|
+
help=f"máx. de resultados (default {DEFAULT_LIMIT}, teto {MAX_LIMIT})")
|
|
2054
|
+
p.add_argument("--offset", metavar="N", type=int, default=0,
|
|
2055
|
+
help="deslocamento de paginação")
|
|
2056
|
+
if filters:
|
|
2057
|
+
p.add_argument("--language", metavar="LANG", help="filtra por linguagem")
|
|
2058
|
+
p.add_argument("--kind", metavar="KIND", help="filtra por NodeKind")
|
|
2059
|
+
if p_path:
|
|
2060
|
+
p.add_argument("--path", dest="path_filter", metavar="SUBSTR",
|
|
2061
|
+
help="substring de relative_path")
|
|
2062
|
+
else:
|
|
2063
|
+
p.set_defaults(language=None, kind=None, path_filter=None)
|
|
2064
|
+
p.add_argument("--json", action="store_true", help="saída em JSON")
|
|
2065
|
+
|
|
2066
|
+
|
|
2067
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
2068
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
2069
|
+
# Bare `code-atlas install` on a TTY → guided wizard (same contract as
|
|
2070
|
+
# `sparkforge-aws install` / `productforge install`); flags and pipes
|
|
2071
|
+
# keep the argparse path unchanged.
|
|
2072
|
+
if argv == ["install"] and sys.stdin.isatty() and sys.stdout.isatty():
|
|
2073
|
+
try:
|
|
2074
|
+
from code_atlas.install.cli import run_guided_install
|
|
2075
|
+
from code_atlas.ui.kit import NonInteractive
|
|
2076
|
+
except ImportError:
|
|
2077
|
+
pass
|
|
2078
|
+
else:
|
|
2079
|
+
try:
|
|
2080
|
+
return run_guided_install()
|
|
2081
|
+
except NonInteractive:
|
|
2082
|
+
pass
|
|
2083
|
+
args = build_parser().parse_args(argv)
|
|
2084
|
+
return int(args.func(args))
|