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,760 @@
|
|
|
1
|
+
"""Context packs semeados por entidade de domínio (lf-0121, Cycle 7 Ph13).
|
|
2
|
+
|
|
3
|
+
Mesma disciplina do ``ContextEngine``: seleção determinística sob
|
|
4
|
+
budget, nenhum corpo gerado por LLM — todo conteúdo vem de fatos
|
|
5
|
+
persistidos (``domain_entities``, ``domain_entity_occurrences``,
|
|
6
|
+
``domain_relations``, ``evidence``, símbolos). Nada é re-extraído de
|
|
7
|
+
arquivo, nenhum código/job/terraform é executado, nenhuma conexão
|
|
8
|
+
externa é aberta — e todo read é ancorado em ``snapshot_id`` +
|
|
9
|
+
``repository_id`` (isolamento por construção).
|
|
10
|
+
|
|
11
|
+
O pack é um ``ContextPack`` comum (envelope schema v2) com a seção
|
|
12
|
+
aditiva ``domain`` (schema próprio v1): cartão da entidade-semente,
|
|
13
|
+
vizinhos limitados de ``domain_relations`` (top-N por confiança),
|
|
14
|
+
contadores honestos por direção/status — incluindo alvos ``dynamic:``
|
|
15
|
+
e não-resolvidos —, amostras de alvos pendentes, o resumo determinístico
|
|
16
|
+
de ``domains.summarize`` e o contrato de re-consulta dos refs.
|
|
17
|
+
|
|
18
|
+
``expansion_refs`` carregam refs re-consultáveis: ``dom:<identity_key>``
|
|
19
|
+
(entidade de domínio) ou ``sym:<qualified_name>`` (símbolo de código —
|
|
20
|
+
a ponte ``*_symbol_occ_id``). ``resolve_domain_ref`` é a função que um
|
|
21
|
+
handler ``/domains/entities`` chamaria; até a rota existir (wiring
|
|
22
|
+
documentado em ``docs/cycle-7-domain-context-packs.md``, spec lf-0123)
|
|
23
|
+
o contrato está declarado em ``DomainPackSection.requery``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import dataclasses
|
|
29
|
+
import json
|
|
30
|
+
import sqlite3
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import TYPE_CHECKING
|
|
33
|
+
|
|
34
|
+
from code_atlas.context.contracts import (
|
|
35
|
+
DOMAIN_PACK_SCHEMA_VERSION,
|
|
36
|
+
PRIORITY_DEP,
|
|
37
|
+
PRIORITY_DIRECT,
|
|
38
|
+
PRIORITY_ESSENTIAL,
|
|
39
|
+
Budget,
|
|
40
|
+
ContextItem,
|
|
41
|
+
ContextPack,
|
|
42
|
+
DomainEntityCard,
|
|
43
|
+
DomainNeighborRef,
|
|
44
|
+
DomainPackPart,
|
|
45
|
+
DomainPackRequest,
|
|
46
|
+
DomainPackSection,
|
|
47
|
+
ExpansionRef,
|
|
48
|
+
PackEvidence,
|
|
49
|
+
)
|
|
50
|
+
from code_atlas.context.engine import ContextEngine
|
|
51
|
+
from code_atlas.context.tokenizer import TokenCounter
|
|
52
|
+
from code_atlas.contracts.enums import ConfidenceClass, ResolutionStatus
|
|
53
|
+
from code_atlas.domains.contracts import confidence_rank
|
|
54
|
+
from code_atlas.domains.summarize import LineageFact, summarize_lineage
|
|
55
|
+
from code_atlas.storage.store import Store
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
59
|
+
from typing import Any
|
|
60
|
+
|
|
61
|
+
# Prefixos que adapters usam como marcador de honestidade ("o alvo é
|
|
62
|
+
# dinâmico/ambiente — não é candidato estático"). Espelha
|
|
63
|
+
# ``crossresolve._SKIP_PREFIXES`` — mantido local para não importar nome
|
|
64
|
+
# privado; a semântica é a mesma: conta à parte, nunca pendência.
|
|
65
|
+
DYNAMIC_TARGET_PREFIXES = ("dynamic:", "${dynamic}", "environment:")
|
|
66
|
+
|
|
67
|
+
MAX_NEIGHBORS_CAP = 100 # teto duro do top-N
|
|
68
|
+
_SAMPLE_CAP = 10 # amostras de alvos não-resolvidos/ambíguos
|
|
69
|
+
_EVIDENCE_PER_ITEM = 6 # cap de PackEvidence por vizinho
|
|
70
|
+
|
|
71
|
+
# Contrato de re-consulta — declarado DENTRO do pack (spec lf-0121:
|
|
72
|
+
# "exact re-query contract documented in the pack itself").
|
|
73
|
+
DOMAIN_REQUERY_CONTRACT = (
|
|
74
|
+
"expansion_refs[].ref_id forms: 'dom:<identity_key>' resolves to a "
|
|
75
|
+
"domain entity of this repository — re-query contract (route to be "
|
|
76
|
+
"wired by lf-0123): GET /domains/entities"
|
|
77
|
+
"?identity_key=<ref>&repository_root=<root>&snapshot_id=<sid>; "
|
|
78
|
+
"engine-side backing function: "
|
|
79
|
+
"code_atlas.context.domain.resolve_domain_ref"
|
|
80
|
+
"(conn, repository_id, snapshot_id, ref). "
|
|
81
|
+
"'sym:<qualified_name>' resolves to a code-graph symbol of this "
|
|
82
|
+
"snapshot (structural search / ContextExpander, not /domains/entities)."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
_RANKING_DOMAIN = (
|
|
86
|
+
"seed card (essential) > vizinhos de domínio por confiança "
|
|
87
|
+
"(resolved>syntactic>heuristic>inferred, desempate por status e ref); "
|
|
88
|
+
"vizinhos além do top-N viram expansion_refs re-consultáveis")
|
|
89
|
+
|
|
90
|
+
# Status da seção — a palavra honesta do build.
|
|
91
|
+
_STATUS_OK = "ok"
|
|
92
|
+
_STATUS_ABSENT = "absent"
|
|
93
|
+
_STATUS_NOT_FOUND = "seed-not-found"
|
|
94
|
+
_STATUS_AMBIGUOUS = "seed-ambiguous"
|
|
95
|
+
_STATUS_NOT_IN_SNAP = "seed-not-in-snapshot"
|
|
96
|
+
|
|
97
|
+
_STATUS_RANK = {
|
|
98
|
+
str(ResolutionStatus.RESOLVED): 0,
|
|
99
|
+
str(ResolutionStatus.EXTERNAL): 1,
|
|
100
|
+
str(ResolutionStatus.AMBIGUOUS): 2,
|
|
101
|
+
str(ResolutionStatus.UNRESOLVED): 3,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_dynamic(text: str) -> bool:
|
|
106
|
+
return text.strip().startswith(DYNAMIC_TARGET_PREFIXES)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _entity_row(
|
|
110
|
+
conn: sqlite3.Connection, repo_id: int, entity_id: int
|
|
111
|
+
) -> dict[str, Any] | None:
|
|
112
|
+
"""Row de ``domain_entities`` do PRÓPRIO repositório — isolamento."""
|
|
113
|
+
row = conn.execute(
|
|
114
|
+
"SELECT id, domain, kind, identity_key, qualified_name, name,"
|
|
115
|
+
" catalog, namespace, environment, attributes"
|
|
116
|
+
" FROM domain_entities WHERE id = ? AND repository_id = ?",
|
|
117
|
+
(entity_id, repo_id),
|
|
118
|
+
).fetchone()
|
|
119
|
+
return dict(row) if row is not None else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _card(
|
|
123
|
+
entity: Mapping[str, Any],
|
|
124
|
+
occs: Sequence[Mapping[str, Any]],
|
|
125
|
+
repository_id: int,
|
|
126
|
+
snapshot_id: int,
|
|
127
|
+
) -> DomainEntityCard:
|
|
128
|
+
"""Cartão da entidade ancorado na primeira ocorrência do snapshot."""
|
|
129
|
+
first = occs[0]
|
|
130
|
+
return DomainEntityCard(
|
|
131
|
+
entity_id=int(entity["id"]),
|
|
132
|
+
occurrence_id=int(first["occ_id"]),
|
|
133
|
+
occurrence_count=len(occs),
|
|
134
|
+
domain=str(entity["domain"]),
|
|
135
|
+
kind=str(entity["kind"]),
|
|
136
|
+
name=str(entity["name"]),
|
|
137
|
+
qualified_name=str(entity["qualified_name"]),
|
|
138
|
+
identity_key=str(entity["identity_key"]),
|
|
139
|
+
repository_id=repository_id,
|
|
140
|
+
snapshot_id=snapshot_id,
|
|
141
|
+
catalog=str(entity["catalog"]),
|
|
142
|
+
namespace=str(entity["namespace"]),
|
|
143
|
+
environment=str(entity["environment"]),
|
|
144
|
+
source_path=str(first["source_path"] or ""),
|
|
145
|
+
extraction_status=str(first["extraction_status"]),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def resolve_domain_ref(
|
|
150
|
+
conn: sqlite3.Connection,
|
|
151
|
+
repository_id: int,
|
|
152
|
+
snapshot_id: int,
|
|
153
|
+
ref: str,
|
|
154
|
+
) -> DomainEntityCard | None:
|
|
155
|
+
"""Resolve um ref de expansão para o cartão da entidade — a função
|
|
156
|
+
que um handler ``GET /domains/entities`` chamaria.
|
|
157
|
+
|
|
158
|
+
Formas: ``dom:<identity_key>`` (chave lógica canônica) ou
|
|
159
|
+
``entity:<row_id>``. ``None`` para ref malformado, entidade de outro
|
|
160
|
+
repositório ou entidade sem ocorrência neste snapshot — nunca
|
|
161
|
+
fabrica identidade nem atravessa isolamento.
|
|
162
|
+
"""
|
|
163
|
+
store = Store(conn)
|
|
164
|
+
entity_id: int | None = None
|
|
165
|
+
ref = ref.strip()
|
|
166
|
+
if ref.startswith("dom:"):
|
|
167
|
+
entity_id = store.domain_entity_id_for(repository_id, ref)
|
|
168
|
+
elif ref.startswith("entity:") and ref[len("entity:"):].isdigit():
|
|
169
|
+
entity_id = int(ref[len("entity:"):])
|
|
170
|
+
elif ref.isdigit():
|
|
171
|
+
entity_id = int(ref)
|
|
172
|
+
if entity_id is None:
|
|
173
|
+
return None
|
|
174
|
+
entity = _entity_row(conn, repository_id, entity_id)
|
|
175
|
+
if entity is None:
|
|
176
|
+
return None
|
|
177
|
+
occs = [
|
|
178
|
+
dict(r) for r in store.domain_occurrences_snapshot(snapshot_id)
|
|
179
|
+
if int(r["entity_id"]) == entity_id
|
|
180
|
+
]
|
|
181
|
+
if not occs:
|
|
182
|
+
return None
|
|
183
|
+
return _card(entity, occs, repository_id, snapshot_id)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(frozen=True)
|
|
187
|
+
class _SeedHit:
|
|
188
|
+
"""Desfecho da resolução da seed dentro do snapshot."""
|
|
189
|
+
|
|
190
|
+
status: str
|
|
191
|
+
entity: Mapping[str, Any] | None = None
|
|
192
|
+
occs: tuple[Mapping[str, Any], ...] = ()
|
|
193
|
+
candidates: tuple[str, ...] = ()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class _Neighbor:
|
|
198
|
+
"""Arestas deduplicadas por vizinho (entidade ou símbolo)."""
|
|
199
|
+
|
|
200
|
+
ref: str # dom:… | sym:<qname>
|
|
201
|
+
kind: str # "entity" | "symbol"
|
|
202
|
+
entity_kind: str
|
|
203
|
+
display: str
|
|
204
|
+
source_path: str = ""
|
|
205
|
+
directions: set[str] = field(default_factory=set)
|
|
206
|
+
relation_types: set[str] = field(default_factory=set)
|
|
207
|
+
best_rank: int = -1 # confidence_rank máx.
|
|
208
|
+
best_status_rank: int = 99 # _STATUS_RANK da aresta mais forte
|
|
209
|
+
best_status: str = ""
|
|
210
|
+
best_confidence: str = ""
|
|
211
|
+
first_relation_id: int = 1 << 62
|
|
212
|
+
edge_count: int = 0
|
|
213
|
+
evidence_ids: set[int] = field(default_factory=set)
|
|
214
|
+
occ_paths: set[str] = field(default_factory=set)
|
|
215
|
+
|
|
216
|
+
def direction(self) -> str:
|
|
217
|
+
if self.directions == {"incoming"}:
|
|
218
|
+
return "incoming"
|
|
219
|
+
if self.directions == {"outgoing"}:
|
|
220
|
+
return "outgoing"
|
|
221
|
+
return "both"
|
|
222
|
+
|
|
223
|
+
def touch(
|
|
224
|
+
self, *, direction: str, relation_type: str, relation_id: int,
|
|
225
|
+
status: str, confidence: str, evidence: Iterable[int],
|
|
226
|
+
) -> None:
|
|
227
|
+
self.directions.add(direction)
|
|
228
|
+
self.relation_types.add(relation_type)
|
|
229
|
+
self.edge_count += 1
|
|
230
|
+
self.first_relation_id = min(self.first_relation_id, relation_id)
|
|
231
|
+
self.evidence_ids.update(int(e) for e in evidence)
|
|
232
|
+
rank = confidence_rank(ConfidenceClass(confidence))
|
|
233
|
+
srank = _STATUS_RANK.get(status, 9)
|
|
234
|
+
if (rank, -srank) > (self.best_rank, -self.best_status_rank):
|
|
235
|
+
self.best_rank = rank
|
|
236
|
+
self.best_status_rank = srank
|
|
237
|
+
self.best_status = status
|
|
238
|
+
self.best_confidence = confidence
|
|
239
|
+
|
|
240
|
+
def order_key(self) -> tuple[int, int, int, str]:
|
|
241
|
+
"""Top-N por confiança: rank desc → status melhor → id estável."""
|
|
242
|
+
return (-self.best_rank, self.best_status_rank,
|
|
243
|
+
self.first_relation_id, self.ref)
|
|
244
|
+
|
|
245
|
+
def ref_view(self, item_id: str) -> DomainNeighborRef:
|
|
246
|
+
return DomainNeighborRef(
|
|
247
|
+
item_id=item_id, ref=self.ref, kind=self.kind,
|
|
248
|
+
entity_kind=self.entity_kind, display=self.display,
|
|
249
|
+
direction=self.direction(),
|
|
250
|
+
relation_types=tuple(sorted(self.relation_types)),
|
|
251
|
+
resolution_status=self.best_status,
|
|
252
|
+
confidence_class=self.best_confidence,
|
|
253
|
+
edge_count=self.edge_count,
|
|
254
|
+
evidence_ids=tuple(sorted(self.evidence_ids)))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _new_neighbor(
|
|
258
|
+
ref: str, occ_by_identity: Mapping[str, Mapping[str, Any]],
|
|
259
|
+
) -> _Neighbor | None:
|
|
260
|
+
"""Cria o vizinho a partir do ref — ``None`` quando a ocorrência
|
|
261
|
+
não está no snapshot (nunca fabrica identidade)."""
|
|
262
|
+
if ref.startswith("dom:"):
|
|
263
|
+
occ = occ_by_identity.get(ref)
|
|
264
|
+
if occ is None:
|
|
265
|
+
return None
|
|
266
|
+
return _Neighbor(
|
|
267
|
+
ref=ref, kind="entity", entity_kind=str(occ["kind"]),
|
|
268
|
+
display=str(occ["qualified_name"]),
|
|
269
|
+
source_path=str(occ["source_path"] or ""))
|
|
270
|
+
if ref.startswith("sym:") and ref[len("sym:"):]:
|
|
271
|
+
return _Neighbor(ref=ref, kind="symbol", entity_kind="symbol",
|
|
272
|
+
display=ref[len("sym:"):])
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class DomainContextEngine(ContextEngine):
|
|
277
|
+
"""``ContextEngine`` semeado por entidade de domínio.
|
|
278
|
+
|
|
279
|
+
Reusa o maquinário de budget/seleção do engine base (``_budget``,
|
|
280
|
+
``_fit``, ``_enforce_payload_budget``) — a seção ``domain`` entra no
|
|
281
|
+
envelope via ``domain_factory`` e é contada no payload estimado.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
def __init__(
|
|
285
|
+
self, conn: sqlite3.Connection, counter: TokenCounter | None = None
|
|
286
|
+
) -> None:
|
|
287
|
+
super().__init__(conn, counter)
|
|
288
|
+
self._dstore = Store(conn)
|
|
289
|
+
|
|
290
|
+
# -------------------------------------------------------- público
|
|
291
|
+
def build_domain(self, req: DomainPackRequest) -> ContextPack:
|
|
292
|
+
diags: list[str] = []
|
|
293
|
+
repo_id = self._structural.repo_for_root(req.repository_root)
|
|
294
|
+
if repo_id is None:
|
|
295
|
+
return self._absent(
|
|
296
|
+
req, f"repositório não indexado: {req.repository_root}")
|
|
297
|
+
snap, err = self._structural.resolve_snapshot(
|
|
298
|
+
repo_id, req.snapshot_id)
|
|
299
|
+
if snap is None or snap.id is None:
|
|
300
|
+
return self._absent(req, err or "nenhum snapshot READY")
|
|
301
|
+
sid = int(snap.id or 0)
|
|
302
|
+
budget = self._budget(req)
|
|
303
|
+
max_n = max(0, min(req.max_neighbors, MAX_NEIGHBORS_CAP))
|
|
304
|
+
ck = ("domain", sid, req.seed, req.level, budget.limit, max_n)
|
|
305
|
+
cached = self._pack_cache.get(ck)
|
|
306
|
+
if cached is not None:
|
|
307
|
+
return cached
|
|
308
|
+
|
|
309
|
+
occs = self._dstore.domain_occurrences_snapshot(sid)
|
|
310
|
+
if not occs:
|
|
311
|
+
pack = self._assemble(
|
|
312
|
+
req, sid, budget, card=None, neighbors=(), total=0,
|
|
313
|
+
counts=self._zero_counts(), unresolved=(), ambiguous=(),
|
|
314
|
+
diags=[*diags, "domain-absent",
|
|
315
|
+
"snapshot sem ocorrências de domínio — nenhum "
|
|
316
|
+
"adapter de domínio rodou ou snapshot anterior "
|
|
317
|
+
"à v9"],
|
|
318
|
+
status=_STATUS_ABSENT, snap_status=str(snap.status))
|
|
319
|
+
self._cache_put(ck, pack)
|
|
320
|
+
return pack
|
|
321
|
+
|
|
322
|
+
occ_by_entity: dict[int, list[dict[str, Any]]] = {}
|
|
323
|
+
occ_map: dict[int, dict[str, Any]] = {}
|
|
324
|
+
occ_by_identity: dict[str, dict[str, Any]] = {}
|
|
325
|
+
for row in occs:
|
|
326
|
+
o = dict(row)
|
|
327
|
+
occ_map[int(o["occ_id"])] = o
|
|
328
|
+
occ_by_entity.setdefault(int(o["entity_id"]), []).append(o)
|
|
329
|
+
occ_by_identity.setdefault(str(o["identity_key"]), o)
|
|
330
|
+
|
|
331
|
+
hit = self._resolve_seed(repo_id, req.seed, occ_by_entity)
|
|
332
|
+
if hit.status != _STATUS_OK or hit.entity is None:
|
|
333
|
+
diags.append(f"domain-{hit.status}")
|
|
334
|
+
pack = self._assemble(
|
|
335
|
+
req, sid, budget, card=None, neighbors=(), total=0,
|
|
336
|
+
counts=self._zero_counts(), unresolved=(), ambiguous=(),
|
|
337
|
+
diags=diags, status=hit.status,
|
|
338
|
+
candidates=hit.candidates,
|
|
339
|
+
snap_status=str(snap.status))
|
|
340
|
+
self._cache_put(ck, pack)
|
|
341
|
+
return pack
|
|
342
|
+
|
|
343
|
+
entity = hit.entity
|
|
344
|
+
seed_occ_ids = {int(o["occ_id"]) for o in hit.occs}
|
|
345
|
+
seed_ref = str(entity["identity_key"]) # já carrega "dom:"
|
|
346
|
+
card = _card(entity, list(hit.occs), repo_id, sid)
|
|
347
|
+
|
|
348
|
+
edges, counts, unresolved, ambiguous = self._seed_edges(
|
|
349
|
+
sid, seed_occ_ids, diags)
|
|
350
|
+
sym_names = self._symbol_names({
|
|
351
|
+
int(r[c]) for r in edges
|
|
352
|
+
for c in ("from_symbol_occ_id", "to_symbol_occ_id")
|
|
353
|
+
if r[c] is not None})
|
|
354
|
+
neighbors = self._neighbor_views(
|
|
355
|
+
edges, seed_occ_ids, seed_ref, occ_map, occ_by_identity,
|
|
356
|
+
sym_names)
|
|
357
|
+
ordered = sorted(neighbors.values(), key=_Neighbor.order_key)
|
|
358
|
+
shown, beyond = ordered[:max_n], ordered[max_n:]
|
|
359
|
+
if beyond:
|
|
360
|
+
diags.append(
|
|
361
|
+
f"domain-neighbors-capped:{len(shown)}/{len(ordered)}")
|
|
362
|
+
facts = self._lineage_facts(
|
|
363
|
+
edges, seed_occ_ids, seed_ref, str(entity["qualified_name"]),
|
|
364
|
+
occ_map, sym_names, neighbors)
|
|
365
|
+
pack = self._assemble(
|
|
366
|
+
req, sid, budget, card=card,
|
|
367
|
+
neighbors=tuple(shown), total=len(ordered),
|
|
368
|
+
counts=counts, unresolved=unresolved, ambiguous=ambiguous,
|
|
369
|
+
diags=diags, status=_STATUS_OK,
|
|
370
|
+
beyond=tuple(n.ref for n in beyond),
|
|
371
|
+
summary=summarize_lineage(self._subject(entity), facts),
|
|
372
|
+
snap_status=str(snap.status))
|
|
373
|
+
self._cache_put(ck, pack)
|
|
374
|
+
return pack
|
|
375
|
+
|
|
376
|
+
def _cache_put(
|
|
377
|
+
self, ck: tuple[object, ...], pack: ContextPack
|
|
378
|
+
) -> None:
|
|
379
|
+
if len(self._pack_cache) >= 32:
|
|
380
|
+
self._pack_cache.pop(next(iter(self._pack_cache)))
|
|
381
|
+
self._pack_cache[ck] = pack
|
|
382
|
+
|
|
383
|
+
# -------------------------------------------------------- seed
|
|
384
|
+
def _resolve_seed(
|
|
385
|
+
self, repo_id: int, seed: str,
|
|
386
|
+
occ_by_entity: Mapping[int, Sequence[Mapping[str, Any]]],
|
|
387
|
+
) -> _SeedHit:
|
|
388
|
+
"""Formas: ``dom:``/``entity:<id>``/dígitos/``qualified_name``."""
|
|
389
|
+
s = seed.strip()
|
|
390
|
+
if not s:
|
|
391
|
+
return _SeedHit(status=_STATUS_NOT_FOUND)
|
|
392
|
+
entity: dict[str, Any] | None = None
|
|
393
|
+
if s.startswith("dom:"):
|
|
394
|
+
eid = self._dstore.domain_entity_id_for(repo_id, s)
|
|
395
|
+
if eid is not None:
|
|
396
|
+
entity = _entity_row(self._conn, repo_id, eid)
|
|
397
|
+
elif s.startswith("entity:") and s[len("entity:"):].isdigit():
|
|
398
|
+
entity = _entity_row(
|
|
399
|
+
self._conn, repo_id, int(s[len("entity:"):]))
|
|
400
|
+
elif s.isdigit():
|
|
401
|
+
entity = _entity_row(self._conn, repo_id, int(s))
|
|
402
|
+
else:
|
|
403
|
+
rows = self._conn.execute(
|
|
404
|
+
"SELECT id, domain, kind, identity_key, qualified_name,"
|
|
405
|
+
" name, catalog, namespace, environment, attributes"
|
|
406
|
+
" FROM domain_entities"
|
|
407
|
+
" WHERE repository_id = ? AND qualified_name = ?"
|
|
408
|
+
" ORDER BY identity_key",
|
|
409
|
+
(repo_id, s),
|
|
410
|
+
).fetchall()
|
|
411
|
+
if len(rows) > 1:
|
|
412
|
+
return _SeedHit(
|
|
413
|
+
status=_STATUS_AMBIGUOUS,
|
|
414
|
+
candidates=tuple(str(r["identity_key"]) for r in rows))
|
|
415
|
+
if rows:
|
|
416
|
+
entity = dict(rows[0])
|
|
417
|
+
if entity is None:
|
|
418
|
+
return _SeedHit(status=_STATUS_NOT_FOUND)
|
|
419
|
+
occs = occ_by_entity.get(int(entity["id"]), ())
|
|
420
|
+
if not occs:
|
|
421
|
+
return _SeedHit(status=_STATUS_NOT_IN_SNAP, entity=entity)
|
|
422
|
+
return _SeedHit(status=_STATUS_OK, entity=entity, occs=tuple(occs))
|
|
423
|
+
|
|
424
|
+
# -------------------------------------------------------- arestas
|
|
425
|
+
def _seed_edges(
|
|
426
|
+
self, sid: int, seed_occ_ids: set[int], diags: list[str]
|
|
427
|
+
) -> tuple[
|
|
428
|
+
list[dict[str, Any]], dict[str, int],
|
|
429
|
+
tuple[str, ...], tuple[str, ...],
|
|
430
|
+
]:
|
|
431
|
+
"""Arestas do snapshot que tocam a seed + contadores honestos.
|
|
432
|
+
|
|
433
|
+
``unresolved`` conta só pendências reais — alvos ``dynamic:``
|
|
434
|
+
(marcador de adapter) contam em ``dynamic`` à parte."""
|
|
435
|
+
counts = self._zero_counts()
|
|
436
|
+
edges: list[dict[str, Any]] = []
|
|
437
|
+
unresolved: list[str] = []
|
|
438
|
+
ambiguous: list[str] = []
|
|
439
|
+
for r in self._dstore.domain_relations_snapshot(sid):
|
|
440
|
+
fe, te = r["from_entity_occ_id"], r["to_entity_occ_id"]
|
|
441
|
+
out = fe is not None and int(fe) in seed_occ_ids
|
|
442
|
+
inc = te is not None and int(te) in seed_occ_ids
|
|
443
|
+
if not out and not inc:
|
|
444
|
+
continue
|
|
445
|
+
edges.append(dict(r))
|
|
446
|
+
if out:
|
|
447
|
+
counts["outgoing"] += 1
|
|
448
|
+
if inc:
|
|
449
|
+
counts["incoming"] += 1
|
|
450
|
+
self._count_status(
|
|
451
|
+
counts, str(r["resolution_status"]),
|
|
452
|
+
str(r["to_unresolved"] or ""), unresolved, ambiguous)
|
|
453
|
+
for key in ("resolved", "ambiguous", "unresolved", "external",
|
|
454
|
+
"dynamic"):
|
|
455
|
+
if counts[key]:
|
|
456
|
+
diags.append(f"domain-{key}:{counts[key]}")
|
|
457
|
+
return edges, counts, tuple(unresolved), tuple(ambiguous)
|
|
458
|
+
|
|
459
|
+
@staticmethod
|
|
460
|
+
def _count_status(
|
|
461
|
+
counts: dict[str, int], status: str, target: str,
|
|
462
|
+
unresolved: list[str], ambiguous: list[str],
|
|
463
|
+
) -> None:
|
|
464
|
+
"""Contador por status; ``dynamic:`` conta à parte de
|
|
465
|
+
``unresolved`` (marcador de adapter, não pendência)."""
|
|
466
|
+
if status == str(ResolutionStatus.UNRESOLVED):
|
|
467
|
+
if _is_dynamic(target):
|
|
468
|
+
counts["dynamic"] += 1
|
|
469
|
+
else:
|
|
470
|
+
counts["unresolved"] += 1
|
|
471
|
+
if target and len(unresolved) < _SAMPLE_CAP:
|
|
472
|
+
unresolved.append(target)
|
|
473
|
+
elif status == str(ResolutionStatus.AMBIGUOUS):
|
|
474
|
+
counts["ambiguous"] += 1
|
|
475
|
+
if target and len(ambiguous) < _SAMPLE_CAP:
|
|
476
|
+
ambiguous.append(target)
|
|
477
|
+
else:
|
|
478
|
+
counts[status] = counts.get(status, 0) + 1
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def _zero_counts() -> dict[str, int]:
|
|
482
|
+
return {"incoming": 0, "outgoing": 0, "resolved": 0,
|
|
483
|
+
"ambiguous": 0, "unresolved": 0, "external": 0,
|
|
484
|
+
"dynamic": 0}
|
|
485
|
+
|
|
486
|
+
# -------------------------------------------------------- vizinhos
|
|
487
|
+
def _neighbor_views(
|
|
488
|
+
self, edges: list[dict[str, Any]], seed_occ_ids: set[int],
|
|
489
|
+
seed_ref: str,
|
|
490
|
+
occ_map: Mapping[int, Mapping[str, Any]],
|
|
491
|
+
occ_by_identity: Mapping[str, Mapping[str, Any]],
|
|
492
|
+
sym_names: Mapping[int, str],
|
|
493
|
+
) -> dict[str, _Neighbor]:
|
|
494
|
+
"""Agrupa arestas por vizinho — entidade (``dom:``) ou símbolo
|
|
495
|
+
de código (``sym:``). Arestas para ``to_unresolved`` não têm
|
|
496
|
+
vizinho (são contagens/alvos pendentes, nunca itens); arestas
|
|
497
|
+
seed↔seed contam mas não viram vizinho (uma entidade não é
|
|
498
|
+
vizinha de si mesma)."""
|
|
499
|
+
neighbors: dict[str, _Neighbor] = {}
|
|
500
|
+
for r in edges:
|
|
501
|
+
fe, te = r["from_entity_occ_id"], r["to_entity_occ_id"]
|
|
502
|
+
out = fe is not None and int(fe) in seed_occ_ids
|
|
503
|
+
inc = te is not None and int(te) in seed_occ_ids
|
|
504
|
+
rtype = str(r["type"])
|
|
505
|
+
status = str(r["resolution_status"])
|
|
506
|
+
conf = str(r["confidence_class"])
|
|
507
|
+
try:
|
|
508
|
+
evidence = tuple(
|
|
509
|
+
int(e) for e in json.loads(str(r["evidence_ids"])))
|
|
510
|
+
except (ValueError, TypeError):
|
|
511
|
+
evidence = ()
|
|
512
|
+
rel_id = int(r["id"])
|
|
513
|
+
sides: list[tuple[str, str]] = []
|
|
514
|
+
if out:
|
|
515
|
+
sides.append(("outgoing", self._other_ref(
|
|
516
|
+
r["to_entity_occ_id"], r["to_symbol_occ_id"],
|
|
517
|
+
occ_map, sym_names)))
|
|
518
|
+
if inc:
|
|
519
|
+
sides.append(("incoming", self._other_ref(
|
|
520
|
+
r["from_entity_occ_id"], r["from_symbol_occ_id"],
|
|
521
|
+
occ_map, sym_names)))
|
|
522
|
+
for direction, other in sides:
|
|
523
|
+
if not other or other == seed_ref:
|
|
524
|
+
continue
|
|
525
|
+
nb = neighbors.get(other)
|
|
526
|
+
if nb is None:
|
|
527
|
+
nb = _new_neighbor(other, occ_by_identity)
|
|
528
|
+
if nb is None:
|
|
529
|
+
continue # occ fora do snapshot — nunca fabrica
|
|
530
|
+
neighbors[other] = nb
|
|
531
|
+
nb.touch(direction=direction, relation_type=rtype,
|
|
532
|
+
relation_id=rel_id, status=status,
|
|
533
|
+
confidence=conf, evidence=evidence)
|
|
534
|
+
return neighbors
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def _other_ref(
|
|
538
|
+
entity_occ: object, symbol_occ: object,
|
|
539
|
+
occ_map: Mapping[int, Mapping[str, Any]],
|
|
540
|
+
sym_names: Mapping[int, str],
|
|
541
|
+
) -> str:
|
|
542
|
+
"""Ref re-consultável do outro endpoint ("" quando não há)."""
|
|
543
|
+
if isinstance(entity_occ, int):
|
|
544
|
+
occ = occ_map.get(entity_occ)
|
|
545
|
+
if occ is not None:
|
|
546
|
+
return str(occ["identity_key"]) # já carrega "dom:"
|
|
547
|
+
if isinstance(symbol_occ, int):
|
|
548
|
+
qn = sym_names.get(symbol_occ, "")
|
|
549
|
+
if qn:
|
|
550
|
+
return f"sym:{qn}"
|
|
551
|
+
return ""
|
|
552
|
+
|
|
553
|
+
def _symbol_names(self, occ_ids: set[int]) -> dict[int, str]:
|
|
554
|
+
"""``occ_id`` → ``qualified_name`` — batched, sem N+1."""
|
|
555
|
+
if not occ_ids:
|
|
556
|
+
return {}
|
|
557
|
+
rows = self._conn.execute(
|
|
558
|
+
"SELECT so.id, s.qualified_name FROM symbol_occurrences so"
|
|
559
|
+
" JOIN symbols s ON s.id = so.logical_symbol_id"
|
|
560
|
+
" WHERE so.id IN (SELECT value FROM json_each(?))",
|
|
561
|
+
(json.dumps(sorted(occ_ids)),),
|
|
562
|
+
).fetchall()
|
|
563
|
+
return {int(r["id"]): str(r["qualified_name"]) for r in rows}
|
|
564
|
+
|
|
565
|
+
# -------------------------------------------------------- fatos/items
|
|
566
|
+
def _lineage_facts(
|
|
567
|
+
self, edges: list[dict[str, Any]], seed_occ_ids: set[int],
|
|
568
|
+
seed_ref: str, seed_display: str,
|
|
569
|
+
occ_map: Mapping[int, Mapping[str, Any]],
|
|
570
|
+
sym_names: Mapping[int, str],
|
|
571
|
+
neighbors: Mapping[str, _Neighbor],
|
|
572
|
+
) -> list[LineageFact]:
|
|
573
|
+
"""Rows → ``LineageFact`` para o resumo determinístico."""
|
|
574
|
+
facts: list[LineageFact] = []
|
|
575
|
+
for r in edges:
|
|
576
|
+
fe, te = r["from_entity_occ_id"], r["to_entity_occ_id"]
|
|
577
|
+
out = fe is not None and int(fe) in seed_occ_ids
|
|
578
|
+
inc = te is not None and int(te) in seed_occ_ids
|
|
579
|
+
rtype = str(r["type"])
|
|
580
|
+
status = str(r["resolution_status"])
|
|
581
|
+
target = str(r["to_unresolved"] or "")
|
|
582
|
+
dynamic = _is_dynamic(target)
|
|
583
|
+
if out:
|
|
584
|
+
other = self._other_ref(
|
|
585
|
+
r["to_entity_occ_id"], r["to_symbol_occ_id"],
|
|
586
|
+
occ_map, sym_names)
|
|
587
|
+
nb = neighbors.get(other)
|
|
588
|
+
if other == seed_ref:
|
|
589
|
+
label = seed_display
|
|
590
|
+
elif nb is not None:
|
|
591
|
+
label = nb.display
|
|
592
|
+
else:
|
|
593
|
+
label = target or "(no target)"
|
|
594
|
+
facts.append(LineageFact(
|
|
595
|
+
relation_type=rtype, direction="outgoing",
|
|
596
|
+
label=label, resolution_status=status,
|
|
597
|
+
neighbor_kind=nb.entity_kind if nb else "",
|
|
598
|
+
dynamic=dynamic))
|
|
599
|
+
if inc:
|
|
600
|
+
other = self._other_ref(
|
|
601
|
+
r["from_entity_occ_id"], r["from_symbol_occ_id"],
|
|
602
|
+
occ_map, sym_names)
|
|
603
|
+
nb = neighbors.get(other)
|
|
604
|
+
if other == seed_ref:
|
|
605
|
+
label = seed_display
|
|
606
|
+
elif nb is not None:
|
|
607
|
+
label = nb.display
|
|
608
|
+
else:
|
|
609
|
+
label = "(unresolved)"
|
|
610
|
+
facts.append(LineageFact(
|
|
611
|
+
relation_type=rtype, direction="incoming",
|
|
612
|
+
label=label, resolution_status=status,
|
|
613
|
+
neighbor_kind=nb.entity_kind if nb else "",
|
|
614
|
+
dynamic=dynamic))
|
|
615
|
+
return facts
|
|
616
|
+
|
|
617
|
+
@staticmethod
|
|
618
|
+
def _subject(entity: Mapping[str, Any]) -> str:
|
|
619
|
+
"""Rótulo da seed no resumo — espelha ``DomainIdentity.display``."""
|
|
620
|
+
head = ".".join(p for p in (
|
|
621
|
+
str(entity["catalog"]), str(entity["namespace"])) if p)
|
|
622
|
+
qn = str(entity["qualified_name"])
|
|
623
|
+
disp = f"{head}.{qn}" if head else qn
|
|
624
|
+
env = str(entity["environment"])
|
|
625
|
+
if env:
|
|
626
|
+
disp = f"{disp}@{env}"
|
|
627
|
+
return f"{entity['kind']} {disp}"
|
|
628
|
+
|
|
629
|
+
def _items(
|
|
630
|
+
self, card: DomainEntityCard,
|
|
631
|
+
neighbors: tuple[_Neighbor, ...],
|
|
632
|
+
evidence_map: Mapping[int, str],
|
|
633
|
+
) -> list[ContextItem]:
|
|
634
|
+
body = "\n".join(
|
|
635
|
+
ln for ln in (
|
|
636
|
+
f"kind: {card.kind}",
|
|
637
|
+
f"domain: {card.domain}",
|
|
638
|
+
f"identity: {card.identity_key}",
|
|
639
|
+
f"qualified_name: {card.qualified_name}",
|
|
640
|
+
f"catalog: {card.catalog}" if card.catalog else "",
|
|
641
|
+
f"namespace: {card.namespace}" if card.namespace else "",
|
|
642
|
+
f"environment: @{card.environment}"
|
|
643
|
+
if card.environment else "",
|
|
644
|
+
f"source: {card.source_path}" if card.source_path else "",
|
|
645
|
+
f"occurrences: {card.occurrence_count}",
|
|
646
|
+
) if ln)
|
|
647
|
+
items = [ContextItem(
|
|
648
|
+
item_id=f"dcard:{card.identity_key}", kind="domain-entity",
|
|
649
|
+
priority=PRIORITY_ESSENTIAL, title=card.qualified_name,
|
|
650
|
+
body=body, est_tokens=0, path=card.source_path,
|
|
651
|
+
evidence=(PackEvidence(kind="domain-entity",
|
|
652
|
+
ref=card.identity_key),
|
|
653
|
+
PackEvidence(kind="domain-occurrence",
|
|
654
|
+
ref=str(card.occurrence_id))),
|
|
655
|
+
expandable=False)]
|
|
656
|
+
for nb in neighbors:
|
|
657
|
+
ev = [PackEvidence(kind="domain-relation",
|
|
658
|
+
ref=str(nb.first_relation_id))]
|
|
659
|
+
for eid in sorted(nb.evidence_ids)[:_EVIDENCE_PER_ITEM]:
|
|
660
|
+
ev.append(PackEvidence(kind="evidence", ref=str(eid),
|
|
661
|
+
detail=evidence_map.get(eid, "")))
|
|
662
|
+
rel_desc = ", ".join(
|
|
663
|
+
f"{t} ({nb.direction()})"
|
|
664
|
+
for t in sorted(nb.relation_types))
|
|
665
|
+
nbody = "\n".join((
|
|
666
|
+
f"{nb.entity_kind} {nb.display}",
|
|
667
|
+
f"ref: {nb.ref}",
|
|
668
|
+
f"relations: {rel_desc} — {nb.best_status}/"
|
|
669
|
+
f"{nb.best_confidence} x{nb.edge_count}",
|
|
670
|
+
))
|
|
671
|
+
items.append(ContextItem(
|
|
672
|
+
item_id=nb.ref, kind="domain-neighbor",
|
|
673
|
+
priority=PRIORITY_DEP, title=nb.display, body=nbody,
|
|
674
|
+
est_tokens=0,
|
|
675
|
+
symbol_key=(nb.display if nb.kind == "symbol" else ""),
|
|
676
|
+
path=nb.source_path,
|
|
677
|
+
evidence=tuple(ev), expandable=True))
|
|
678
|
+
return items
|
|
679
|
+
|
|
680
|
+
def _evidence_paths(self, ids: set[int]) -> dict[int, str]:
|
|
681
|
+
if not ids:
|
|
682
|
+
return {}
|
|
683
|
+
rows = self._conn.execute(
|
|
684
|
+
"SELECT id, path FROM evidence"
|
|
685
|
+
" WHERE id IN (SELECT value FROM json_each(?))",
|
|
686
|
+
(json.dumps(sorted(ids)),),
|
|
687
|
+
).fetchall()
|
|
688
|
+
return {int(r["id"]): str(r["path"]) for r in rows}
|
|
689
|
+
|
|
690
|
+
# -------------------------------------------------------- assemble
|
|
691
|
+
def _assemble(
|
|
692
|
+
self, req: DomainPackRequest, sid: int, budget: Budget, *,
|
|
693
|
+
card: DomainEntityCard | None, neighbors: tuple[_Neighbor, ...],
|
|
694
|
+
total: int, counts: Mapping[str, int],
|
|
695
|
+
unresolved: tuple[str, ...], ambiguous: tuple[str, ...],
|
|
696
|
+
diags: list[str], status: str, snap_status: str,
|
|
697
|
+
candidates: tuple[str, ...] = (),
|
|
698
|
+
beyond: tuple[str, ...] = (),
|
|
699
|
+
summary: str = "",
|
|
700
|
+
) -> ContextPack:
|
|
701
|
+
"""Empilha itens + fábrica de seção no maquinário de budget."""
|
|
702
|
+
ev_map = self._evidence_paths({
|
|
703
|
+
e for nb in neighbors for e in nb.evidence_ids})
|
|
704
|
+
items = (self._items(card, neighbors, ev_map)
|
|
705
|
+
if card is not None else [])
|
|
706
|
+
items = self._dedup(self._estimate(items))
|
|
707
|
+
items.sort(key=lambda i: (i.priority, i.item_id))
|
|
708
|
+
selected, omitted = self._fit(items, budget.available_items)
|
|
709
|
+
essentials = sum(
|
|
710
|
+
i.est_tokens for i in items if i.priority <= PRIORITY_DIRECT)
|
|
711
|
+
insufficient = len(selected) < len(
|
|
712
|
+
[i for i in items if i.priority <= PRIORITY_DIRECT])
|
|
713
|
+
|
|
714
|
+
neighbor_by_ref = {nb.ref: nb for nb in neighbors}
|
|
715
|
+
|
|
716
|
+
def factory(sel: tuple[ContextItem, ...]) -> DomainPackPart:
|
|
717
|
+
chosen = [
|
|
718
|
+
neighbor_by_ref[i.item_id].ref_view(i.item_id)
|
|
719
|
+
for i in sel if i.item_id in neighbor_by_ref]
|
|
720
|
+
section = DomainPackSection(
|
|
721
|
+
schema_version=DOMAIN_PACK_SCHEMA_VERSION,
|
|
722
|
+
status=status, entity=card,
|
|
723
|
+
neighbors=tuple(chosen),
|
|
724
|
+
neighbors_total=total,
|
|
725
|
+
neighbors_omitted=total - len(chosen),
|
|
726
|
+
counts=dict(counts),
|
|
727
|
+
unresolved_targets=unresolved,
|
|
728
|
+
ambiguous_targets=ambiguous,
|
|
729
|
+
summary=summary,
|
|
730
|
+
requery=DOMAIN_REQUERY_CONTRACT,
|
|
731
|
+
candidates=candidates)
|
|
732
|
+
return DomainPackPart(
|
|
733
|
+
section=section,
|
|
734
|
+
extra_expansion_refs=tuple(
|
|
735
|
+
ExpansionRef(
|
|
736
|
+
ref_id=ref, kind="domain-neighbor",
|
|
737
|
+
reason="além do top-N de vizinhos — "
|
|
738
|
+
"re-consultável via ref")
|
|
739
|
+
for ref in beyond),
|
|
740
|
+
extra_unresolved=(*unresolved, *ambiguous),
|
|
741
|
+
ranking_explanation=_RANKING_DOMAIN)
|
|
742
|
+
|
|
743
|
+
return self._enforce_payload_budget(
|
|
744
|
+
req, sid, budget, selected, omitted, essentials,
|
|
745
|
+
insufficient, diags, snap_status, domain_factory=factory)
|
|
746
|
+
|
|
747
|
+
def _absent(
|
|
748
|
+
self, req: DomainPackRequest, msg: str
|
|
749
|
+
) -> ContextPack:
|
|
750
|
+
"""Falha de resolução (repo/snapshot) — pack vazio honesto."""
|
|
751
|
+
base = self._empty(req, msg)
|
|
752
|
+
return dataclasses.replace(
|
|
753
|
+
base,
|
|
754
|
+
domain=DomainPackSection(
|
|
755
|
+
schema_version=DOMAIN_PACK_SCHEMA_VERSION,
|
|
756
|
+
status=_STATUS_ABSENT, entity=None, neighbors=(),
|
|
757
|
+
neighbors_total=0, neighbors_omitted=0,
|
|
758
|
+
counts=self._zero_counts(), unresolved_targets=(),
|
|
759
|
+
ambiguous_targets=(), summary="", candidates=(),
|
|
760
|
+
requery=DOMAIN_REQUERY_CONTRACT))
|