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,758 @@
|
|
|
1
|
+
"""lf-0135 (Cycle 8 Phase 9) — blast radius contra verdade-terreno rotulada.
|
|
2
|
+
|
|
3
|
+
Corpus de cenários em ``evaluation/impact_truth/scenarios.json``: cada
|
|
4
|
+
cenário rotula **à mão** o conjunto esperado de impacto
|
|
5
|
+
(``direct``/``transitive``/``absent``) a partir da leitura da fixture e
|
|
6
|
+
da semântica declarada de travessia — arestas ``*_DEPENDS_ON`` propagam
|
|
7
|
+
``to → from``, arestas acopladas (``WRITES_TO``, ``TRANSFORMS``,
|
|
8
|
+
``PROVISIONS``, ``TRIGGERS``, ``DEPLOYS``, ``DEFINES``) propagam nos
|
|
9
|
+
dois sentidos, ``REFERENCES`` só quando é par de identidade.
|
|
10
|
+
|
|
11
|
+
O que é medido por cenário (pipeline REAL — o binário
|
|
12
|
+
``code-atlas``/``cli.main`` dirigido in-process com
|
|
13
|
+
``CODE_ATLAS_DATA_DIR``/``CODE_ATLAS_CONFIG_DIR`` isolados num
|
|
14
|
+
workspace temporário; nada é executado dos repos indexados):
|
|
15
|
+
|
|
16
|
+
- ``index <fixture> --domains <ids> [--cross-domain]`` — indexação real;
|
|
17
|
+
- ``impact symbol <seed> --root <fixture> --format json`` — modo de
|
|
18
|
+
domínio: a ref de entidade atravessa ``resolve_seeds`` (via
|
|
19
|
+
``ImpactRequest.symbol``, a fiação mínima do lf-0120);
|
|
20
|
+
- precision/recall do conjunto alcançável (deduplicado por nó) vs
|
|
21
|
+
rótulo — por classe (direct/transitive) e agregado;
|
|
22
|
+
- **false-confidence**: todo candidato ``verified`` precisa ter
|
|
23
|
+
``fully_verified`` + hops ``resolved``; itens rotulados possíveis
|
|
24
|
+
jamais podem subir a verified — orçamento ZERO, asserção dura;
|
|
25
|
+
- **evidence validity**: todo ``edge_refs`` aponta para uma row real do
|
|
26
|
+
snapshot alvo (``dom:N`` → ``domain_relations``, ``rel:N`` →
|
|
27
|
+
``relations``), ``hops == len(edge_refs) == len(statuses)`` e a
|
|
28
|
+
ocorrência do candidato existe no snapshot;
|
|
29
|
+
- **truncation honesty**: re-runs com ``--max-paths`` no limite exato
|
|
30
|
+
e com folga — ``truncated``/``truncation_reasons`` coerentes com a
|
|
31
|
+
união dos bounds que cortaram trabalho (lf-0151);
|
|
32
|
+
- ``unknown_coverage`` e ``diagnostics`` reportados verbatim.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import contextlib
|
|
38
|
+
import io
|
|
39
|
+
import json
|
|
40
|
+
import sqlite3
|
|
41
|
+
from collections.abc import Mapping, Sequence
|
|
42
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
SCENARIO_SCHEMA = 1
|
|
48
|
+
RESULTS_SCHEMA = 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Classe de impacto em ordem decrescente de "alcançável verificado".
|
|
52
|
+
_IMPACT_RANK = {"direct": 0, "transitive": 1, "possible": 2, "unknown": 3}
|
|
53
|
+
_STRENGTH_RANK = {"verified": 0, "supported": 1, "possible": 2,
|
|
54
|
+
"unknown": 3}
|
|
55
|
+
|
|
56
|
+
#: Folga acima do total de linhas no probe "over-cap" — o traversal
|
|
57
|
+
#: termina naturalmente, ``truncated`` deve ser False.
|
|
58
|
+
TRUNCATION_OVERCAP_SLACK = 3
|
|
59
|
+
|
|
60
|
+
#: Razões ligadas ao bound variado pelas sondas (``--max-paths``) —
|
|
61
|
+
#: todas as outras independem do cap de emissão.
|
|
62
|
+
_EMISSION_REASONS = frozenset({"max-paths"})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class ImpactScenario:
|
|
67
|
+
"""Cenário rotulado — fixture + seed + conjuntos esperados."""
|
|
68
|
+
|
|
69
|
+
scenario_id: str
|
|
70
|
+
prompt_class: str
|
|
71
|
+
fixture: str
|
|
72
|
+
domains: tuple[str, ...]
|
|
73
|
+
cross_domain: bool
|
|
74
|
+
seed: str
|
|
75
|
+
task: str
|
|
76
|
+
expected_direct: tuple[str, ...]
|
|
77
|
+
expected_transitive: tuple[str, ...]
|
|
78
|
+
expected_absent: tuple[str, ...]
|
|
79
|
+
notes: str = ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class CliRun:
|
|
84
|
+
"""Resultado de uma invocação in-process do CLI real."""
|
|
85
|
+
|
|
86
|
+
argv: tuple[str, ...]
|
|
87
|
+
exit_code: int
|
|
88
|
+
stdout: str
|
|
89
|
+
stderr: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_scenarios(path: Path) -> list[ImpactScenario]:
|
|
93
|
+
"""Carrega ``scenarios.json`` — validação estrutural mínima."""
|
|
94
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
95
|
+
if int(raw.get("schema", 0)) != SCENARIO_SCHEMA:
|
|
96
|
+
raise ValueError(f"impact truth schema != {SCENARIO_SCHEMA}")
|
|
97
|
+
out: list[ImpactScenario] = []
|
|
98
|
+
for s in raw["scenarios"]:
|
|
99
|
+
exp = s["expected"]
|
|
100
|
+
out.append(ImpactScenario(
|
|
101
|
+
scenario_id=str(s["id"]),
|
|
102
|
+
prompt_class=str(s.get("prompt_class", "")),
|
|
103
|
+
fixture=str(s["fixture"]),
|
|
104
|
+
domains=tuple(str(d) for d in s["domains"]),
|
|
105
|
+
cross_domain=bool(s.get("cross_domain", False)),
|
|
106
|
+
seed=str(s["seed"]),
|
|
107
|
+
task=str(s.get("task", "")),
|
|
108
|
+
expected_direct=tuple(str(x) for x in exp.get("direct", ())),
|
|
109
|
+
expected_transitive=tuple(
|
|
110
|
+
str(x) for x in exp.get("transitive", ())),
|
|
111
|
+
expected_absent=tuple(str(x) for x in exp.get("absent", ())),
|
|
112
|
+
notes=str(s.get("notes", "")),
|
|
113
|
+
))
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def validate_scenario(repo_root: Path, sc: ImpactScenario) -> list[str]:
|
|
118
|
+
"""Higiene do rótulo: conjuntos disjuntos, fixture existente."""
|
|
119
|
+
errs: list[str] = []
|
|
120
|
+
if not sc.seed.strip():
|
|
121
|
+
errs.append("seed vazia")
|
|
122
|
+
if not sc.domains:
|
|
123
|
+
errs.append("domains vazio — o cenário precisa de adapters")
|
|
124
|
+
if sc.cross_domain and len(sc.domains) < 2:
|
|
125
|
+
errs.append("cross_domain com <2 adapters")
|
|
126
|
+
if not (repo_root / sc.fixture).is_dir():
|
|
127
|
+
errs.append(f"fixture ausente: {sc.fixture}")
|
|
128
|
+
seen: dict[str, str] = {}
|
|
129
|
+
for label, items in (
|
|
130
|
+
("direct", sc.expected_direct),
|
|
131
|
+
("transitive", sc.expected_transitive),
|
|
132
|
+
("absent", sc.expected_absent),
|
|
133
|
+
):
|
|
134
|
+
for name in items:
|
|
135
|
+
if name in seen:
|
|
136
|
+
errs.append(
|
|
137
|
+
f"'{name}' rotulado em {seen[name]} e {label}")
|
|
138
|
+
seen[name] = label
|
|
139
|
+
return errs
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------
|
|
143
|
+
# harness — invoca o CLI real in-process, isolado por env
|
|
144
|
+
# ---------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def run_cli(
|
|
148
|
+
argv: Sequence[str], *, data_dir: Path, config_dir: Path
|
|
149
|
+
) -> CliRun:
|
|
150
|
+
"""Executa ``code-atlas <argv>`` in-process (argparse → run_* →
|
|
151
|
+
stdout), com ``CODE_ATLAS_DATA_DIR``/``CODE_ATLAS_CONFIG_DIR``
|
|
152
|
+
apontados para o workspace temporário. É a mesma superfície do
|
|
153
|
+
binário — só o transporte é in-process."""
|
|
154
|
+
from code_atlas.cli import main as cli_main
|
|
155
|
+
from code_atlas.env_override import patched_env
|
|
156
|
+
|
|
157
|
+
out_buf, err_buf = io.StringIO(), io.StringIO()
|
|
158
|
+
with patched_env({
|
|
159
|
+
"CODE_ATLAS_DATA_DIR": str(data_dir),
|
|
160
|
+
"CODE_ATLAS_CONFIG_DIR": str(config_dir),
|
|
161
|
+
}), redirect_stdout(out_buf), redirect_stderr(err_buf):
|
|
162
|
+
code = int(cli_main(list(argv)))
|
|
163
|
+
return CliRun(tuple(argv), code, out_buf.getvalue(),
|
|
164
|
+
err_buf.getvalue())
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def index_fixture(
|
|
168
|
+
fixture: Path, domains: Sequence[str], cross_domain: bool,
|
|
169
|
+
*, data_dir: Path, config_dir: Path,
|
|
170
|
+
) -> CliRun:
|
|
171
|
+
"""``index <fixture> --domains ...`` — pipeline real de indexação."""
|
|
172
|
+
argv = ["index", str(fixture)]
|
|
173
|
+
if domains:
|
|
174
|
+
argv += ["--domains", ",".join(domains)]
|
|
175
|
+
if cross_domain:
|
|
176
|
+
argv += ["--cross-domain"]
|
|
177
|
+
argv.append("--json")
|
|
178
|
+
return run_cli(argv, data_dir=data_dir, config_dir=config_dir)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def run_impact(
|
|
182
|
+
fixture: Path, seed: str, *, data_dir: Path, config_dir: Path,
|
|
183
|
+
extra_args: Sequence[str] = (),
|
|
184
|
+
) -> tuple[CliRun, dict[str, Any] | None]:
|
|
185
|
+
"""``impact symbol <seed> --root <fixture> --format json`` → payload."""
|
|
186
|
+
argv = ["impact", "symbol", seed, "--root", str(fixture),
|
|
187
|
+
"--format", "json", *extra_args]
|
|
188
|
+
run = run_cli(argv, data_dir=data_dir, config_dir=config_dir)
|
|
189
|
+
payload: dict[str, Any] | None = None
|
|
190
|
+
text = run.stdout.strip()
|
|
191
|
+
if text.startswith("{"):
|
|
192
|
+
try:
|
|
193
|
+
parsed = json.loads(text)
|
|
194
|
+
except json.JSONDecodeError:
|
|
195
|
+
parsed = None
|
|
196
|
+
if isinstance(parsed, dict):
|
|
197
|
+
payload = parsed
|
|
198
|
+
return run, payload
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------
|
|
202
|
+
# dedup + métricas
|
|
203
|
+
# ---------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class _Node:
|
|
208
|
+
"""Vista deduplicada de um nó impactado (várias linhas por
|
|
209
|
+
caminhos alternativos colapsam aqui)."""
|
|
210
|
+
|
|
211
|
+
name: str
|
|
212
|
+
node_type: str
|
|
213
|
+
kind: str
|
|
214
|
+
best_impact: str = "unknown"
|
|
215
|
+
best_strength: str = "unknown"
|
|
216
|
+
min_hops: int = 0
|
|
217
|
+
min_verified_hops: int | None = None
|
|
218
|
+
rows: int = 0
|
|
219
|
+
fully_verified_any: bool = False
|
|
220
|
+
|
|
221
|
+
def absorb(self, cand: Mapping[str, Any]) -> None:
|
|
222
|
+
impact = str(cand.get("impact", "unknown"))
|
|
223
|
+
strength = str(cand.get("strength", "unknown"))
|
|
224
|
+
path = cand.get("path") or {}
|
|
225
|
+
hops = int(path.get("hops") or 0)
|
|
226
|
+
verified = bool(path.get("fully_verified"))
|
|
227
|
+
if _IMPACT_RANK.get(impact, 9) < _IMPACT_RANK.get(
|
|
228
|
+
self.best_impact, 9):
|
|
229
|
+
self.best_impact = impact
|
|
230
|
+
if _STRENGTH_RANK.get(strength, 9) < _STRENGTH_RANK.get(
|
|
231
|
+
self.best_strength, 9):
|
|
232
|
+
self.best_strength = strength
|
|
233
|
+
if self.rows == 0 or hops < self.min_hops:
|
|
234
|
+
self.min_hops = hops
|
|
235
|
+
if verified and (
|
|
236
|
+
self.min_verified_hops is None
|
|
237
|
+
or hops < self.min_verified_hops):
|
|
238
|
+
self.min_verified_hops = hops
|
|
239
|
+
self.fully_verified_any = self.fully_verified_any or verified
|
|
240
|
+
self.rows += 1
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def dedup_candidates(
|
|
244
|
+
payload: Mapping[str, Any],
|
|
245
|
+
) -> dict[tuple[str, str], _Node]:
|
|
246
|
+
"""Colapsa linhas de ``impacts`` por ``(node_type, qualified_name)``."""
|
|
247
|
+
out: dict[tuple[str, str], _Node] = {}
|
|
248
|
+
for cand in payload.get("impacts") or ():
|
|
249
|
+
if not isinstance(cand, Mapping):
|
|
250
|
+
continue
|
|
251
|
+
key = (str(cand.get("node_type", "symbol")),
|
|
252
|
+
str(cand.get("qualified_name", "")))
|
|
253
|
+
node = out.get(key)
|
|
254
|
+
if node is None:
|
|
255
|
+
node = _Node(name=key[1], node_type=key[0],
|
|
256
|
+
kind=str(cand.get("kind", "")))
|
|
257
|
+
out[key] = node
|
|
258
|
+
node.absorb(cand)
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _pr(tp: int, fp: int, fn: int) -> dict[str, Any]:
|
|
263
|
+
"""precision/recall/f1 — ``None`` quando indefinido (nunca 1.0
|
|
264
|
+
fabricado por denominador vazio)."""
|
|
265
|
+
precision = tp / (tp + fp) if (tp + fp) else None
|
|
266
|
+
recall = tp / (tp + fn) if (tp + fn) else None
|
|
267
|
+
f1 = (2 * precision * recall / (precision + recall)
|
|
268
|
+
if precision and recall else
|
|
269
|
+
(0.0 if precision == 0.0 or recall == 0.0 else None))
|
|
270
|
+
return {"tp": tp, "fp": fp, "fn": fn,
|
|
271
|
+
"precision": precision, "recall": recall, "f1": f1}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _class_diff(
|
|
275
|
+
actual: set[str], expected: Sequence[str], other_expected: set[str],
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
"""Diff de uma classe: ``other_expected`` evita que um item
|
|
278
|
+
esperado na OUTRA classe vire FP aqui — vira class_mismatch."""
|
|
279
|
+
exp = set(expected)
|
|
280
|
+
tp = actual & exp
|
|
281
|
+
fp = actual - exp - other_expected
|
|
282
|
+
fn = exp - actual
|
|
283
|
+
mismatched = sorted((actual - exp) & other_expected)
|
|
284
|
+
rep = _pr(len(tp), len(fp), len(fn))
|
|
285
|
+
rep["missing"] = sorted(fn)
|
|
286
|
+
rep["unexpected"] = sorted(fp)
|
|
287
|
+
rep["class_mismatches"] = mismatched
|
|
288
|
+
return rep
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------
|
|
292
|
+
# invariantes — false-confidence + evidência + truncamento
|
|
293
|
+
# ---------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def false_confidence_leaks(payload: Mapping[str, Any]) -> list[str]:
|
|
297
|
+
"""Invasões de confiança — ORÇAMENTO ZERO.
|
|
298
|
+
|
|
299
|
+
- ``strength=verified`` exige ``fully_verified`` e todos os hops
|
|
300
|
+
``resolved``;
|
|
301
|
+
- ``impact=direct|transitive`` implica ``strength=verified``;
|
|
302
|
+
- ``impact=possible|unknown`` nunca pode ter ``strength=verified``.
|
|
303
|
+
"""
|
|
304
|
+
leaks: list[str] = []
|
|
305
|
+
for cand in payload.get("impacts") or ():
|
|
306
|
+
name = str(cand.get("qualified_name", "?"))
|
|
307
|
+
strength = str(cand.get("strength", "unknown"))
|
|
308
|
+
impact = str(cand.get("impact", "unknown"))
|
|
309
|
+
path = cand.get("path") or {}
|
|
310
|
+
statuses = [str(s) for s in (path.get("resolution_statuses")
|
|
311
|
+
or ())]
|
|
312
|
+
verified_path = bool(path.get("fully_verified")) and all(
|
|
313
|
+
s == "resolved" for s in statuses)
|
|
314
|
+
if strength == "verified" and not verified_path:
|
|
315
|
+
leaks.append(
|
|
316
|
+
f"{name}: strength=verified mas path não provado "
|
|
317
|
+
f"(statuses={statuses}, "
|
|
318
|
+
f"fully_verified={path.get('fully_verified')})")
|
|
319
|
+
if impact in ("direct", "transitive") and strength != "verified":
|
|
320
|
+
leaks.append(
|
|
321
|
+
f"{name}: impact={impact} com strength={strength}")
|
|
322
|
+
if impact in ("possible", "unknown") and strength == "verified":
|
|
323
|
+
leaks.append(
|
|
324
|
+
f"{name}: impact={impact} promovido a verified")
|
|
325
|
+
return leaks
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _snapshot_id_sets(
|
|
329
|
+
conn: sqlite3.Connection, snapshot_id: int,
|
|
330
|
+
) -> dict[str, set[int]]:
|
|
331
|
+
"""Ids reais de cada tabela de evidência do snapshot alvo."""
|
|
332
|
+
return {
|
|
333
|
+
"rel": {int(r[0]) for r in conn.execute(
|
|
334
|
+
"SELECT id FROM relations WHERE snapshot_id = ?",
|
|
335
|
+
(snapshot_id,))},
|
|
336
|
+
"dom": {int(r[0]) for r in conn.execute(
|
|
337
|
+
"SELECT id FROM domain_relations WHERE snapshot_id = ?",
|
|
338
|
+
(snapshot_id,))},
|
|
339
|
+
"sym_occ": {int(r[0]) for r in conn.execute(
|
|
340
|
+
"SELECT id FROM symbol_occurrences WHERE snapshot_id = ?",
|
|
341
|
+
(snapshot_id,))},
|
|
342
|
+
"dom_occ": {int(r[0]) for r in conn.execute(
|
|
343
|
+
"SELECT id FROM domain_entity_occurrences"
|
|
344
|
+
" WHERE snapshot_id = ?",
|
|
345
|
+
(snapshot_id,))},
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _check_path(
|
|
350
|
+
name: str, key: str, cand: Mapping[str, Any],
|
|
351
|
+
) -> tuple[list[str], list[str], list[str]]:
|
|
352
|
+
"""Consistência interna do ``ImpactPath`` — retorna
|
|
353
|
+
``(erros, edge_refs, statuses)``."""
|
|
354
|
+
errors: list[str] = []
|
|
355
|
+
path = cand.get("path")
|
|
356
|
+
if not isinstance(path, Mapping):
|
|
357
|
+
return [f"{name}: sem ImpactPath"], [], []
|
|
358
|
+
edge_refs = [str(r) for r in (path.get("edge_refs") or ())]
|
|
359
|
+
statuses = [str(s) for s in (path.get("resolution_statuses") or ())]
|
|
360
|
+
hops = int(path.get("hops") or 0)
|
|
361
|
+
if hops != len(edge_refs):
|
|
362
|
+
errors.append(
|
|
363
|
+
f"{name}: hops={hops} != {len(edge_refs)} edge_refs")
|
|
364
|
+
if hops != len(statuses):
|
|
365
|
+
errors.append(
|
|
366
|
+
f"{name}: hops={hops} != {len(statuses)} statuses")
|
|
367
|
+
if str(path.get("affected_symbol_key", "")) != key:
|
|
368
|
+
errors.append(
|
|
369
|
+
f"{name}: affected_symbol_key "
|
|
370
|
+
f"{path.get('affected_symbol_key')!r} != symbol_key {key!r}")
|
|
371
|
+
mids = [str(m) for m in (path.get("intermediate_keys") or ())]
|
|
372
|
+
if mids and mids[-1] != key:
|
|
373
|
+
errors.append(
|
|
374
|
+
f"{name}: intermediate_keys[-1]={mids[-1]!r} != "
|
|
375
|
+
f"symbol_key {key!r}")
|
|
376
|
+
if not str(path.get("origin_symbol_key", "")):
|
|
377
|
+
errors.append(f"{name}: origin_symbol_key vazio")
|
|
378
|
+
return errors, edge_refs, statuses
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _check_edge_refs(
|
|
382
|
+
name: str, edge_refs: list[str], ids: Mapping[str, set[int]],
|
|
383
|
+
snapshot_id: int,
|
|
384
|
+
) -> list[str]:
|
|
385
|
+
"""Cada ``dom:N``/``rel:N`` aponta para row real do snapshot."""
|
|
386
|
+
errors: list[str] = []
|
|
387
|
+
for ref in edge_refs:
|
|
388
|
+
prefix, sep, num = ref.partition(":")
|
|
389
|
+
if not sep or not num.isdigit():
|
|
390
|
+
errors.append(f"{name}: edge_ref malformado {ref!r}")
|
|
391
|
+
continue
|
|
392
|
+
table = ids.get(prefix)
|
|
393
|
+
if table is None:
|
|
394
|
+
errors.append(
|
|
395
|
+
f"{name}: edge_ref de tipo desconhecido {ref!r}")
|
|
396
|
+
elif int(num) not in table:
|
|
397
|
+
errors.append(
|
|
398
|
+
f"{name}: edge_ref {ref} não existe no snapshot "
|
|
399
|
+
f"{snapshot_id}")
|
|
400
|
+
return errors
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _check_occurrence(
|
|
404
|
+
name: str, cand: Mapping[str, Any], ids: Mapping[str, set[int]],
|
|
405
|
+
) -> list[str]:
|
|
406
|
+
"""A ocorrência do candidato existe no snapshot alvo (espaço de
|
|
407
|
+
ids certo para ``node_type``)."""
|
|
408
|
+
if str(cand.get("node_type", "symbol")) == "domain":
|
|
409
|
+
occ = cand.get("domain_occurrence_id")
|
|
410
|
+
errors = [] if occ is not None and int(occ) in ids["dom_occ"] \
|
|
411
|
+
else [f"{name}: domain_occurrence_id {occ} fora do "
|
|
412
|
+
"snapshot"]
|
|
413
|
+
if cand.get("occurrence_id") is not None:
|
|
414
|
+
errors.append(
|
|
415
|
+
f"{name}: nó de domínio com occurrence_id de símbolo")
|
|
416
|
+
return errors
|
|
417
|
+
occ = cand.get("occurrence_id")
|
|
418
|
+
if occ is None or int(occ) not in ids["sym_occ"]:
|
|
419
|
+
return [f"{name}: occurrence_id {occ} fora do snapshot"]
|
|
420
|
+
return []
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def validate_evidence(
|
|
424
|
+
payload: Mapping[str, Any], conn: sqlite3.Connection,
|
|
425
|
+
snapshot_id: int,
|
|
426
|
+
) -> list[str]:
|
|
427
|
+
"""Cada candidato carrega prova real: path consistente, edge_refs
|
|
428
|
+
resolvíveis no snapshot alvo, ocorrência existente."""
|
|
429
|
+
errors: list[str] = []
|
|
430
|
+
ids = _snapshot_id_sets(conn, snapshot_id)
|
|
431
|
+
for cand in payload.get("impacts") or ():
|
|
432
|
+
name = str(cand.get("qualified_name", "?"))
|
|
433
|
+
key = str(cand.get("symbol_key", ""))
|
|
434
|
+
errs, edge_refs, _st = _check_path(name, key, cand)
|
|
435
|
+
errors.extend(errs)
|
|
436
|
+
errors.extend(
|
|
437
|
+
_check_edge_refs(name, edge_refs, ids, snapshot_id))
|
|
438
|
+
errors.extend(_check_occurrence(name, cand, ids))
|
|
439
|
+
return errors
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _probe_trunc(
|
|
443
|
+
full_rows: int, capped: Mapping[str, Any], limit: int,
|
|
444
|
+
) -> dict[str, Any]:
|
|
445
|
+
"""Uma sonda de truncamento num ``--max-paths`` dado.
|
|
446
|
+
|
|
447
|
+
Semântica medida do engine (lf-0151): ``truncated`` é a união de
|
|
448
|
+
``truncation_reasons``. ``max-paths`` declara o corte de emissão
|
|
449
|
+
e só pode disparar com a página exatamente no limite (a emissão
|
|
450
|
+
para ali); os demais bounds — fronteira de depth, scan de
|
|
451
|
+
vizinhos, seeds, diff, testes — independem de ``--max-paths``.
|
|
452
|
+
Honesto = envelope coerente (boolean ⟺ razões presentes), razão
|
|
453
|
+
de emissão ⟺ página no limite e ``capped_rows <= limit``.
|
|
454
|
+
"""
|
|
455
|
+
capped_rows = len(capped.get("impacts") or ())
|
|
456
|
+
declared = bool(capped.get("truncated"))
|
|
457
|
+
reasons = {str(r)
|
|
458
|
+
for r in (capped.get("truncation_reasons") or ())}
|
|
459
|
+
emission_cut = capped_rows == limit
|
|
460
|
+
non_emission = bool(reasons - _EMISSION_REASONS)
|
|
461
|
+
expected = emission_cut or non_emission
|
|
462
|
+
consistent = declared == bool(reasons)
|
|
463
|
+
emission_ok = ("max-paths" in reasons) == emission_cut
|
|
464
|
+
return {
|
|
465
|
+
"limit": limit,
|
|
466
|
+
"full_rows": full_rows,
|
|
467
|
+
"capped_rows": capped_rows,
|
|
468
|
+
"truncation_reasons": sorted(reasons),
|
|
469
|
+
"expected_truncated": expected,
|
|
470
|
+
"declared_truncated": declared,
|
|
471
|
+
"within_limit": capped_rows <= limit,
|
|
472
|
+
"envelope_consistent": consistent,
|
|
473
|
+
"emission_reason_ok": emission_ok,
|
|
474
|
+
"honest": (declared == expected and consistent
|
|
475
|
+
and emission_ok and capped_rows <= limit),
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def truncation_check(
|
|
480
|
+
full: Mapping[str, Any],
|
|
481
|
+
at_cap: Mapping[str, Any] | None,
|
|
482
|
+
over_cap: Mapping[str, Any] | None,
|
|
483
|
+
) -> dict[str, Any]:
|
|
484
|
+
"""Honestidade de truncamento — duas sondas por cenário.
|
|
485
|
+
|
|
486
|
+
``at_cap`` (``--max-paths = max(1, full_rows)``): a emissão bate no
|
|
487
|
+
cap — ``truncated=True`` com ``max-paths`` declarado é a resposta
|
|
488
|
+
honesta mesmo quando o conjunto completo tem exatamente
|
|
489
|
+
``full_rows`` linhas (prefixo no limite é declarado, nunca
|
|
490
|
+
"completo" duvidoso).
|
|
491
|
+
|
|
492
|
+
``over_cap`` (``--max-paths = full_rows + slack``): a emissão
|
|
493
|
+
termina naturalmente — ``max-paths`` não pode constar e todas as
|
|
494
|
+
``full_rows`` linhas têm que sair (cap solto não pode cortar).
|
|
495
|
+
``truncated`` ainda pode ser True por outros bounds
|
|
496
|
+
(``truncation_reasons`` não-emissão) — lf-0151: bounds reais são
|
|
497
|
+
declarados, nunca escondidos sob um cap folgado.
|
|
498
|
+
"""
|
|
499
|
+
full_rows = len(full.get("impacts") or ())
|
|
500
|
+
cap = max(1, full_rows)
|
|
501
|
+
probes: dict[str, Any] = {}
|
|
502
|
+
honest = True
|
|
503
|
+
if at_cap is None:
|
|
504
|
+
probes["at_cap"] = {"limit": cap, "error": "sem JSON"}
|
|
505
|
+
honest = False
|
|
506
|
+
else:
|
|
507
|
+
probes["at_cap"] = _probe_trunc(full_rows, at_cap, cap)
|
|
508
|
+
honest = honest and bool(probes["at_cap"]["honest"])
|
|
509
|
+
free = full_rows + TRUNCATION_OVERCAP_SLACK
|
|
510
|
+
if over_cap is None:
|
|
511
|
+
probes["over_cap"] = {"limit": free, "error": "sem JSON"}
|
|
512
|
+
honest = False
|
|
513
|
+
else:
|
|
514
|
+
probes["over_cap"] = _probe_trunc(full_rows, over_cap, free)
|
|
515
|
+
oc = probes["over_cap"]
|
|
516
|
+
# over-cap também exige conjunto completo (cap solto ≠ corte).
|
|
517
|
+
oc["emitted_all"] = oc["capped_rows"] == full_rows
|
|
518
|
+
honest = (honest and bool(oc["honest"])
|
|
519
|
+
and bool(oc["emitted_all"]))
|
|
520
|
+
return {"probes": probes, "honest": honest}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# ---------------------------------------------------------------------
|
|
524
|
+
# avaliação
|
|
525
|
+
# ---------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _seed_entity_count(
|
|
529
|
+
conn: sqlite3.Connection, repository_id: int, seed: str,
|
|
530
|
+
) -> int:
|
|
531
|
+
"""Quantas entidades a ref de seed casa — detecta cenário cujo
|
|
532
|
+
blast vazio vem de seed não resolvida (falso negativo de medição)."""
|
|
533
|
+
from code_atlas.impact.domain_traversal import resolve_entity_refs
|
|
534
|
+
|
|
535
|
+
rows, _how = resolve_entity_refs(conn, repository_id, seed)
|
|
536
|
+
return len(rows)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def evaluate_scenario(
|
|
540
|
+
repo_root: Path, sc: ImpactScenario, conn: sqlite3.Connection,
|
|
541
|
+
*, data_dir: Path, config_dir: Path,
|
|
542
|
+
) -> dict[str, Any]:
|
|
543
|
+
"""Mede um cenário num repositório JÁ indexado (``conn`` aberto no
|
|
544
|
+
``atlas.db`` do workspace temporário)."""
|
|
545
|
+
out: dict[str, Any] = {
|
|
546
|
+
"id": sc.scenario_id,
|
|
547
|
+
"prompt_class": sc.prompt_class,
|
|
548
|
+
"fixture": sc.fixture,
|
|
549
|
+
"seed": sc.seed,
|
|
550
|
+
"task": sc.task,
|
|
551
|
+
"notes": sc.notes,
|
|
552
|
+
"expected": {
|
|
553
|
+
"direct": len(sc.expected_direct),
|
|
554
|
+
"transitive": len(sc.expected_transitive),
|
|
555
|
+
"absent": len(sc.expected_absent),
|
|
556
|
+
},
|
|
557
|
+
}
|
|
558
|
+
fixture = repo_root / sc.fixture
|
|
559
|
+
|
|
560
|
+
run, payload = run_impact(
|
|
561
|
+
fixture, sc.seed, data_dir=data_dir, config_dir=config_dir)
|
|
562
|
+
out["impact_exit"] = run.exit_code
|
|
563
|
+
if payload is None:
|
|
564
|
+
out["error"] = (
|
|
565
|
+
f"impact não retornou JSON (exit {run.exit_code}): "
|
|
566
|
+
f"{run.stderr.strip()[:200]}")
|
|
567
|
+
return out
|
|
568
|
+
sid = payload.get("target_snapshot_id")
|
|
569
|
+
if not isinstance(sid, int):
|
|
570
|
+
out["error"] = "impact sem target_snapshot_id"
|
|
571
|
+
out["diagnostics"] = [
|
|
572
|
+
d.get("code") for d in payload.get("diagnostics", ())
|
|
573
|
+
if isinstance(d, Mapping)]
|
|
574
|
+
return out
|
|
575
|
+
|
|
576
|
+
repo_row = conn.execute(
|
|
577
|
+
"SELECT repository_id FROM snapshots WHERE id = ?",
|
|
578
|
+
(sid,)).fetchone()
|
|
579
|
+
repo_id = int(repo_row[0]) if repo_row else -1
|
|
580
|
+
|
|
581
|
+
nodes = dedup_candidates(payload)
|
|
582
|
+
actual_direct = {k[1] for k, n in nodes.items()
|
|
583
|
+
if n.best_impact == "direct"}
|
|
584
|
+
actual_transitive = {k[1] for k, n in nodes.items()
|
|
585
|
+
if n.best_impact == "transitive"}
|
|
586
|
+
actual_possible = {k[1] for k, n in nodes.items()
|
|
587
|
+
if n.best_impact in ("possible", "unknown")}
|
|
588
|
+
actual_all = {k[1] for k in nodes}
|
|
589
|
+
|
|
590
|
+
exp_direct = set(sc.expected_direct)
|
|
591
|
+
exp_transitive = set(sc.expected_transitive)
|
|
592
|
+
exp_reachable = exp_direct | exp_transitive
|
|
593
|
+
exp_absent = set(sc.expected_absent)
|
|
594
|
+
|
|
595
|
+
direct = _class_diff(actual_direct, sc.expected_direct,
|
|
596
|
+
exp_transitive)
|
|
597
|
+
transitive = _class_diff(actual_transitive, sc.expected_transitive,
|
|
598
|
+
exp_direct)
|
|
599
|
+
verified_actual = actual_direct | actual_transitive
|
|
600
|
+
tp_v = len(verified_actual & exp_reachable)
|
|
601
|
+
fp_v = len(verified_actual - exp_reachable)
|
|
602
|
+
fn_v = len(exp_reachable - verified_actual)
|
|
603
|
+
verified = _pr(tp_v, fp_v, fn_v)
|
|
604
|
+
verified["missing"] = sorted(exp_reachable - verified_actual)
|
|
605
|
+
verified["unexpected"] = sorted(verified_actual - exp_reachable)
|
|
606
|
+
|
|
607
|
+
leaks = false_confidence_leaks(payload)
|
|
608
|
+
ev_errors = validate_evidence(payload, conn, sid)
|
|
609
|
+
absent_violations = sorted(actual_all & exp_absent)
|
|
610
|
+
|
|
611
|
+
# probes de truncamento — re-run no cap exato e com folga.
|
|
612
|
+
full_rows_n = len(payload.get("impacts") or ())
|
|
613
|
+
_run_cap, at_cap = run_impact(
|
|
614
|
+
fixture, sc.seed, data_dir=data_dir, config_dir=config_dir,
|
|
615
|
+
extra_args=("--max-paths", str(max(1, full_rows_n))))
|
|
616
|
+
_run_free, over_cap = run_impact(
|
|
617
|
+
fixture, sc.seed, data_dir=data_dir, config_dir=config_dir,
|
|
618
|
+
extra_args=("--max-paths",
|
|
619
|
+
str(full_rows_n + TRUNCATION_OVERCAP_SLACK)))
|
|
620
|
+
trunc = truncation_check(payload, at_cap, over_cap)
|
|
621
|
+
|
|
622
|
+
out.update({
|
|
623
|
+
"index_independent_check": {
|
|
624
|
+
"seed_entity_rows": _seed_entity_count(conn, repo_id,
|
|
625
|
+
sc.seed),
|
|
626
|
+
"target_snapshot_id": sid,
|
|
627
|
+
},
|
|
628
|
+
"candidates": {
|
|
629
|
+
"rows": len(payload.get("impacts") or ()),
|
|
630
|
+
"unique": len(nodes),
|
|
631
|
+
"by_best_impact": {
|
|
632
|
+
"direct": len(actual_direct),
|
|
633
|
+
"transitive": len(actual_transitive),
|
|
634
|
+
"possible_or_unknown": len(actual_possible),
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
"direct": direct,
|
|
638
|
+
"transitive": transitive,
|
|
639
|
+
"verified": verified,
|
|
640
|
+
"possible_actual": sorted(actual_possible),
|
|
641
|
+
"absent_violations": absent_violations,
|
|
642
|
+
"false_confidence": {
|
|
643
|
+
"leaks": leaks,
|
|
644
|
+
"count": len(leaks),
|
|
645
|
+
"rate": (len(leaks) / len(nodes)) if nodes else 0.0,
|
|
646
|
+
},
|
|
647
|
+
"evidence_errors": ev_errors,
|
|
648
|
+
"unknown_coverage": list(payload.get("unknown_coverage") or ()),
|
|
649
|
+
"truncated": bool(payload.get("truncated")),
|
|
650
|
+
"truncation": trunc,
|
|
651
|
+
"risk": dict(payload.get("risk") or {}),
|
|
652
|
+
"diagnostics": [
|
|
653
|
+
{"code": str(d.get("code")), "message": str(d.get("message"))}
|
|
654
|
+
for d in payload.get("diagnostics", ())
|
|
655
|
+
if isinstance(d, Mapping)
|
|
656
|
+
],
|
|
657
|
+
})
|
|
658
|
+
return out
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@dataclass
|
|
662
|
+
class _IndexedFixture:
|
|
663
|
+
"""Workspace temporário já indexado para uma fixture."""
|
|
664
|
+
|
|
665
|
+
key: tuple[str, tuple[str, ...], bool]
|
|
666
|
+
data_dir: Path
|
|
667
|
+
config_dir: Path
|
|
668
|
+
index_run: CliRun
|
|
669
|
+
conn: sqlite3.Connection = field(init=False)
|
|
670
|
+
|
|
671
|
+
def open(self) -> sqlite3.Connection:
|
|
672
|
+
self.conn = sqlite3.connect(str(self.data_dir / "atlas.db"))
|
|
673
|
+
self.conn.row_factory = sqlite3.Row
|
|
674
|
+
return self.conn
|
|
675
|
+
|
|
676
|
+
def close(self) -> None:
|
|
677
|
+
with contextlib.suppress(sqlite3.Error):
|
|
678
|
+
self.conn.close()
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def evaluate_all(
|
|
682
|
+
repo_root: Path, scenarios: Sequence[ImpactScenario],
|
|
683
|
+
work_dir: Path,
|
|
684
|
+
) -> dict[str, Any]:
|
|
685
|
+
"""Roda todos os cenários: uma indexação real por (fixture,
|
|
686
|
+
adapters, cross) seguida dos impacts de cada cenário nela."""
|
|
687
|
+
results: dict[str, Any] = {"schema": RESULTS_SCHEMA,
|
|
688
|
+
"scenarios": {}}
|
|
689
|
+
indexed: dict[tuple[str, tuple[str, ...], bool],
|
|
690
|
+
_IndexedFixture] = {}
|
|
691
|
+
for sc in scenarios:
|
|
692
|
+
key = (sc.fixture, sc.domains, sc.cross_domain)
|
|
693
|
+
fx = indexed.get(key)
|
|
694
|
+
if fx is None:
|
|
695
|
+
slot = work_dir / f"ws-{len(indexed)}"
|
|
696
|
+
data_dir, config_dir = slot / "data", slot / "cfg"
|
|
697
|
+
data_dir.mkdir(parents=True)
|
|
698
|
+
config_dir.mkdir(parents=True)
|
|
699
|
+
run = index_fixture(
|
|
700
|
+
repo_root / sc.fixture, sc.domains,
|
|
701
|
+
sc.cross_domain,
|
|
702
|
+
data_dir=data_dir, config_dir=config_dir)
|
|
703
|
+
fx = _IndexedFixture(key, data_dir, config_dir, run)
|
|
704
|
+
indexed[key] = fx
|
|
705
|
+
if fx.index_run.exit_code != 0:
|
|
706
|
+
results["scenarios"][sc.scenario_id] = {
|
|
707
|
+
"id": sc.scenario_id,
|
|
708
|
+
"error": f"index exit {fx.index_run.exit_code}: "
|
|
709
|
+
f"{fx.index_run.stderr.strip()[:200]}",
|
|
710
|
+
}
|
|
711
|
+
continue
|
|
712
|
+
conn = fx.open()
|
|
713
|
+
from code_atlas.env_override import patched_env
|
|
714
|
+
with patched_env({
|
|
715
|
+
"CODE_ATLAS_DATA_DIR": str(fx.data_dir),
|
|
716
|
+
"CODE_ATLAS_CONFIG_DIR": str(fx.config_dir),
|
|
717
|
+
}):
|
|
718
|
+
try:
|
|
719
|
+
results["scenarios"][sc.scenario_id] = \
|
|
720
|
+
evaluate_scenario(
|
|
721
|
+
repo_root, sc, conn,
|
|
722
|
+
data_dir=fx.data_dir,
|
|
723
|
+
config_dir=fx.config_dir)
|
|
724
|
+
finally:
|
|
725
|
+
fx.close()
|
|
726
|
+
|
|
727
|
+
# sumário agregado — micro-média sobre cenários com conjunto
|
|
728
|
+
# esperado não-vazio; vazios honestos contam como cobertura, não
|
|
729
|
+
# como recall inventado.
|
|
730
|
+
total_tp = total_fp = total_fn = 0
|
|
731
|
+
leaks_total = evidence_total = absent_total = 0
|
|
732
|
+
truncation_bad: list[str] = []
|
|
733
|
+
empty_but_seed_ok: list[str] = []
|
|
734
|
+
for sid_name, rep in results["scenarios"].items():
|
|
735
|
+
if "verified" not in rep:
|
|
736
|
+
continue
|
|
737
|
+
total_tp += int(rep["verified"]["tp"])
|
|
738
|
+
total_fp += int(rep["verified"]["fp"])
|
|
739
|
+
total_fn += int(rep["verified"]["fn"])
|
|
740
|
+
leaks_total += int(rep["false_confidence"]["count"])
|
|
741
|
+
evidence_total += len(rep["evidence_errors"])
|
|
742
|
+
absent_total += len(rep["absent_violations"])
|
|
743
|
+
if rep["truncation"].get("honest") is False:
|
|
744
|
+
truncation_bad.append(sid_name)
|
|
745
|
+
if rep["candidates"]["unique"] == 0 and rep[
|
|
746
|
+
"index_independent_check"]["seed_entity_rows"] > 0:
|
|
747
|
+
empty_but_seed_ok.append(sid_name)
|
|
748
|
+
verified_micro = _pr(total_tp, total_fp, total_fn)
|
|
749
|
+
results["summary"] = {
|
|
750
|
+
"scenarios": len(results["scenarios"]),
|
|
751
|
+
"verified_micro": verified_micro,
|
|
752
|
+
"false_confidence_leaks": leaks_total,
|
|
753
|
+
"evidence_errors": evidence_total,
|
|
754
|
+
"absent_violations": absent_total,
|
|
755
|
+
"truncation_dishonest": truncation_bad,
|
|
756
|
+
"empty_verified_blasts": empty_but_seed_ok,
|
|
757
|
+
}
|
|
758
|
+
return results
|