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
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"""Avaliação medida da extração de domínio (lf-0124, Cycle 7 Phase 16).
|
|
2
|
+
|
|
3
|
+
Framework evidence-first de precisão medida: cada fixture versionada é
|
|
4
|
+
indexada pela **pipeline real** — ``IndexCoordinator`` com
|
|
5
|
+
``DomainRegistry`` (e ``CrossDomainResolver`` no cenário cross-domain) —
|
|
6
|
+
e o estado persistido (``domain_entities`` + ``domain_relations`` +
|
|
7
|
+
``domain_entity_occurrences``) é comparado contra goldens curados.
|
|
8
|
+
|
|
9
|
+
Dois níveis medidos, sem inventar nada:
|
|
10
|
+
|
|
11
|
+
- **extração**: ``adapter.extract`` sobre os artefatos que o coordinator
|
|
12
|
+
descobriria (``supported_artifacts`` ∩ scan) — entidades, relações e
|
|
13
|
+
``DomainDiagnostic`` (que nunca são persistidos por desenho);
|
|
14
|
+
- **persistido**: as rows reais do snapshot — ``resolution_status``
|
|
15
|
+
final (``resolved``/``ambiguous``/``unresolved``/``external``),
|
|
16
|
+
``edges_skipped`` (origem sem âncora) e arestas produzidas pelo
|
|
17
|
+
resolver cross-domain (``resolver='cross-domain'``).
|
|
18
|
+
|
|
19
|
+
Goldens (``evaluation/golden/*.json``) são inventários **exaustivos**
|
|
20
|
+
de arestas + contagens exatas + listas de entidades proibidas — a
|
|
21
|
+
comparação é conjunto-a-conjunto (``matched``/``missing``/``unexpected``),
|
|
22
|
+
nunca amostragem otimista. ``run_eval.py --bless`` regenera; qualquer
|
|
23
|
+
diff vira falha legível no pytest.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import sqlite3
|
|
30
|
+
import tempfile
|
|
31
|
+
from collections import Counter
|
|
32
|
+
from collections.abc import Callable, Mapping
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any, cast
|
|
36
|
+
|
|
37
|
+
from code_atlas.discovery.identity import identity_key
|
|
38
|
+
from code_atlas.discovery.scanner import scan as default_scan
|
|
39
|
+
from code_atlas.domains.contracts import (
|
|
40
|
+
ArtifactSupport,
|
|
41
|
+
DiscoveredArtifact,
|
|
42
|
+
DomainAdapter,
|
|
43
|
+
DomainExtractionRequest,
|
|
44
|
+
DomainExtractionResult,
|
|
45
|
+
)
|
|
46
|
+
from code_atlas.domains.crossresolve import CrossDomainResolver
|
|
47
|
+
from code_atlas.domains.identity import DomainIdentity
|
|
48
|
+
from code_atlas.domains.registry import DomainRegistry
|
|
49
|
+
from code_atlas.indexing import IndexCoordinator
|
|
50
|
+
from code_atlas.storage import connect, migrate
|
|
51
|
+
from code_atlas.storage.store import Store
|
|
52
|
+
|
|
53
|
+
GOLDEN_SCHEMA = 1
|
|
54
|
+
RESULTS_SCHEMA = 1
|
|
55
|
+
|
|
56
|
+
#: Prefixes que distinguem o tipo de endpoint na forma renderizada da
|
|
57
|
+
#: aresta: entidade ``dom:cat.ns.name@env``, símbolo ``sym:qname`` e
|
|
58
|
+
#: alvo textual honesto ``txt:...`` (unresolved/external declarado).
|
|
59
|
+
SYM_PREFIX = "sym:"
|
|
60
|
+
TXT_PREFIX = "txt:"
|
|
61
|
+
|
|
62
|
+
AdapterFactory = Callable[[Path], DomainAdapter]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sql(fx: Path) -> DomainAdapter:
|
|
66
|
+
from code_atlas.domains.sql import SqlDomainAdapter
|
|
67
|
+
|
|
68
|
+
return SqlDomainAdapter(base_path=fx)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _spark(fx: Path) -> DomainAdapter:
|
|
72
|
+
from code_atlas.domains.spark import PySparkAdapter
|
|
73
|
+
|
|
74
|
+
return PySparkAdapter(base_path=fx)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _aws(fx: Path) -> DomainAdapter:
|
|
78
|
+
from code_atlas.domains.aws import AwsDataAdapter
|
|
79
|
+
|
|
80
|
+
return AwsDataAdapter(base_path=fx)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _api(fx: Path) -> DomainAdapter:
|
|
84
|
+
from code_atlas.domains.api import ApiContractAdapter
|
|
85
|
+
|
|
86
|
+
return ApiContractAdapter(workspace_root=fx)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _terraform(fx: Path) -> DomainAdapter:
|
|
90
|
+
from code_atlas.domains.terraform import TerraformAdapter
|
|
91
|
+
|
|
92
|
+
return TerraformAdapter(base_path=fx)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cicd(fx: Path) -> DomainAdapter:
|
|
96
|
+
from code_atlas.domains.cicd import GitHubActionsAdapter
|
|
97
|
+
|
|
98
|
+
return GitHubActionsAdapter.for_workspace(fx)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_ADAPTER_FACTORIES: dict[str, AdapterFactory] = {
|
|
102
|
+
"sql": _sql,
|
|
103
|
+
"pyspark": _spark,
|
|
104
|
+
"aws-data": _aws,
|
|
105
|
+
"api": _api,
|
|
106
|
+
"terraform": _terraform,
|
|
107
|
+
"cicd": _cicd,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class DomainSpec:
|
|
113
|
+
"""Cenário de avaliação: fixture + adapters + resolução cross.
|
|
114
|
+
|
|
115
|
+
``adapter_ids`` são os ``domain_id`` reais registrados no
|
|
116
|
+
``DomainRegistry`` — para cenários cross-domain mais de um adapter
|
|
117
|
+
observa o mesmo snapshot. ``cross=True`` injeta o
|
|
118
|
+
``CrossDomainResolver`` na pipeline (lf-0117).
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
eval_id: str
|
|
122
|
+
fixture: str # relativo à raiz do repositório
|
|
123
|
+
adapter_ids: tuple[str, ...]
|
|
124
|
+
cross: bool = False
|
|
125
|
+
note: str = ""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
#: Cenários avaliados — um por fixture versionada + o workspace
|
|
129
|
+
#: cross-domain (lf-0124). Ordem estável = ordem do relatório.
|
|
130
|
+
DOMAIN_SPECS: tuple[DomainSpec, ...] = (
|
|
131
|
+
DomainSpec(
|
|
132
|
+
"sql", "tests/fixtures/sql_warehouse", ("sql",),
|
|
133
|
+
note="pipeline landing→staging→mart + arquivo malformado",
|
|
134
|
+
),
|
|
135
|
+
DomainSpec(
|
|
136
|
+
"pyspark", "tests/fixtures/pyspark_repo", ("pyspark",),
|
|
137
|
+
note="jobs DataFrame/spark.sql/temp views + dinâmicos honestos",
|
|
138
|
+
),
|
|
139
|
+
DomainSpec(
|
|
140
|
+
"aws", "tests/fixtures/aws_repo", ("aws-data",),
|
|
141
|
+
note="Glue/Athena/Iceberg/EMR + catálogo cross-account + configs",
|
|
142
|
+
),
|
|
143
|
+
DomainSpec(
|
|
144
|
+
"api", "tests/fixtures/api_repo", ("api",),
|
|
145
|
+
note="OpenAPI + FastAPI/Express + consumidores (2 edges_skipped "
|
|
146
|
+
"— web.legacy sem parser JS)",
|
|
147
|
+
),
|
|
148
|
+
DomainSpec(
|
|
149
|
+
"terraform", "tests/fixtures/terraform_infra", ("terraform",),
|
|
150
|
+
note="HCL + tf.json + tfvars + módulo local/registry + broken/",
|
|
151
|
+
),
|
|
152
|
+
DomainSpec(
|
|
153
|
+
"cicd", "tests/fixtures/cicd_repo", ("cicd",),
|
|
154
|
+
note="GitHub Actions workflows/jobs/deps + workflow malformado",
|
|
155
|
+
),
|
|
156
|
+
DomainSpec(
|
|
157
|
+
"crossdomain", "tests/fixtures/crossdomain_ws",
|
|
158
|
+
("sql", "pyspark", "api"),
|
|
159
|
+
cross=True,
|
|
160
|
+
note="workspace multi-adapter: spark↔sql identidade plena e "
|
|
161
|
+
"parcial, consumidor↔endpoint OpenAPI (lf-0117)",
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def spec_by_id(eval_id: str) -> DomainSpec:
|
|
167
|
+
for spec in DOMAIN_SPECS:
|
|
168
|
+
if spec.eval_id == eval_id:
|
|
169
|
+
return spec
|
|
170
|
+
raise KeyError(f"unknown eval domain: {eval_id!r}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _display(identity_key_str: str) -> str:
|
|
174
|
+
"""``identity_key`` canônica → forma legível ``dom:cat.ns.name@env``."""
|
|
175
|
+
segs = DomainIdentity.parse_key(identity_key_str)
|
|
176
|
+
if segs is None or len(segs) < 7:
|
|
177
|
+
return identity_key_str
|
|
178
|
+
domain, repository, root, catalog, namespace, env, qname = segs[:7]
|
|
179
|
+
return DomainIdentity(
|
|
180
|
+
domain=domain, repository=repository, root=root, catalog=catalog,
|
|
181
|
+
namespace=namespace, environment=env, qualified_name=qname,
|
|
182
|
+
).display()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _discover_artifacts(
|
|
186
|
+
files: Mapping[str, str], support: ArtifactSupport
|
|
187
|
+
) -> tuple[DiscoveredArtifact, ...]:
|
|
188
|
+
"""Mesmo recorte que ``IndexCoordinator._extract_domains`` aplica:
|
|
189
|
+
``supported_artifacts`` ∩ inventário, ordenado, com sha do scan."""
|
|
190
|
+
return tuple(
|
|
191
|
+
DiscoveredArtifact(path=p, sha256=files[p])
|
|
192
|
+
for p in sorted(p for p in files if support.matches(p))
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _extract_for_diagnostics(
|
|
197
|
+
adapters: Mapping[str, DomainAdapter],
|
|
198
|
+
files: Mapping[str, str],
|
|
199
|
+
repository: str,
|
|
200
|
+
snapshot: str,
|
|
201
|
+
) -> dict[str, DomainExtractionResult]:
|
|
202
|
+
"""Re-roda ``extract`` com o MESMO request do coordinator.
|
|
203
|
+
|
|
204
|
+
``DomainDiagnostic`` nunca é persistido (desenho da v9) — para
|
|
205
|
+
medi-lo reexecutamos os adapters, que são puros/determinísticos
|
|
206
|
+
sobre o mesmo recorte (evidence-first, sem I/O fora do request).
|
|
207
|
+
"""
|
|
208
|
+
results: dict[str, DomainExtractionResult] = {}
|
|
209
|
+
for domain_id, adapter in adapters.items():
|
|
210
|
+
artifacts = _discover_artifacts(files, adapter.supported_artifacts())
|
|
211
|
+
results[domain_id] = adapter.extract(
|
|
212
|
+
DomainExtractionRequest(
|
|
213
|
+
repository=repository,
|
|
214
|
+
artifacts=artifacts,
|
|
215
|
+
snapshot=snapshot,
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
return results
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _read_persisted(
|
|
222
|
+
conn: sqlite3.Connection, store: Store, snapshot_id: int
|
|
223
|
+
) -> dict[str, Any]:
|
|
224
|
+
"""Estado persistido do snapshot → relatório de domínio."""
|
|
225
|
+
occ_display: dict[int, str] = {}
|
|
226
|
+
occ_kind: dict[int, str] = {}
|
|
227
|
+
for row in store.domain_occurrences_snapshot(snapshot_id):
|
|
228
|
+
occ_display[int(row["occ_id"])] = _display(str(row["identity_key"]))
|
|
229
|
+
occ_kind[int(row["occ_id"])] = str(row["kind"])
|
|
230
|
+
sym_qname: dict[int, str] = {
|
|
231
|
+
int(r["id"]): str(r["qualified_name"])
|
|
232
|
+
for r in conn.execute(
|
|
233
|
+
"SELECT so.id, s.qualified_name FROM symbol_occurrences so"
|
|
234
|
+
" JOIN symbols s ON s.id = so.logical_symbol_id"
|
|
235
|
+
" WHERE so.snapshot_id = ?",
|
|
236
|
+
(snapshot_id,),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
kind_counts = {
|
|
240
|
+
str(r["kind"]): int(r["c"])
|
|
241
|
+
for r in conn.execute(
|
|
242
|
+
"SELECT kind, COUNT(*) c FROM domain_entities GROUP BY kind"
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
entities = sorted(
|
|
246
|
+
_display(str(r["identity_key"]))
|
|
247
|
+
for r in conn.execute(
|
|
248
|
+
"SELECT identity_key FROM domain_entities ORDER BY identity_key"
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
rels = store.domain_relations_snapshot(snapshot_id)
|
|
252
|
+
|
|
253
|
+
def _frm(r: sqlite3.Row) -> str:
|
|
254
|
+
fe = r["from_entity_occ_id"]
|
|
255
|
+
if fe is not None:
|
|
256
|
+
return occ_display.get(int(fe), f"?e{fe}")
|
|
257
|
+
fs = r["from_symbol_occ_id"]
|
|
258
|
+
return f"{SYM_PREFIX}{sym_qname.get(int(fs), fs)}"
|
|
259
|
+
|
|
260
|
+
def _to(r: sqlite3.Row) -> str:
|
|
261
|
+
te = r["to_entity_occ_id"]
|
|
262
|
+
if te is not None:
|
|
263
|
+
return occ_display.get(int(te), f"?e{te}")
|
|
264
|
+
ts = r["to_symbol_occ_id"]
|
|
265
|
+
if ts is not None:
|
|
266
|
+
return f"{SYM_PREFIX}{sym_qname.get(int(ts), ts)}"
|
|
267
|
+
return f"{TXT_PREFIX}{r['to_unresolved']}"
|
|
268
|
+
|
|
269
|
+
edges = [
|
|
270
|
+
{
|
|
271
|
+
"type": str(r["type"]),
|
|
272
|
+
"from": _frm(r),
|
|
273
|
+
"to": _to(r),
|
|
274
|
+
"status": str(r["resolution_status"]),
|
|
275
|
+
"confidence": str(r["confidence_class"]),
|
|
276
|
+
"resolver": str(r["resolver"]),
|
|
277
|
+
}
|
|
278
|
+
for r in rels
|
|
279
|
+
]
|
|
280
|
+
edges.sort(
|
|
281
|
+
key=lambda e: (e["type"], e["from"], e["to"], e["status"],
|
|
282
|
+
e["confidence"], e["resolver"])
|
|
283
|
+
)
|
|
284
|
+
return {
|
|
285
|
+
"entities": entities,
|
|
286
|
+
"by_kind": kind_counts,
|
|
287
|
+
"occurrences": len(occ_display),
|
|
288
|
+
"edges": edges,
|
|
289
|
+
"by_type": dict(Counter(e["type"] for e in edges)),
|
|
290
|
+
"by_status": dict(Counter(e["status"] for e in edges)),
|
|
291
|
+
"by_confidence": dict(Counter(e["confidence"] for e in edges)),
|
|
292
|
+
"by_resolver": dict(Counter(e["resolver"] for e in edges)),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def run_domain_eval(
|
|
297
|
+
repo_root: Path, spec: DomainSpec, work_dir: Path
|
|
298
|
+
) -> dict[str, object]:
|
|
299
|
+
"""Executa a pipeline real sobre a fixture e devolve o relatório.
|
|
300
|
+
|
|
301
|
+
``work_dir`` recebe o ``atlas.db`` temporário do cenário (um por
|
|
302
|
+
domínio — isolamento de snapshot/repositório entre fixtures).
|
|
303
|
+
"""
|
|
304
|
+
fixture = Path(repo_root) / spec.fixture
|
|
305
|
+
scan = default_scan(fixture)
|
|
306
|
+
files = {f.path: (f.sha256 or "") for f in scan.files}
|
|
307
|
+
adapters: dict[str, DomainAdapter] = {
|
|
308
|
+
aid: _ADAPTER_FACTORIES[aid](fixture) for aid in spec.adapter_ids
|
|
309
|
+
}
|
|
310
|
+
registry = DomainRegistry()
|
|
311
|
+
for adapter in adapters.values():
|
|
312
|
+
registry.register(adapter)
|
|
313
|
+
|
|
314
|
+
conn = connect(Path(work_dir) / f"{spec.eval_id}.db")
|
|
315
|
+
migrate(conn)
|
|
316
|
+
try:
|
|
317
|
+
coord = IndexCoordinator(
|
|
318
|
+
conn,
|
|
319
|
+
domain_registry=registry,
|
|
320
|
+
# cross rodado manualmente abaixo — mesmas chamadas do
|
|
321
|
+
# coordinator, mas capturando o plano (diagnósticos+stats).
|
|
322
|
+
cross_resolver=None,
|
|
323
|
+
)
|
|
324
|
+
outcome = coord.index(fixture)
|
|
325
|
+
if outcome.status != "success" or outcome.snapshot_id is None:
|
|
326
|
+
return {
|
|
327
|
+
"domain": spec.eval_id,
|
|
328
|
+
"fixture": spec.fixture,
|
|
329
|
+
"index_status": outcome.status,
|
|
330
|
+
"error": outcome.error,
|
|
331
|
+
}
|
|
332
|
+
snapshot_id = outcome.snapshot_id
|
|
333
|
+
store = Store(conn)
|
|
334
|
+
|
|
335
|
+
cross_info: dict[str, Any] = {}
|
|
336
|
+
if spec.cross:
|
|
337
|
+
# Réplica fiel de ``_run_cross_resolution``: mesmo resolver,
|
|
338
|
+
# mesma ordem (plan → persist). Roda pós-publish numa
|
|
339
|
+
# transação própria — o estado final é idêntico ao do
|
|
340
|
+
# caminho inline; a diferença é só observável: o plano
|
|
341
|
+
# (diagnósticos + stats) fica disponível para medir.
|
|
342
|
+
resolver = CrossDomainResolver()
|
|
343
|
+
with conn:
|
|
344
|
+
xplan = resolver.plan(store, snapshot_id)
|
|
345
|
+
applied = store.persist_cross_resolutions(
|
|
346
|
+
snapshot_id, xplan.update_rows(),
|
|
347
|
+
xplan.insert_rows(snapshot_id),
|
|
348
|
+
)
|
|
349
|
+
cross_info = {
|
|
350
|
+
"stats": dict(xplan.stats),
|
|
351
|
+
"applied": dict(applied),
|
|
352
|
+
"diagnostics": [
|
|
353
|
+
{"severity": d.severity, "code": d.code,
|
|
354
|
+
"message": d.message}
|
|
355
|
+
for d in xplan.diagnostics
|
|
356
|
+
],
|
|
357
|
+
}
|
|
358
|
+
persisted = _read_persisted(conn, store, snapshot_id)
|
|
359
|
+
|
|
360
|
+
# Mesmo request do coordinator (repository=ikey, snapshot=digest,
|
|
361
|
+
# artifacts por supported_artifacts) — re-run só para medir os
|
|
362
|
+
# diagnósticos, que não são persistidos por desenho.
|
|
363
|
+
repository = identity_key(scan.git, fixture)
|
|
364
|
+
digest = scan.manifest_sha256()
|
|
365
|
+
extractions = _extract_for_diagnostics(
|
|
366
|
+
adapters, files, repository, digest
|
|
367
|
+
)
|
|
368
|
+
finally:
|
|
369
|
+
conn.close()
|
|
370
|
+
|
|
371
|
+
diagnostics = [
|
|
372
|
+
{
|
|
373
|
+
"severity": d.severity,
|
|
374
|
+
"code": d.code,
|
|
375
|
+
"path": d.path,
|
|
376
|
+
"adapter": d.adapter_id or domain_id,
|
|
377
|
+
}
|
|
378
|
+
for domain_id, res in extractions.items()
|
|
379
|
+
for d in res.diagnostics
|
|
380
|
+
]
|
|
381
|
+
diagnostics.extend(
|
|
382
|
+
{
|
|
383
|
+
"severity": str(d["severity"]),
|
|
384
|
+
"code": str(d["code"]),
|
|
385
|
+
"path": "",
|
|
386
|
+
"adapter": "cross-domain",
|
|
387
|
+
}
|
|
388
|
+
for d in cross_info.get("diagnostics", ())
|
|
389
|
+
)
|
|
390
|
+
diagnostics.sort(key=lambda d: (d["path"], d["code"], d["severity"]))
|
|
391
|
+
artifacts = {
|
|
392
|
+
domain_id: [a.path for a in _discover_artifacts(
|
|
393
|
+
files, adapters[domain_id].supported_artifacts()
|
|
394
|
+
)]
|
|
395
|
+
for domain_id in adapters
|
|
396
|
+
}
|
|
397
|
+
stats: dict[str, int] = {}
|
|
398
|
+
for domain_id, res in extractions.items():
|
|
399
|
+
for k, v in res.stats.items():
|
|
400
|
+
stats[f"{domain_id}.{k}"] = stats.get(f"{domain_id}.{k}", 0) + int(v)
|
|
401
|
+
|
|
402
|
+
# contagens de domínio/cross do job de indexação — medido, não derivado
|
|
403
|
+
job_counts = {
|
|
404
|
+
k: int(v) for k, v in (outcome.stats or {}).items()
|
|
405
|
+
if k.startswith(("domain_", "cross_"))
|
|
406
|
+
}
|
|
407
|
+
edges_skipped = sum(
|
|
408
|
+
int(v) for k, v in (outcome.stats or {}).items()
|
|
409
|
+
if k.startswith("domain_") and k.endswith("_edges_skipped")
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
"domain": spec.eval_id,
|
|
414
|
+
"fixture": spec.fixture,
|
|
415
|
+
"note": spec.note,
|
|
416
|
+
"index_status": outcome.status,
|
|
417
|
+
"adapters": sorted(adapters),
|
|
418
|
+
"artifacts": artifacts,
|
|
419
|
+
"entities": {
|
|
420
|
+
"total": len(persisted["entities"]),
|
|
421
|
+
"by_kind": persisted["by_kind"],
|
|
422
|
+
"occurrences": persisted["occurrences"],
|
|
423
|
+
"names": persisted["entities"],
|
|
424
|
+
},
|
|
425
|
+
"relations": {
|
|
426
|
+
"total": len(persisted["edges"]),
|
|
427
|
+
"by_type": persisted["by_type"],
|
|
428
|
+
"by_status": persisted["by_status"],
|
|
429
|
+
"by_confidence": persisted["by_confidence"],
|
|
430
|
+
"by_resolver": persisted["by_resolver"],
|
|
431
|
+
"edges_skipped": edges_skipped,
|
|
432
|
+
},
|
|
433
|
+
"diagnostics": diagnostics,
|
|
434
|
+
"edges": persisted["edges"],
|
|
435
|
+
"cross": cross_info,
|
|
436
|
+
"job_counts": job_counts,
|
|
437
|
+
"extract_stats": stats,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ---------------------------------------------------------------------
|
|
442
|
+
# goldens — comparação conjunto-a-conjunto
|
|
443
|
+
# ---------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _edge_key(e: Mapping[str, object]) -> tuple[str, str, str, str, str, str]:
|
|
447
|
+
return (
|
|
448
|
+
str(e["type"]), str(e["from"]), str(e["to"]), str(e["status"]),
|
|
449
|
+
str(e["confidence"]), str(e["resolver"]),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _fmt_edge(e: Mapping[str, object]) -> str:
|
|
454
|
+
return (
|
|
455
|
+
f"{e['type']} {e['from']} -> {e['to']} "
|
|
456
|
+
f"[{e['status']}|{e['confidence']}|{e['resolver']}]"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _diag_key(d: Mapping[str, object]) -> tuple[str, str, str]:
|
|
461
|
+
return (str(d["severity"]), str(d["code"]), str(d["path"]))
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _fmt_diag(d: Mapping[str, object]) -> str:
|
|
465
|
+
return f"{d['severity']}:{d['code']}:{d['path'] or '-'}"
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _count_diff(
|
|
469
|
+
diffs: list[str], actual: int, want: Any, label: str
|
|
470
|
+
) -> None:
|
|
471
|
+
if want is None:
|
|
472
|
+
return
|
|
473
|
+
if actual != int(want):
|
|
474
|
+
diffs.append(f"{label}: expected {want}, got {actual}")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _diff_entities(
|
|
478
|
+
report: Mapping[str, Any], g_entities: Mapping[str, Any],
|
|
479
|
+
diffs: list[str],
|
|
480
|
+
) -> None:
|
|
481
|
+
r_entities = cast(Mapping[str, Any], report["entities"])
|
|
482
|
+
_count_diff(
|
|
483
|
+
diffs, int(r_entities["total"]), g_entities.get("total"),
|
|
484
|
+
"entities.total",
|
|
485
|
+
)
|
|
486
|
+
want_kind = g_entities.get("by_kind")
|
|
487
|
+
if want_kind is not None and dict(
|
|
488
|
+
r_entities["by_kind"]
|
|
489
|
+
) != dict(want_kind):
|
|
490
|
+
diffs.append(
|
|
491
|
+
"entities.by_kind: expected "
|
|
492
|
+
f"{dict(want_kind)}, got {dict(r_entities['by_kind'])}"
|
|
493
|
+
)
|
|
494
|
+
names = set(r_entities["names"])
|
|
495
|
+
for want in g_entities.get("expected", ()):
|
|
496
|
+
if want not in names:
|
|
497
|
+
diffs.append(f"entity expected but absent: {want}")
|
|
498
|
+
for bad in g_entities.get("forbidden", ()):
|
|
499
|
+
if bad in names:
|
|
500
|
+
diffs.append(f"entity forbidden but present: {bad}")
|
|
501
|
+
# inventário exato de nomes — quando presente no golden,
|
|
502
|
+
# rename/removal/addition vira diff explícito, não só delta
|
|
503
|
+
# agregado de total/by_kind
|
|
504
|
+
want_names = g_entities.get("names")
|
|
505
|
+
if want_names is not None:
|
|
506
|
+
for missing in sorted(set(want_names) - names):
|
|
507
|
+
diffs.append(f"missing entity: {missing}")
|
|
508
|
+
for extra in sorted(names - set(want_names)):
|
|
509
|
+
diffs.append(f"unexpected entity: {extra}")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _diff_relations(
|
|
513
|
+
report: Mapping[str, Any], g_relations: Mapping[str, Any],
|
|
514
|
+
diffs: list[str],
|
|
515
|
+
) -> None:
|
|
516
|
+
r_relations = cast(Mapping[str, Any], report["relations"])
|
|
517
|
+
_count_diff(
|
|
518
|
+
diffs, int(r_relations["total"]), g_relations.get("total"),
|
|
519
|
+
"relations.total",
|
|
520
|
+
)
|
|
521
|
+
for group in ("by_type", "by_status", "by_confidence", "by_resolver"):
|
|
522
|
+
want = g_relations.get(group)
|
|
523
|
+
if want is not None and dict(r_relations[group]) != dict(want):
|
|
524
|
+
diffs.append(
|
|
525
|
+
f"relations.{group}: expected {dict(want)}, "
|
|
526
|
+
f"got {dict(r_relations[group])}"
|
|
527
|
+
)
|
|
528
|
+
_count_diff(
|
|
529
|
+
diffs, int(r_relations["edges_skipped"]),
|
|
530
|
+
g_relations.get("edges_skipped"),
|
|
531
|
+
"relations.edges_skipped",
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _diff_edges(
|
|
536
|
+
report: Mapping[str, Any], golden: Mapping[str, Any],
|
|
537
|
+
diffs: list[str],
|
|
538
|
+
) -> None:
|
|
539
|
+
# inventário exaustivo de arestas — matched/missing/unexpected
|
|
540
|
+
want_edges = {_edge_key(e) for e in golden.get("edges", ())}
|
|
541
|
+
got_edges = {_edge_key(e) for e in report["edges"]}
|
|
542
|
+
by_key = {_edge_key(e): e for e in golden.get("edges", ())}
|
|
543
|
+
by_key.update({_edge_key(e): e for e in report["edges"]})
|
|
544
|
+
for ekey in sorted(want_edges - got_edges):
|
|
545
|
+
diffs.append(f"missing edge: {_fmt_edge(by_key[ekey])}")
|
|
546
|
+
for ekey in sorted(got_edges - want_edges):
|
|
547
|
+
diffs.append(f"unexpected edge: {_fmt_edge(by_key[ekey])}")
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _diff_diagnostics(
|
|
551
|
+
report: Mapping[str, Any], golden: Mapping[str, Any],
|
|
552
|
+
diffs: list[str],
|
|
553
|
+
) -> None:
|
|
554
|
+
# diagnósticos como multiconjunto (severity, code, path)
|
|
555
|
+
want_diags = Counter(
|
|
556
|
+
_diag_key(d) for d in golden.get("diagnostics", ())
|
|
557
|
+
)
|
|
558
|
+
got_diags = Counter(
|
|
559
|
+
_diag_key(d) for d in report["diagnostics"]
|
|
560
|
+
)
|
|
561
|
+
by_dkey: dict[tuple[str, str, str], Mapping[str, object]] = {
|
|
562
|
+
_diag_key(d): d for d in golden.get("diagnostics", ())
|
|
563
|
+
}
|
|
564
|
+
by_dkey.update(
|
|
565
|
+
{_diag_key(d): d for d in report["diagnostics"]}
|
|
566
|
+
)
|
|
567
|
+
for dkey in sorted(set(want_diags) | set(got_diags)):
|
|
568
|
+
if want_diags[dkey] != got_diags[dkey]:
|
|
569
|
+
diffs.append(
|
|
570
|
+
f"diagnostic {_fmt_diag(by_dkey[dkey])}: expected "
|
|
571
|
+
f"{want_diags[dkey]}x, got {got_diags[dkey]}x"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def diff_report(
|
|
576
|
+
report: Mapping[str, Any], golden: Mapping[str, Any]
|
|
577
|
+
) -> list[str]:
|
|
578
|
+
"""Diff legível report↔golden; ``[]`` = cenário dentro do esperado.
|
|
579
|
+
|
|
580
|
+
Checagens: contagens exatas (entidades/relations por kind/type/
|
|
581
|
+
status), amostras de entidades esperadas/proibidas, inventário
|
|
582
|
+
exaustivo de arestas (``missing``/``unexpected``) e diagnósticos.
|
|
583
|
+
"""
|
|
584
|
+
diffs: list[str] = []
|
|
585
|
+
g_entities = golden.get("entities", {})
|
|
586
|
+
if isinstance(g_entities, Mapping):
|
|
587
|
+
_diff_entities(report, g_entities, diffs)
|
|
588
|
+
g_relations = golden.get("relations", {})
|
|
589
|
+
if isinstance(g_relations, Mapping):
|
|
590
|
+
_diff_relations(report, g_relations, diffs)
|
|
591
|
+
_diff_edges(report, golden, diffs)
|
|
592
|
+
_diff_diagnostics(report, golden, diffs)
|
|
593
|
+
return diffs
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def load_golden(golden_dir: Path, eval_id: str) -> dict[str, Any] | None:
|
|
597
|
+
path = Path(golden_dir) / f"{eval_id}.json"
|
|
598
|
+
if not path.is_file():
|
|
599
|
+
return None
|
|
600
|
+
return cast("dict[str, Any]", json.loads(path.read_text(encoding="utf-8")))
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def evaluate_all(
|
|
604
|
+
repo_root: Path,
|
|
605
|
+
golden_dir: Path | None = None,
|
|
606
|
+
*,
|
|
607
|
+
work_dir: Path | None = None,
|
|
608
|
+
) -> dict[str, object]:
|
|
609
|
+
"""Roda todos os ``DOMAIN_SPECS`` e compara com goldens presentes."""
|
|
610
|
+
tmp: tempfile.TemporaryDirectory[str] | None = None
|
|
611
|
+
if work_dir is None:
|
|
612
|
+
tmp = tempfile.TemporaryDirectory(prefix="domain-eval-")
|
|
613
|
+
work_dir = Path(tmp.name)
|
|
614
|
+
try:
|
|
615
|
+
domains: dict[str, object] = {}
|
|
616
|
+
ok = True
|
|
617
|
+
for spec in DOMAIN_SPECS:
|
|
618
|
+
report = run_domain_eval(Path(repo_root), spec, Path(work_dir))
|
|
619
|
+
golden = (
|
|
620
|
+
load_golden(Path(golden_dir), spec.eval_id)
|
|
621
|
+
if golden_dir is not None
|
|
622
|
+
else None
|
|
623
|
+
)
|
|
624
|
+
check: dict[str, object] = {"golden": golden is not None}
|
|
625
|
+
if golden is not None:
|
|
626
|
+
diffs = diff_report(report, golden)
|
|
627
|
+
check["diffs"] = diffs
|
|
628
|
+
check["pass"] = not diffs
|
|
629
|
+
ok = ok and not diffs
|
|
630
|
+
report["check"] = check
|
|
631
|
+
domains[spec.eval_id] = report
|
|
632
|
+
return {
|
|
633
|
+
"schema": RESULTS_SCHEMA,
|
|
634
|
+
"domains": domains,
|
|
635
|
+
"summary": {
|
|
636
|
+
"scenarios": len(domains),
|
|
637
|
+
"golden_compared": sum(
|
|
638
|
+
1 for d in domains.values()
|
|
639
|
+
if isinstance(d, dict)
|
|
640
|
+
and isinstance(d.get("check"), dict)
|
|
641
|
+
and cast("dict[str, Any]", d["check"]).get("golden")
|
|
642
|
+
),
|
|
643
|
+
"all_pass": ok,
|
|
644
|
+
},
|
|
645
|
+
}
|
|
646
|
+
finally:
|
|
647
|
+
if tmp is not None:
|
|
648
|
+
tmp.cleanup()
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def report_to_golden(report: Mapping[str, Any]) -> dict[str, Any]:
|
|
652
|
+
"""Recorte persistível de ``report`` no schema de golden (bless).
|
|
653
|
+
|
|
654
|
+
``--bless`` é a ferramenta de curadoria: a saída DEVE ser revista
|
|
655
|
+
linha a linha contra a fixture antes de commitar — golden gerado
|
|
656
|
+
sem revisão é só um eco do comportamento corrente, não expectativa.
|
|
657
|
+
"""
|
|
658
|
+
entities = cast(Mapping[str, Any], report["entities"])
|
|
659
|
+
relations = cast(Mapping[str, Any], report["relations"])
|
|
660
|
+
return {
|
|
661
|
+
"schema": GOLDEN_SCHEMA,
|
|
662
|
+
"domain": report["domain"],
|
|
663
|
+
"fixture": report["fixture"],
|
|
664
|
+
"entities": {
|
|
665
|
+
"total": entities["total"],
|
|
666
|
+
"by_kind": entities["by_kind"],
|
|
667
|
+
"names": entities["names"],
|
|
668
|
+
"expected": [],
|
|
669
|
+
"forbidden": [],
|
|
670
|
+
},
|
|
671
|
+
"relations": {
|
|
672
|
+
"total": relations["total"],
|
|
673
|
+
"by_type": relations["by_type"],
|
|
674
|
+
"by_status": relations["by_status"],
|
|
675
|
+
"by_confidence": relations["by_confidence"],
|
|
676
|
+
"by_resolver": relations["by_resolver"],
|
|
677
|
+
"edges_skipped": relations["edges_skipped"],
|
|
678
|
+
},
|
|
679
|
+
"diagnostics": report["diagnostics"],
|
|
680
|
+
"edges": report["edges"],
|
|
681
|
+
}
|