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.
Files changed (181) hide show
  1. code_atlas/__init__.py +6 -0
  2. code_atlas/__main__.py +6 -0
  3. code_atlas/api/__init__.py +5 -0
  4. code_atlas/api/handlers.py +1514 -0
  5. code_atlas/api/server.py +394 -0
  6. code_atlas/cli.py +2084 -0
  7. code_atlas/config.py +149 -0
  8. code_atlas/context/__init__.py +66 -0
  9. code_atlas/context/contracts.py +277 -0
  10. code_atlas/context/domain.py +760 -0
  11. code_atlas/context/engine.py +442 -0
  12. code_atlas/context/expand.py +133 -0
  13. code_atlas/context/renderers.py +71 -0
  14. code_atlas/context/tokenizer.py +37 -0
  15. code_atlas/contracts/__init__.py +38 -0
  16. code_atlas/contracts/enums.py +67 -0
  17. code_atlas/contracts/models.py +175 -0
  18. code_atlas/contracts/ports.py +70 -0
  19. code_atlas/discovery/__init__.py +25 -0
  20. code_atlas/discovery/gitignore.py +119 -0
  21. code_atlas/discovery/identity.py +52 -0
  22. code_atlas/discovery/models.py +204 -0
  23. code_atlas/discovery/scanner.py +495 -0
  24. code_atlas/domains/__init__.py +107 -0
  25. code_atlas/domains/api/__init__.py +28 -0
  26. code_atlas/domains/api/adapter.py +405 -0
  27. code_atlas/domains/api/common.py +308 -0
  28. code_atlas/domains/api/consumers.py +224 -0
  29. code_atlas/domains/api/emit.py +231 -0
  30. code_atlas/domains/api/openapi.py +398 -0
  31. code_atlas/domains/api/routes.py +582 -0
  32. code_atlas/domains/api/yaml_subset.py +683 -0
  33. code_atlas/domains/aws/__init__.py +28 -0
  34. code_atlas/domains/aws/adapter.py +382 -0
  35. code_atlas/domains/aws/common.py +261 -0
  36. code_atlas/domains/aws/configs.py +1164 -0
  37. code_atlas/domains/aws/emit.py +411 -0
  38. code_atlas/domains/aws/pyfiles.py +2220 -0
  39. code_atlas/domains/aws/sqlbridge.py +448 -0
  40. code_atlas/domains/cicd/__init__.py +50 -0
  41. code_atlas/domains/cicd/adapter.py +762 -0
  42. code_atlas/domains/cicd/classify.py +136 -0
  43. code_atlas/domains/cicd/workflow.py +504 -0
  44. code_atlas/domains/cicd/yamlmini.py +946 -0
  45. code_atlas/domains/cli_surface.py +739 -0
  46. code_atlas/domains/contracts.py +274 -0
  47. code_atlas/domains/crossresolve.py +1208 -0
  48. code_atlas/domains/evidence.py +57 -0
  49. code_atlas/domains/identity.py +149 -0
  50. code_atlas/domains/model.py +202 -0
  51. code_atlas/domains/registry.py +250 -0
  52. code_atlas/domains/resolution.py +65 -0
  53. code_atlas/domains/spark/__init__.py +31 -0
  54. code_atlas/domains/spark/adapter.py +146 -0
  55. code_atlas/domains/spark/emit.py +344 -0
  56. code_atlas/domains/spark/extract.py +108 -0
  57. code_atlas/domains/spark/sqllit.py +192 -0
  58. code_atlas/domains/spark/walk.py +1943 -0
  59. code_atlas/domains/sql/__init__.py +30 -0
  60. code_atlas/domains/sql/adapter.py +382 -0
  61. code_atlas/domains/sql/extract.py +1271 -0
  62. code_atlas/domains/sql/nodes.py +264 -0
  63. code_atlas/domains/sql/parser.py +1510 -0
  64. code_atlas/domains/sql/tokens.py +318 -0
  65. code_atlas/domains/summarize.py +126 -0
  66. code_atlas/domains/terraform/__init__.py +20 -0
  67. code_atlas/domains/terraform/adapter.py +127 -0
  68. code_atlas/domains/terraform/catalog.py +43 -0
  69. code_atlas/domains/terraform/extract.py +1277 -0
  70. code_atlas/domains/terraform/hcl.py +538 -0
  71. code_atlas/domains/terraform/refs.py +181 -0
  72. code_atlas/domains/terraform/tfjson.py +163 -0
  73. code_atlas/domains/terraform/tfvars.py +98 -0
  74. code_atlas/domains/validation.py +120 -0
  75. code_atlas/env_override.py +27 -0
  76. code_atlas/eval/__init__.py +26 -0
  77. code_atlas/eval/domain_eval.py +681 -0
  78. code_atlas/eval/ground_truth.py +688 -0
  79. code_atlas/eval/impact_truth.py +758 -0
  80. code_atlas/eval/metrics.py +178 -0
  81. code_atlas/eval/pack_value.py +610 -0
  82. code_atlas/federation/__init__.py +72 -0
  83. code_atlas/federation/federation.py +676 -0
  84. code_atlas/federation/model.py +355 -0
  85. code_atlas/federation/store.py +194 -0
  86. code_atlas/gitbin.py +48 -0
  87. code_atlas/graph/__init__.py +20 -0
  88. code_atlas/graph/builder.py +378 -0
  89. code_atlas/graph/identity.py +54 -0
  90. code_atlas/graph/queries.py +47 -0
  91. code_atlas/graph/validation.py +92 -0
  92. code_atlas/impact/__init__.py +37 -0
  93. code_atlas/impact/blast_radius.py +171 -0
  94. code_atlas/impact/contract_analyzer.py +104 -0
  95. code_atlas/impact/contracts.py +315 -0
  96. code_atlas/impact/diff_reader.py +142 -0
  97. code_atlas/impact/domain_traversal.py +768 -0
  98. code_atlas/impact/engine.py +392 -0
  99. code_atlas/impact/packs.py +128 -0
  100. code_atlas/impact/report.py +136 -0
  101. code_atlas/impact/revision_binding.py +220 -0
  102. code_atlas/impact/risk_classifier.py +83 -0
  103. code_atlas/impact/snapshot_resolver.py +136 -0
  104. code_atlas/impact/symbol_diff.py +210 -0
  105. code_atlas/impact/test_selector.py +166 -0
  106. code_atlas/indexing/__init__.py +5 -0
  107. code_atlas/indexing/cache.py +396 -0
  108. code_atlas/indexing/change_detector.py +278 -0
  109. code_atlas/indexing/coordinator.py +1032 -0
  110. code_atlas/indexing/dependency_planner.py +268 -0
  111. code_atlas/indexing/persist.py +436 -0
  112. code_atlas/indexing/reuse.py +85 -0
  113. code_atlas/install/__init__.py +7 -0
  114. code_atlas/install/cli.py +168 -0
  115. code_atlas/install/lifecycle.py +286 -0
  116. code_atlas/mcp.py +674 -0
  117. code_atlas/observability/__init__.py +19 -0
  118. code_atlas/observability/logging.py +199 -0
  119. code_atlas/parsers/__init__.py +43 -0
  120. code_atlas/parsers/contracts.py +209 -0
  121. code_atlas/parsers/engine.py +125 -0
  122. code_atlas/parsers/languages/__init__.py +1 -0
  123. code_atlas/parsers/languages/python.py +391 -0
  124. code_atlas/parsers/languages/tsx.py +28 -0
  125. code_atlas/parsers/languages/typescript.py +909 -0
  126. code_atlas/parsers/registry.py +51 -0
  127. code_atlas/parsers/tree_sitter_runtime.py +118 -0
  128. code_atlas/resolvers/__init__.py +33 -0
  129. code_atlas/resolvers/bootstrap.py +246 -0
  130. code_atlas/resolvers/capabilities.py +33 -0
  131. code_atlas/resolvers/contracts.py +133 -0
  132. code_atlas/resolvers/coordinator.py +96 -0
  133. code_atlas/resolvers/enrichment.py +318 -0
  134. code_atlas/resolvers/lsp/__init__.py +26 -0
  135. code_atlas/resolvers/lsp/adapter.py +256 -0
  136. code_atlas/resolvers/lsp/lifecycle.py +251 -0
  137. code_atlas/resolvers/lsp/spawn.py +92 -0
  138. code_atlas/resolvers/lsp/transport.py +240 -0
  139. code_atlas/resolvers/registry.py +33 -0
  140. code_atlas/resolvers/scip/__init__.py +49 -0
  141. code_atlas/resolvers/scip/decoder.py +216 -0
  142. code_atlas/resolvers/scip/importer.py +248 -0
  143. code_atlas/resolvers/scip/loader.py +90 -0
  144. code_atlas/resolvers/scip/mapper.py +271 -0
  145. code_atlas/resolvers/scip/provenance.py +211 -0
  146. code_atlas/resolvers/scip/resolver.py +289 -0
  147. code_atlas/resolvers/scip/validation.py +101 -0
  148. code_atlas/resolvers/syntactic/__init__.py +8 -0
  149. code_atlas/resolvers/syntactic/resolver.py +254 -0
  150. code_atlas/search/__init__.py +32 -0
  151. code_atlas/search/contracts.py +176 -0
  152. code_atlas/search/domain.py +561 -0
  153. code_atlas/search/engine.py +530 -0
  154. code_atlas/search/federated.py +276 -0
  155. code_atlas/search/graph.py +282 -0
  156. code_atlas/search/hybrid.py +151 -0
  157. code_atlas/search/intent.py +48 -0
  158. code_atlas/search/lexical.py +176 -0
  159. code_atlas/search/refs.py +359 -0
  160. code_atlas/search/structural.py +505 -0
  161. code_atlas/search/tokenizer.py +43 -0
  162. code_atlas/search/vector/__init__.py +35 -0
  163. code_atlas/search/vector/contracts.py +76 -0
  164. code_atlas/search/vector/embeddings.py +96 -0
  165. code_atlas/search/vector/index.py +169 -0
  166. code_atlas/search/vector/provider.py +50 -0
  167. code_atlas/storage/__init__.py +14 -0
  168. code_atlas/storage/db.py +47 -0
  169. code_atlas/storage/migrations.py +552 -0
  170. code_atlas/storage/store.py +1045 -0
  171. code_atlas/ui/__init__.py +29 -0
  172. code_atlas/ui/i18n.py +143 -0
  173. code_atlas/ui/kit.py +689 -0
  174. code_atlas/ui/wizard.py +317 -0
  175. code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
  176. code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
  177. code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
  178. code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
  179. code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
  180. code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
  181. code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,610 @@
1
+ """lf-0136 (Cycle 8 Phase 10) — valor medido do context pack (proxy).
2
+
3
+ **Não é avaliação de agente** — não existe LLM/agent loop no
4
+ repositório, então a qualidade-de-resposta é NOT RUN (declarado, não
5
+ simulado). O que se mede é um **proxy estrutural honesto**:
6
+
7
+ - ``no_atlas``: simulação determinística de "engenheiro com grep" —
8
+ busca literal iterativa sobre a árvore da fixture (1 query por
9
+ rodada, tokens = conteúdo dos arquivos abertos);
10
+ - ``pack_only``: pack real de ``DomainContextEngine.build_domain``
11
+ (budget default 4000) — ``tokens_payload`` estimado pelo próprio
12
+ engine vs fatos contidos;
13
+ - ``atlas_plus_search``: duas variantes reais — ``impact symbol``
14
+ (especialista, 1 query) e ``domains entities``+``lineage`` (montagem
15
+ por busca estrutural — as queries necessárias para juntar a mesma
16
+ informação sem o pack).
17
+
18
+ Fatos = os mesmos ``ImpactCandidate.qualified_name`` verificados em
19
+ lf-0135 — o "o que precisa constar na resposta" rotulado à mão. Um
20
+ fato é "answerable" por um braço quando uma de suas formas aparece no
21
+ conteúdo que o braço entrega (match com fronteira de token — `orders`
22
+ não casa dentro de `enriched_orders`/`staging.orders`).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import re
29
+ import sqlite3
30
+ from collections.abc import Mapping, Sequence
31
+ from dataclasses import dataclass
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from code_atlas.context.contracts import DomainPackRequest
36
+ from code_atlas.context.domain import DomainContextEngine
37
+ from code_atlas.context.tokenizer import HeuristicTokenizer
38
+ from code_atlas.eval.impact_truth import (
39
+ index_fixture,
40
+ run_cli,
41
+ run_impact,
42
+ )
43
+ from code_atlas.impact.domain_traversal import resolve_entity_refs
44
+
45
+ SCENARIO_SCHEMA = 1
46
+ RESULTS_SCHEMA = 1
47
+
48
+ PACK_BUDGET = 4000 # default do DomainPackRequest
49
+ NO_ATLAS_MAX_ROUNDS = 4 # o agente real desiste; cap honesto
50
+ _LINEAGE_DEPTH = 8 # teto real do `domains lineage` (MAX_DEPTH)
51
+
52
+ _DOMAIN_PREFIXES = ("sql:", "pyspark:", "aws-data:", "terraform:",
53
+ "cicd:", "api:")
54
+
55
+ def _edge_pattern(token: str) -> re.Pattern[str]:
56
+ """Fronteira para DETECÇÃO DE FATO: o nome não pode estar colado
57
+ em identificador maior (``orders`` não casa em ``enriched_orders``
58
+ nem em ``staging.orders`` — a forma qualificada cobre esse caso).
59
+ ``/`` não é bloqueada no lookbehind: ``x/orders`` termina com o
60
+ nome de verdade."""
61
+ return re.compile(
62
+ r"(?<![\w.\-])" + re.escape(token) + r"(?![\w])")
63
+
64
+
65
+ def _display_of_row(row: Mapping[str, Any]) -> str:
66
+ """``ns.qn[@env]`` — mesma forma de ``_display_of`` do traversal."""
67
+ head = ".".join(
68
+ p for p in (str(row["catalog"] or ""),
69
+ str(row["namespace"] or "")) if p)
70
+ qn = str(row["qualified_name"])
71
+ disp = f"{head}.{qn}" if head else qn
72
+ env = str(row["environment"] or "")
73
+ return f"{disp}@{env}" if env else disp
74
+
75
+
76
+ def resolve_fact_forms(
77
+ conn: sqlite3.Connection, repository_id: int, fact: str,
78
+ ) -> tuple[str, ...]:
79
+ """Formas reais do fato — resolvidas em ``domain_entities`` quando
80
+ o fato é de domínio (o ``qualified_name`` de verdade, que é o que
81
+ aparece no código-fonte); fallback heurístico para símbolos."""
82
+ domain = ""
83
+ target = fact
84
+ for prefix in _DOMAIN_PREFIXES:
85
+ if fact.startswith(prefix):
86
+ domain = prefix[:-1]
87
+ target = fact[len(prefix):]
88
+ break
89
+ if not domain:
90
+ return fact_forms(fact)
91
+ for row in conn.execute(
92
+ "SELECT qualified_name, namespace, catalog, environment"
93
+ " FROM domain_entities"
94
+ " WHERE repository_id = ? AND domain = ?",
95
+ (repository_id, domain)).fetchall():
96
+ disp = _display_of_row(row)
97
+ if target == disp or target == str(row["qualified_name"]):
98
+ return _entity_forms(fact, target, disp,
99
+ str(row["qualified_name"]))
100
+ return fact_forms(fact)
101
+
102
+
103
+ def _entity_forms(
104
+ fact: str, target: str, disp: str, qn: str,
105
+ ) -> tuple[str, ...]:
106
+ """Formas de match derivadas da entidade real — ``file#N`` rende
107
+ o caminho (o ``#N`` nunca existe como texto)."""
108
+ forms = {fact, target, disp, disp.split("@", 1)[0], qn,
109
+ qn.split("@", 1)[0]}
110
+ for base in (qn, disp.split("@", 1)[0]):
111
+ if "/" in base:
112
+ forms.add(base.rstrip("/").rsplit("/", 1)[-1])
113
+ if "." in base:
114
+ forms.add(base.rsplit(".", 1)[-1])
115
+ if "#" in base:
116
+ path, _, frag = base.partition("#")
117
+ if path:
118
+ forms.add(path)
119
+ forms.add(path.rsplit("/", 1)[-1])
120
+ if frag:
121
+ forms.add(frag.rsplit("/", 1)[-1])
122
+ return tuple(sorted(f for f in forms if f and not f.isdigit()))
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class PackTask:
127
+ """Tarefa rotulada — pergunta real + conjunto de fatos esperados."""
128
+
129
+ task_id: str
130
+ question: str
131
+ fixture: str
132
+ domains: tuple[str, ...]
133
+ cross_domain: bool
134
+ seed: str
135
+ seed_domain: str
136
+ grep_seeds: tuple[str, ...]
137
+ facts: tuple[str, ...]
138
+ notes: str = ""
139
+
140
+
141
+ def load_tasks(path: Path) -> list[PackTask]:
142
+ raw = json.loads(path.read_text(encoding="utf-8"))
143
+ if int(raw.get("schema", 0)) != SCENARIO_SCHEMA:
144
+ raise ValueError(f"pack-value tasks schema != {SCENARIO_SCHEMA}")
145
+ out: list[PackTask] = []
146
+ for t in raw["tasks"]:
147
+ out.append(PackTask(
148
+ task_id=str(t["id"]),
149
+ question=str(t["question"]),
150
+ fixture=str(t["fixture"]),
151
+ domains=tuple(str(d) for d in t["domains"]),
152
+ cross_domain=bool(t.get("cross_domain", False)),
153
+ seed=str(t["seed"]),
154
+ seed_domain=str(t.get("seed_domain", "")),
155
+ grep_seeds=tuple(str(g) for g in t.get("grep_seeds", ())),
156
+ facts=tuple(str(f) for f in t["facts"]),
157
+ notes=str(t.get("notes", "")),
158
+ ))
159
+ return out
160
+
161
+
162
+ def validate_task(repo_root: Path, task: PackTask) -> list[str]:
163
+ errs: list[str] = []
164
+ if not (repo_root / task.fixture).is_dir():
165
+ errs.append(f"fixture ausente: {task.fixture}")
166
+ if not task.seed or not task.facts or not task.grep_seeds:
167
+ errs.append("seed/facts/grep_seeds vazios")
168
+ if len(set(task.facts)) != len(task.facts):
169
+ errs.append("facts duplicados")
170
+ return errs
171
+
172
+
173
+ # ---------------------------------------------------------------------
174
+ # match de fatos — formas + fronteira de token
175
+ # ---------------------------------------------------------------------
176
+
177
+
178
+ def fact_forms(name: str) -> tuple[str, ...]:
179
+ """Formas casáveis de um fato (``sql:staging.orders`` →
180
+ {sql:staging.orders, staging.orders, orders}; ``a/b.py#f`` →
181
+ {a/b.py#f, b.py, f})."""
182
+ s = name
183
+ for prefix in _DOMAIN_PREFIXES:
184
+ if s.startswith(prefix):
185
+ s = s[len(prefix):]
186
+ break
187
+ forms: set[str] = {name, s}
188
+ core = s.split("@", 1)[0] # '@dev' env / '@L19' linha → cauda
189
+ forms.add(core)
190
+ if "#" in core:
191
+ path, _, frag = core.partition("#")
192
+ if path:
193
+ forms.add(path)
194
+ forms.add(path.rsplit("/", 1)[-1])
195
+ if frag:
196
+ forms.add(frag.rsplit("/", 1)[-1])
197
+ if "/" in core:
198
+ forms.add(core.rstrip("/").rsplit("/", 1)[-1])
199
+ elif "." in core:
200
+ forms.add(core.rsplit(".", 1)[-1])
201
+ return tuple(sorted(f for f in forms if f and not f.isdigit()))
202
+
203
+
204
+ def contains_token(text: str, token: str) -> bool:
205
+ """Match com fronteira — ``token`` não pode estar grudado em
206
+ identificador maior (detecção de fato, não grep)."""
207
+ return _edge_pattern(token).search(text) is not None
208
+
209
+
210
+ def facts_in(
211
+ texts: Sequence[str], forms_map: Mapping[str, Sequence[str]],
212
+ ) -> list[str]:
213
+ """Fatos casados no conteúdo entregue por um braço."""
214
+ blob = "\n".join(texts)
215
+ return [f for f, forms in forms_map.items()
216
+ if any(contains_token(blob, form) for form in forms)]
217
+
218
+
219
+ # ---------------------------------------------------------------------
220
+ # braço A — no_atlas: simulação grep+read iterativa (determinística)
221
+ # ---------------------------------------------------------------------
222
+
223
+
224
+ def _iter_fixture_files(fixture_dir: Path) -> dict[str, str]:
225
+ """``relative_path → conteúdo`` de todo arquivo de texto da
226
+ fixture (inclui ``.github/``; binários ilegíveis são pulados)."""
227
+ corpus: dict[str, str] = {}
228
+ for p in sorted(fixture_dir.rglob("*")):
229
+ if not p.is_file():
230
+ continue
231
+ try:
232
+ corpus[str(p.relative_to(fixture_dir))] = \
233
+ p.read_text(encoding="utf-8")
234
+ except (UnicodeDecodeError, ValueError):
235
+ continue
236
+ return corpus
237
+
238
+
239
+ def no_atlas(
240
+ fixture_dir: Path, grep_seeds: Sequence[str],
241
+ forms_map: Mapping[str, Sequence[str]],
242
+ *, max_rounds: int = NO_ATLAS_MAX_ROUNDS,
243
+ counter: HeuristicTokenizer | None = None,
244
+ ) -> dict[str, Any]:
245
+ """Braço "sem atlas": grep literal → lê arquivos que casam → nomes
246
+ encontrados viram próximas buscas. 1 query por rodada (tokens
247
+ empacotados num único ``grep -e a -e b`` — a busca é substring
248
+ crua, como o grep real); tokens = conteúdo dos arquivos abertos."""
249
+ tok = counter or HeuristicTokenizer()
250
+ corpus = _iter_fixture_files(fixture_dir)
251
+ read: dict[str, str] = {}
252
+ found: set[str] = set()
253
+ used: set[str] = set()
254
+ pending = sorted(set(grep_seeds))
255
+ rounds: list[dict[str, Any]] = []
256
+ queries = 0
257
+ for _ in range(max_rounds + 1):
258
+ if not pending:
259
+ break
260
+ queries += 1
261
+ used.update(pending)
262
+ # grep real é substring crua — sem fronteira de token.
263
+ hits = sorted(
264
+ path for path, text in corpus.items()
265
+ if path not in read and any(
266
+ q in text or q in path for q in pending))
267
+ for h in hits:
268
+ read[h] = corpus[h]
269
+ texts = [*read.values(), *read.keys()]
270
+ now = set(facts_in(texts, forms_map))
271
+ new = now - found
272
+ found |= new
273
+ rounds.append({
274
+ "query_tokens": list(pending),
275
+ "files_matched": len(hits),
276
+ "facts_found_total": len(found),
277
+ })
278
+ pending = sorted(
279
+ {tok for f in new
280
+ for tok in forms_map.get(f, ()) if len(tok) >= 4}
281
+ - used)
282
+ tokens = sum(tok.count(t) for t in read.values())
283
+ missing = [f for f in forms_map if f not in found]
284
+ return {
285
+ "queries": queries,
286
+ "files_read": len(read),
287
+ "files_total": len(corpus),
288
+ "tokens": tokens,
289
+ "facts_found": sorted(found),
290
+ "facts_missing": missing,
291
+ "facts_answerable": len(found),
292
+ "facts_expected": len(forms_map),
293
+ "rounds": rounds,
294
+ }
295
+
296
+
297
+ # ---------------------------------------------------------------------
298
+ # braço B — pack-only: DomainContextEngine real
299
+ # ---------------------------------------------------------------------
300
+
301
+
302
+ def _dom_key(
303
+ conn: sqlite3.Connection, repository_id: int, task: PackTask,
304
+ ) -> str | None:
305
+ """``dom:<identity_key>`` da seed — resolvido na base real."""
306
+ rows, _how = resolve_entity_refs(conn, repository_id, task.seed)
307
+ for row in rows:
308
+ if str(row["domain"]) == task.seed_domain:
309
+ return str(row["identity_key"])
310
+ return str(rows[0]["identity_key"]) if rows else None
311
+
312
+
313
+ def pack_arm(
314
+ fixture: Path, task: PackTask, conn: sqlite3.Connection,
315
+ repository_id: int, forms_map: Mapping[str, Sequence[str]],
316
+ *, budget: int = PACK_BUDGET,
317
+ ) -> dict[str, Any]:
318
+ """Braço "pack-only": um ``build_domain`` real — payload estimado
319
+ pelo próprio engine vs fatos contidos no pack."""
320
+ dom_key = _dom_key(conn, repository_id, task)
321
+ req = DomainPackRequest(
322
+ seed=dom_key or task.seed,
323
+ repository_root=str(fixture),
324
+ budget_tokens=budget)
325
+ pack = DomainContextEngine(conn).build_domain(req)
326
+ section = pack.domain
327
+ text = json.dumps(
328
+ _pack_dict(pack), sort_keys=True, default=str)
329
+ found = facts_in([text], forms_map)
330
+ return {
331
+ "queries": 1,
332
+ "tokens": int(pack.manifest.tokens_payload),
333
+ "tokens_estimated": bool(pack.manifest.tokens_estimated),
334
+ "budget_tokens": budget,
335
+ "status": (str(section.status) if section is not None
336
+ else "absent"),
337
+ "neighbors_total": (
338
+ int(section.neighbors_total) if section is not None else 0),
339
+ "neighbors_omitted": (
340
+ int(section.neighbors_omitted)
341
+ if section is not None else 0),
342
+ "items_selected": int(pack.manifest.selected),
343
+ "items_omitted": int(pack.manifest.omitted),
344
+ "truncated": bool(pack.manifest.truncated),
345
+ "seed_identity_key": dom_key,
346
+ "facts_found": found,
347
+ "facts_missing": [f for f in task.facts if f not in found],
348
+ "facts_answerable": len(found),
349
+ "facts_expected": len(task.facts),
350
+ }
351
+
352
+
353
+ def _pack_dict(pack: Any) -> dict[str, Any]:
354
+ import dataclasses
355
+
356
+ def conv(o: Any) -> Any:
357
+ if dataclasses.is_dataclass(o) and not isinstance(o, type):
358
+ return {k: conv(v) for k, v in
359
+ dataclasses.asdict(o).items()}
360
+ if isinstance(o, (tuple, list)):
361
+ return [conv(v) for v in o]
362
+ return o
363
+ out = conv(pack)
364
+ return out if isinstance(out, dict) else {"pack": out}
365
+
366
+
367
+ # ---------------------------------------------------------------------
368
+ # braço C — atlas+search: impact (especialista) + montagem por
369
+ # domains entities/lineage (busca estrutural genérica)
370
+ # ---------------------------------------------------------------------
371
+
372
+
373
+ def _seed_query_term(seed: str) -> str:
374
+ """Termo de ``--q`` natural: basename do caminho (``x.yml#job`` →
375
+ ``x.yml``) ou último segmento (``staging.orders`` → ``orders``)."""
376
+ s = seed
377
+ for prefix in _DOMAIN_PREFIXES:
378
+ if s.startswith(prefix):
379
+ s = s[len(prefix):]
380
+ break
381
+ core = s.split("@", 1)[0].split("#", 1)[0]
382
+ if "/" in core:
383
+ cand = core.rstrip("/").rsplit("/", 1)[-1]
384
+ if cand:
385
+ return cand
386
+ if "." in core:
387
+ return core.rsplit(".", 1)[-1]
388
+ return core
389
+
390
+
391
+ def atlas_search_arm(
392
+ fixture: Path, task: PackTask, conn: sqlite3.Connection,
393
+ repository_id: int, forms_map: Mapping[str, Sequence[str]],
394
+ *, data_dir: Path, config_dir: Path,
395
+ counter: HeuristicTokenizer,
396
+ ) -> dict[str, Any]:
397
+ """Duas variantes medidas:
398
+
399
+ - ``impact``: ``impact symbol <seed> --format json`` — 1 query,
400
+ conjunto verificado completo;
401
+ - ``search``: ``domains entities --q <termo>`` + ``domains
402
+ lineage <dom_key> --direction both --depth 8`` — montagem por
403
+ busca estrutural (só percorre fluxo-de-dados — mede o gap que
404
+ ``CONTAINS``/``REFERENCES``/pontes de símbolo cobrem).
405
+ """
406
+ out: dict[str, Any] = {}
407
+
408
+ _run, payload = run_impact(
409
+ fixture, task.seed, data_dir=data_dir, config_dir=config_dir)
410
+ if payload is not None:
411
+ imp_text = json.dumps(payload, sort_keys=True, default=str)
412
+ found = facts_in([imp_text], forms_map)
413
+ out["impact"] = {
414
+ "queries": 1,
415
+ "tokens": counter.count(imp_text),
416
+ "candidates": len(payload.get("impacts") or ()),
417
+ "facts_found": found,
418
+ "facts_answerable": len(found),
419
+ }
420
+ else:
421
+ out["impact"] = {"queries": 1, "error": "sem JSON"}
422
+
423
+ queries = 1
424
+ texts: list[str] = []
425
+ run_e = run_cli(
426
+ ("domains", "entities", "--q", _seed_query_term(task.seed),
427
+ "--root", str(fixture), "--json"),
428
+ data_dir=data_dir, config_dir=config_dir)
429
+ texts.append(run_e.stdout)
430
+ dom_key = _dom_key(conn, repository_id, task)
431
+ if dom_key is not None:
432
+ queries += 1
433
+ run_l = run_cli(
434
+ ("domains", "lineage", dom_key,
435
+ "--direction", "both", "--depth", str(_LINEAGE_DEPTH),
436
+ "--root", str(fixture), "--json"),
437
+ data_dir=data_dir, config_dir=config_dir)
438
+ texts.append(run_l.stdout)
439
+ found = facts_in(texts, forms_map)
440
+ out["search"] = {
441
+ "queries": queries,
442
+ "tokens": sum(counter.count(t) for t in texts),
443
+ "facts_found": found,
444
+ "facts_answerable": len(found),
445
+ }
446
+ out["facts_expected"] = len(task.facts)
447
+ return out
448
+
449
+
450
+ # ---------------------------------------------------------------------
451
+ # avaliação
452
+ # ---------------------------------------------------------------------
453
+
454
+
455
+ def evaluate_task(
456
+ repo_root: Path, task: PackTask, conn: sqlite3.Connection,
457
+ repository_id: int, snapshot_id: int, *, data_dir: Path,
458
+ config_dir: Path,
459
+ ) -> dict[str, Any]:
460
+ """Mede os três braços de uma tarefa num repo já indexado."""
461
+ fixture = repo_root / task.fixture
462
+ counter = HeuristicTokenizer()
463
+ rep: dict[str, Any] = {
464
+ "id": task.task_id,
465
+ "question": task.question,
466
+ "fixture": task.fixture,
467
+ "seed": task.seed,
468
+ "facts_expected": len(task.facts),
469
+ "notes": task.notes,
470
+ }
471
+ # formas reais dos fatos — resolvidas em ``domain_entities`` deste
472
+ # repositório (o qualified_name de verdade é o que aparece no
473
+ # código fonte e nas saídas dos braços).
474
+ forms_map = {
475
+ f: resolve_fact_forms(conn, repository_id, f)
476
+ for f in task.facts}
477
+ rep["no_atlas"] = no_atlas(
478
+ fixture, task.grep_seeds, forms_map, counter=counter)
479
+ rep["pack_only"] = pack_arm(
480
+ fixture, task, conn, repository_id, forms_map)
481
+ rep["atlas_plus_search"] = atlas_search_arm(
482
+ fixture, task, conn, repository_id, forms_map,
483
+ data_dir=data_dir, config_dir=config_dir, counter=counter)
484
+ rep["target_snapshot_id"] = snapshot_id
485
+ return rep
486
+
487
+
488
+ def _indexed_slot(
489
+ repo_root: Path, task: PackTask, work_dir: Path, ordinal: int,
490
+ ) -> dict[str, Any]:
491
+ """Provisiona o workspace indexado da fixture (uma indexação real
492
+ por par fixture+adapters) — conn aberta na primeira demanda."""
493
+ ws = work_dir / f"ws-{ordinal}"
494
+ data_dir, config_dir = ws / "data", ws / "cfg"
495
+ data_dir.mkdir(parents=True)
496
+ config_dir.mkdir(parents=True)
497
+ run = index_fixture(
498
+ repo_root / task.fixture, task.domains, task.cross_domain,
499
+ data_dir=data_dir, config_dir=config_dir)
500
+ return {"data_dir": data_dir, "config_dir": config_dir,
501
+ "index_run": run, "conn": None,
502
+ "repo_id": -1, "snapshot_id": -1}
503
+
504
+
505
+ def _open_slot(repo_root: Path, task: PackTask,
506
+ slot: dict[str, Any]) -> None:
507
+ """Abre conn + resolve repo/snapshot pela mesma superfície do
508
+ engine (``StructuralSearch``)."""
509
+ conn = sqlite3.connect(str(slot["data_dir"] / "atlas.db"))
510
+ conn.row_factory = sqlite3.Row
511
+ from code_atlas.config import resolve_root
512
+ from code_atlas.search.structural import StructuralSearch
513
+
514
+ search = StructuralSearch(conn)
515
+ repo_id = search.repo_for_root(
516
+ str(resolve_root(str(repo_root / task.fixture))))
517
+ snap = (search.resolve_snapshot(repo_id, None)[0]
518
+ if repo_id is not None else None)
519
+ slot["conn"] = conn
520
+ slot["repo_id"] = repo_id if repo_id is not None else -1
521
+ slot["snapshot_id"] = (
522
+ int(snap.id) if snap is not None and snap.id is not None
523
+ else -1)
524
+
525
+
526
+ def _arm_totals(
527
+ results: Mapping[str, Any], arm: str, sub: str | None,
528
+ ) -> tuple[int, int, int]:
529
+ """``(queries, tokens, facts)`` agregados de um braço (``sub`` =
530
+ variante dentro de ``atlas_plus_search``)."""
531
+ queries = tokens = facts = 0
532
+ for t in results["tasks"].values():
533
+ if not isinstance(t, dict):
534
+ continue
535
+ block = t.get(arm)
536
+ if sub is not None:
537
+ block = block.get(sub) if isinstance(block, dict) else None
538
+ if not isinstance(block, dict):
539
+ continue
540
+ queries += int(block.get("queries") or 0)
541
+ tokens += int(block.get("tokens") or 0)
542
+ facts += int(block.get("facts_answerable") or 0)
543
+ return queries, tokens, facts
544
+
545
+
546
+ def _summarize(results: dict[str, Any]) -> dict[str, Any]:
547
+ """Agregado honesto — totais por braço; nada de média inventada."""
548
+ facts_total = sum(
549
+ int(t["pack_only"]["facts_expected"])
550
+ for t in results["tasks"].values()
551
+ if isinstance(t, dict)
552
+ and isinstance(t.get("pack_only"), dict)
553
+ and "facts_expected" in t["pack_only"])
554
+ summary: dict[str, Any] = {"tasks": len(results["tasks"]),
555
+ "facts_total": facts_total}
556
+ for name, arm, sub in (
557
+ ("no_atlas", "no_atlas", None),
558
+ ("pack_only", "pack_only", None),
559
+ ("atlas_impact", "atlas_plus_search", "impact"),
560
+ ("atlas_search", "atlas_plus_search", "search")):
561
+ queries, tokens, facts = _arm_totals(results, arm, sub)
562
+ summary[name] = {
563
+ "queries": queries,
564
+ "tokens": tokens,
565
+ "facts_answerable": facts,
566
+ "fact_coverage": (facts / facts_total
567
+ if facts_total else None),
568
+ }
569
+ return summary
570
+
571
+
572
+ def evaluate_all(
573
+ repo_root: Path, tasks: Sequence[PackTask], work_dir: Path,
574
+ ) -> dict[str, Any]:
575
+ """Indexa cada fixture uma vez (mesmo ``atlas.db`` por par
576
+ fixture+adapters) e mede todos os braços de cada tarefa."""
577
+ from code_atlas.env_override import patched_env
578
+
579
+ results: dict[str, Any] = {"schema": RESULTS_SCHEMA, "tasks": {}}
580
+ indexed: dict[tuple[str, tuple[str, ...], bool],
581
+ dict[str, Any]] = {}
582
+ for task in tasks:
583
+ key = (task.fixture, task.domains, task.cross_domain)
584
+ slot = indexed.get(key)
585
+ if slot is None:
586
+ slot = _indexed_slot(repo_root, task, work_dir,
587
+ len(indexed))
588
+ indexed[key] = slot
589
+ if slot["index_run"].exit_code != 0:
590
+ results["tasks"][task.task_id] = {
591
+ "id": task.task_id,
592
+ "error": f"index exit {slot['index_run'].exit_code}",
593
+ }
594
+ continue
595
+ if slot["conn"] is None:
596
+ _open_slot(repo_root, task, slot)
597
+ env = {"CODE_ATLAS_DATA_DIR": str(slot["data_dir"]),
598
+ "CODE_ATLAS_CONFIG_DIR": str(slot["config_dir"])}
599
+ with patched_env(env):
600
+ results["tasks"][task.task_id] = evaluate_task(
601
+ repo_root, task, slot["conn"],
602
+ int(slot["repo_id"]), int(slot["snapshot_id"]),
603
+ data_dir=slot["data_dir"],
604
+ config_dir=slot["config_dir"])
605
+ for slot in indexed.values():
606
+ if slot["conn"] is not None:
607
+ slot["conn"].close()
608
+
609
+ results["summary"] = _summarize(results)
610
+ return results
@@ -0,0 +1,72 @@
1
+ """Federação multi-repo (lf-0118, Cycle 7 Phase 10).
2
+
3
+ Workspaces nomeados agrupam >=2 repositórios já indexados; cada membro
4
+ pode pinar um snapshot explícito (o default é o READY mais recente —
5
+ registrado na view, nunca presumido "mesmo tempo"). A leitura federada
6
+ junta entidades de domínio por identidade lógica compartilhada e emite
7
+ arestas ``shared_entity`` **computadas** — nada é persistido além das
8
+ tabelas v10, nenhuma relação é inventada e o isolamento por snapshot
9
+ entre repositórios é preservado por construção.
10
+ """
11
+
12
+ from code_atlas.federation.federation import (
13
+ Federation,
14
+ normalize_lookup_key,
15
+ shared_key_of,
16
+ )
17
+ from code_atlas.federation.model import (
18
+ FEDERATION_RESOLVER_ID,
19
+ MAX_GRAPH_DEPTH,
20
+ MIN_FEDERATION_MEMBERS,
21
+ VIEW_ID_FORMAT,
22
+ EntityNode,
23
+ FederatedEdge,
24
+ FederatedGraph,
25
+ FederatedView,
26
+ FederationError,
27
+ FederationMember,
28
+ FederationWorkspace,
29
+ InvalidKeyError,
30
+ InvalidPinError,
31
+ MemberNotFoundError,
32
+ RepositoryNotFoundError,
33
+ SharedOccurrence,
34
+ SnapshotNotFoundError,
35
+ SnapshotNotReadyError,
36
+ ViewMember,
37
+ WorkspaceExistsError,
38
+ WorkspaceNotFoundError,
39
+ WorkspaceSizeError,
40
+ compute_view_id,
41
+ )
42
+ from code_atlas.federation.store import FederationStore
43
+
44
+ __all__ = [
45
+ "FEDERATION_RESOLVER_ID",
46
+ "MAX_GRAPH_DEPTH",
47
+ "MIN_FEDERATION_MEMBERS",
48
+ "VIEW_ID_FORMAT",
49
+ "EntityNode",
50
+ "FederatedEdge",
51
+ "FederatedGraph",
52
+ "FederatedView",
53
+ "Federation",
54
+ "FederationError",
55
+ "FederationMember",
56
+ "FederationStore",
57
+ "FederationWorkspace",
58
+ "InvalidKeyError",
59
+ "InvalidPinError",
60
+ "MemberNotFoundError",
61
+ "RepositoryNotFoundError",
62
+ "SharedOccurrence",
63
+ "SnapshotNotFoundError",
64
+ "SnapshotNotReadyError",
65
+ "ViewMember",
66
+ "WorkspaceExistsError",
67
+ "WorkspaceNotFoundError",
68
+ "WorkspaceSizeError",
69
+ "compute_view_id",
70
+ "normalize_lookup_key",
71
+ "shared_key_of",
72
+ ]