code-atlas-forge 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. code_atlas/__init__.py +6 -0
  2. code_atlas/__main__.py +6 -0
  3. code_atlas/api/__init__.py +5 -0
  4. code_atlas/api/handlers.py +1514 -0
  5. code_atlas/api/server.py +394 -0
  6. code_atlas/cli.py +2084 -0
  7. code_atlas/config.py +149 -0
  8. code_atlas/context/__init__.py +66 -0
  9. code_atlas/context/contracts.py +277 -0
  10. code_atlas/context/domain.py +760 -0
  11. code_atlas/context/engine.py +442 -0
  12. code_atlas/context/expand.py +133 -0
  13. code_atlas/context/renderers.py +71 -0
  14. code_atlas/context/tokenizer.py +37 -0
  15. code_atlas/contracts/__init__.py +38 -0
  16. code_atlas/contracts/enums.py +67 -0
  17. code_atlas/contracts/models.py +175 -0
  18. code_atlas/contracts/ports.py +70 -0
  19. code_atlas/discovery/__init__.py +25 -0
  20. code_atlas/discovery/gitignore.py +119 -0
  21. code_atlas/discovery/identity.py +52 -0
  22. code_atlas/discovery/models.py +204 -0
  23. code_atlas/discovery/scanner.py +495 -0
  24. code_atlas/domains/__init__.py +107 -0
  25. code_atlas/domains/api/__init__.py +28 -0
  26. code_atlas/domains/api/adapter.py +405 -0
  27. code_atlas/domains/api/common.py +308 -0
  28. code_atlas/domains/api/consumers.py +224 -0
  29. code_atlas/domains/api/emit.py +231 -0
  30. code_atlas/domains/api/openapi.py +398 -0
  31. code_atlas/domains/api/routes.py +582 -0
  32. code_atlas/domains/api/yaml_subset.py +683 -0
  33. code_atlas/domains/aws/__init__.py +28 -0
  34. code_atlas/domains/aws/adapter.py +382 -0
  35. code_atlas/domains/aws/common.py +261 -0
  36. code_atlas/domains/aws/configs.py +1164 -0
  37. code_atlas/domains/aws/emit.py +411 -0
  38. code_atlas/domains/aws/pyfiles.py +2220 -0
  39. code_atlas/domains/aws/sqlbridge.py +448 -0
  40. code_atlas/domains/cicd/__init__.py +50 -0
  41. code_atlas/domains/cicd/adapter.py +762 -0
  42. code_atlas/domains/cicd/classify.py +136 -0
  43. code_atlas/domains/cicd/workflow.py +504 -0
  44. code_atlas/domains/cicd/yamlmini.py +946 -0
  45. code_atlas/domains/cli_surface.py +739 -0
  46. code_atlas/domains/contracts.py +274 -0
  47. code_atlas/domains/crossresolve.py +1208 -0
  48. code_atlas/domains/evidence.py +57 -0
  49. code_atlas/domains/identity.py +149 -0
  50. code_atlas/domains/model.py +202 -0
  51. code_atlas/domains/registry.py +250 -0
  52. code_atlas/domains/resolution.py +65 -0
  53. code_atlas/domains/spark/__init__.py +31 -0
  54. code_atlas/domains/spark/adapter.py +146 -0
  55. code_atlas/domains/spark/emit.py +344 -0
  56. code_atlas/domains/spark/extract.py +108 -0
  57. code_atlas/domains/spark/sqllit.py +192 -0
  58. code_atlas/domains/spark/walk.py +1943 -0
  59. code_atlas/domains/sql/__init__.py +30 -0
  60. code_atlas/domains/sql/adapter.py +382 -0
  61. code_atlas/domains/sql/extract.py +1271 -0
  62. code_atlas/domains/sql/nodes.py +264 -0
  63. code_atlas/domains/sql/parser.py +1510 -0
  64. code_atlas/domains/sql/tokens.py +318 -0
  65. code_atlas/domains/summarize.py +126 -0
  66. code_atlas/domains/terraform/__init__.py +20 -0
  67. code_atlas/domains/terraform/adapter.py +127 -0
  68. code_atlas/domains/terraform/catalog.py +43 -0
  69. code_atlas/domains/terraform/extract.py +1277 -0
  70. code_atlas/domains/terraform/hcl.py +538 -0
  71. code_atlas/domains/terraform/refs.py +181 -0
  72. code_atlas/domains/terraform/tfjson.py +163 -0
  73. code_atlas/domains/terraform/tfvars.py +98 -0
  74. code_atlas/domains/validation.py +120 -0
  75. code_atlas/env_override.py +27 -0
  76. code_atlas/eval/__init__.py +26 -0
  77. code_atlas/eval/domain_eval.py +681 -0
  78. code_atlas/eval/ground_truth.py +688 -0
  79. code_atlas/eval/impact_truth.py +758 -0
  80. code_atlas/eval/metrics.py +178 -0
  81. code_atlas/eval/pack_value.py +610 -0
  82. code_atlas/federation/__init__.py +72 -0
  83. code_atlas/federation/federation.py +676 -0
  84. code_atlas/federation/model.py +355 -0
  85. code_atlas/federation/store.py +194 -0
  86. code_atlas/gitbin.py +48 -0
  87. code_atlas/graph/__init__.py +20 -0
  88. code_atlas/graph/builder.py +378 -0
  89. code_atlas/graph/identity.py +54 -0
  90. code_atlas/graph/queries.py +47 -0
  91. code_atlas/graph/validation.py +92 -0
  92. code_atlas/impact/__init__.py +37 -0
  93. code_atlas/impact/blast_radius.py +171 -0
  94. code_atlas/impact/contract_analyzer.py +104 -0
  95. code_atlas/impact/contracts.py +315 -0
  96. code_atlas/impact/diff_reader.py +142 -0
  97. code_atlas/impact/domain_traversal.py +768 -0
  98. code_atlas/impact/engine.py +392 -0
  99. code_atlas/impact/packs.py +128 -0
  100. code_atlas/impact/report.py +136 -0
  101. code_atlas/impact/revision_binding.py +220 -0
  102. code_atlas/impact/risk_classifier.py +83 -0
  103. code_atlas/impact/snapshot_resolver.py +136 -0
  104. code_atlas/impact/symbol_diff.py +210 -0
  105. code_atlas/impact/test_selector.py +166 -0
  106. code_atlas/indexing/__init__.py +5 -0
  107. code_atlas/indexing/cache.py +396 -0
  108. code_atlas/indexing/change_detector.py +278 -0
  109. code_atlas/indexing/coordinator.py +1032 -0
  110. code_atlas/indexing/dependency_planner.py +268 -0
  111. code_atlas/indexing/persist.py +436 -0
  112. code_atlas/indexing/reuse.py +85 -0
  113. code_atlas/install/__init__.py +7 -0
  114. code_atlas/install/cli.py +168 -0
  115. code_atlas/install/lifecycle.py +286 -0
  116. code_atlas/mcp.py +674 -0
  117. code_atlas/observability/__init__.py +19 -0
  118. code_atlas/observability/logging.py +199 -0
  119. code_atlas/parsers/__init__.py +43 -0
  120. code_atlas/parsers/contracts.py +209 -0
  121. code_atlas/parsers/engine.py +125 -0
  122. code_atlas/parsers/languages/__init__.py +1 -0
  123. code_atlas/parsers/languages/python.py +391 -0
  124. code_atlas/parsers/languages/tsx.py +28 -0
  125. code_atlas/parsers/languages/typescript.py +909 -0
  126. code_atlas/parsers/registry.py +51 -0
  127. code_atlas/parsers/tree_sitter_runtime.py +118 -0
  128. code_atlas/resolvers/__init__.py +33 -0
  129. code_atlas/resolvers/bootstrap.py +246 -0
  130. code_atlas/resolvers/capabilities.py +33 -0
  131. code_atlas/resolvers/contracts.py +133 -0
  132. code_atlas/resolvers/coordinator.py +96 -0
  133. code_atlas/resolvers/enrichment.py +318 -0
  134. code_atlas/resolvers/lsp/__init__.py +26 -0
  135. code_atlas/resolvers/lsp/adapter.py +256 -0
  136. code_atlas/resolvers/lsp/lifecycle.py +251 -0
  137. code_atlas/resolvers/lsp/spawn.py +92 -0
  138. code_atlas/resolvers/lsp/transport.py +240 -0
  139. code_atlas/resolvers/registry.py +33 -0
  140. code_atlas/resolvers/scip/__init__.py +49 -0
  141. code_atlas/resolvers/scip/decoder.py +216 -0
  142. code_atlas/resolvers/scip/importer.py +248 -0
  143. code_atlas/resolvers/scip/loader.py +90 -0
  144. code_atlas/resolvers/scip/mapper.py +271 -0
  145. code_atlas/resolvers/scip/provenance.py +211 -0
  146. code_atlas/resolvers/scip/resolver.py +289 -0
  147. code_atlas/resolvers/scip/validation.py +101 -0
  148. code_atlas/resolvers/syntactic/__init__.py +8 -0
  149. code_atlas/resolvers/syntactic/resolver.py +254 -0
  150. code_atlas/search/__init__.py +32 -0
  151. code_atlas/search/contracts.py +176 -0
  152. code_atlas/search/domain.py +561 -0
  153. code_atlas/search/engine.py +530 -0
  154. code_atlas/search/federated.py +276 -0
  155. code_atlas/search/graph.py +282 -0
  156. code_atlas/search/hybrid.py +151 -0
  157. code_atlas/search/intent.py +48 -0
  158. code_atlas/search/lexical.py +176 -0
  159. code_atlas/search/refs.py +359 -0
  160. code_atlas/search/structural.py +505 -0
  161. code_atlas/search/tokenizer.py +43 -0
  162. code_atlas/search/vector/__init__.py +35 -0
  163. code_atlas/search/vector/contracts.py +76 -0
  164. code_atlas/search/vector/embeddings.py +96 -0
  165. code_atlas/search/vector/index.py +169 -0
  166. code_atlas/search/vector/provider.py +50 -0
  167. code_atlas/storage/__init__.py +14 -0
  168. code_atlas/storage/db.py +47 -0
  169. code_atlas/storage/migrations.py +552 -0
  170. code_atlas/storage/store.py +1045 -0
  171. code_atlas/ui/__init__.py +29 -0
  172. code_atlas/ui/i18n.py +143 -0
  173. code_atlas/ui/kit.py +689 -0
  174. code_atlas/ui/wizard.py +317 -0
  175. code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
  176. code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
  177. code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
  178. code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
  179. code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
  180. code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
  181. code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
code_atlas/config.py ADDED
@@ -0,0 +1,149 @@
1
+ """Resolução de configuração do Code Atlas.
2
+
3
+ Ordem de precedência (Blueprint ADR-013):
4
+
5
+ 1. Argumento explícito (``--root``).
6
+ 2. Variáveis de ambiente ``CODE_ATLAS_*``.
7
+ 3. ``XDG_CONFIG_HOME``/``XDG_DATA_HOME`` (Linux) ou ``%APPDATA%``/``%LOCALAPPDATA%`` (Windows).
8
+ 4. Defaults por plataforma.
9
+
10
+ Nada é escrito dentro do repositório indexado por padrão.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import sys
17
+ import tomllib
18
+ from collections.abc import Mapping
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ ENV_PREFIX = "CODE_ATLAS_"
23
+ ENV_ROOT = "CODE_ATLAS_ROOT"
24
+ ENV_CONFIG_DIR = "CODE_ATLAS_CONFIG_DIR"
25
+ ENV_DATA_DIR = "CODE_ATLAS_DATA_DIR"
26
+
27
+ APP_DIRNAME = "code-atlas"
28
+ CONFIG_FILENAME = "config.toml"
29
+ CONFIG_SCHEMA_VERSION = 1
30
+
31
+ _Env = Mapping[str, str]
32
+
33
+
34
+ def _is_windows() -> bool:
35
+ return sys.platform == "win32"
36
+
37
+
38
+ def config_dir(env: _Env | None = None) -> Path:
39
+ """Diretório de configuração (arquivo ``config.toml``)."""
40
+ e = os.environ if env is None else env
41
+ if override := e.get(ENV_CONFIG_DIR):
42
+ return Path(override).expanduser()
43
+ if _is_windows():
44
+ base = e.get("APPDATA") or e.get("LOCALAPPDATA")
45
+ if base:
46
+ return Path(base) / APP_DIRNAME
47
+ return Path.home() / "AppData" / "Roaming" / APP_DIRNAME
48
+ base = e.get("XDG_CONFIG_HOME")
49
+ return (Path(base) if base else Path.home() / ".config") / APP_DIRNAME
50
+
51
+
52
+ def data_dir(env: _Env | None = None) -> Path:
53
+ """Diretório de dados (bancos SQLite, manifests)."""
54
+ e = os.environ if env is None else env
55
+ if override := e.get(ENV_DATA_DIR):
56
+ return Path(override).expanduser()
57
+ if _is_windows():
58
+ base = e.get("LOCALAPPDATA") or e.get("APPDATA")
59
+ if base:
60
+ return Path(base) / APP_DIRNAME
61
+ return Path.home() / "AppData" / "Local" / APP_DIRNAME
62
+ base = e.get("XDG_DATA_HOME")
63
+ return (Path(base) if base else Path.home() / ".local" / "share") / APP_DIRNAME
64
+
65
+
66
+ def config_file(env: _Env | None = None) -> Path:
67
+ """Caminho completo do ``config.toml``."""
68
+ return config_dir(env) / CONFIG_FILENAME
69
+
70
+
71
+ def resolve_root(arg: str | None = None, env: _Env | None = None) -> Path:
72
+ """Resolve a raiz do workspace: argumento > ``CODE_ATLAS_ROOT`` > cwd."""
73
+ e = os.environ if env is None else env
74
+ candidate = arg or e.get(ENV_ROOT)
75
+ if candidate:
76
+ return Path(candidate).expanduser().resolve()
77
+ return Path.cwd().resolve()
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class WorkspaceConfig:
82
+ """Configuração persistida do workspace (``config.toml``)."""
83
+
84
+ root: Path
85
+ schema_version: int = CONFIG_SCHEMA_VERSION
86
+ # Tabela [resolvers] crua — parsing tipado em resolvers/bootstrap
87
+ # (lf-0027); ausente = sem resolvers opt-in.
88
+ resolvers: Mapping[str, object] | None = None
89
+ # [indexing] retain_snapshots — máximo de snapshots STALE mantidos
90
+ # por repositório (lf-0031). 0/ausente = retenção ilimitada (default:
91
+ # histórico completo preservado para auditoria).
92
+ retain_snapshots: int = 0
93
+
94
+ def to_toml(self) -> str:
95
+ return (
96
+ "# Code Atlas workspace config\n"
97
+ f"schema_version = {self.schema_version}\n"
98
+ "\n"
99
+ "[workspace]\n"
100
+ f'root = "{self.root.as_posix()}"\n'
101
+ )
102
+
103
+ @staticmethod
104
+ def load(path: Path) -> WorkspaceConfig:
105
+ """Carrega o ``config.toml``. Levanta ``ConfigError`` se inválido."""
106
+ try:
107
+ raw = tomllib.loads(path.read_text(encoding="utf-8"))
108
+ except (OSError, tomllib.TOMLDecodeError) as exc:
109
+ raise ConfigError(f"cannot read config {path}: {exc}") from exc
110
+ try:
111
+ root = Path(raw["workspace"]["root"])
112
+ except (KeyError, TypeError) as exc:
113
+ raise ConfigError(f"config {path} missing [workspace].root") from exc
114
+ res = raw.get("resolvers")
115
+ if res is not None and not isinstance(res, dict):
116
+ raise ConfigError(f"config {path}: [resolvers] must be a table")
117
+ idx = raw.get("indexing", {})
118
+ if not isinstance(idx, dict):
119
+ raise ConfigError(f"config {path}: [indexing] must be a table")
120
+ retain = idx.get("retain_snapshots", 0)
121
+ if not isinstance(retain, int) or isinstance(retain, bool) or retain < 0:
122
+ raise ConfigError(
123
+ f"config {path}: [indexing].retain_snapshots must be an "
124
+ "integer >= 0"
125
+ )
126
+ return WorkspaceConfig(
127
+ root=root,
128
+ schema_version=int(raw.get("schema_version", 0)),
129
+ resolvers=res,
130
+ retain_snapshots=retain,
131
+ )
132
+
133
+
134
+ class ConfigError(Exception):
135
+ """Erro de configuração do workspace."""
136
+
137
+
138
+ def write_config(root: Path, env: _Env | None = None, *, force: bool = False) -> Path:
139
+ """Escreve ``config.toml`` e garante o diretório de dados.
140
+
141
+ Levanta ``ConfigError`` se o arquivo já existir e ``force`` for falso.
142
+ """
143
+ cfg_path = config_file(env)
144
+ if cfg_path.exists() and not force:
145
+ raise ConfigError(f"config already exists at {cfg_path} (use --force to overwrite)")
146
+ cfg_path.parent.mkdir(parents=True, exist_ok=True)
147
+ cfg_path.write_text(WorkspaceConfig(root=root).to_toml(), encoding="utf-8")
148
+ data_dir(env).mkdir(parents=True, exist_ok=True)
149
+ return cfg_path
@@ -0,0 +1,66 @@
1
+ """Context pack engine (lf-0040 / Cycle 4 Ph6+7)."""
2
+
3
+ from code_atlas.context.contracts import (
4
+ DOMAIN_PACK_SCHEMA_VERSION,
5
+ PACK_SCHEMA_VERSION,
6
+ Budget,
7
+ ContextItem,
8
+ ContextPack,
9
+ ContextRequest,
10
+ DomainEntityCard,
11
+ DomainNeighborRef,
12
+ DomainPackRequest,
13
+ DomainPackSection,
14
+ ExpansionRef,
15
+ PackDiagnostics,
16
+ PackEvidence,
17
+ PackManifest,
18
+ )
19
+ from code_atlas.context.domain import (
20
+ DOMAIN_REQUERY_CONTRACT,
21
+ DomainContextEngine,
22
+ resolve_domain_ref,
23
+ )
24
+ from code_atlas.context.engine import ContextEngine
25
+ from code_atlas.context.expand import (
26
+ ContextExpander,
27
+ ExpansionResult,
28
+ expansion_to_json,
29
+ )
30
+ from code_atlas.context.renderers import (
31
+ RENDERER_VERSION_JSON,
32
+ RENDERER_VERSION_MD,
33
+ render_json,
34
+ render_markdown,
35
+ )
36
+ from code_atlas.context.tokenizer import HeuristicTokenizer, TokenCounter
37
+
38
+ __all__ = [
39
+ "DOMAIN_PACK_SCHEMA_VERSION",
40
+ "DOMAIN_REQUERY_CONTRACT",
41
+ "PACK_SCHEMA_VERSION",
42
+ "RENDERER_VERSION_JSON",
43
+ "RENDERER_VERSION_MD",
44
+ "Budget",
45
+ "ContextEngine",
46
+ "ContextExpander",
47
+ "ContextItem",
48
+ "ContextPack",
49
+ "ContextRequest",
50
+ "DomainContextEngine",
51
+ "DomainEntityCard",
52
+ "DomainNeighborRef",
53
+ "DomainPackRequest",
54
+ "DomainPackSection",
55
+ "ExpansionRef",
56
+ "ExpansionResult",
57
+ "HeuristicTokenizer",
58
+ "PackDiagnostics",
59
+ "PackEvidence",
60
+ "PackManifest",
61
+ "TokenCounter",
62
+ "expansion_to_json",
63
+ "render_json",
64
+ "render_markdown",
65
+ "resolve_domain_ref",
66
+ ]
@@ -0,0 +1,277 @@
1
+ """Contratos do context pack engine (lf-0040 / Cycle 4 Ph6+7).
2
+
3
+ Determinístico e utilizável sem LLM: seleção por prioridade sobre
4
+ evidências persistidas, nunca geração. Níveis: ``orientation``,
5
+ ``focus``, ``deep`` — outro nível sem reconstruir consulta (pack
6
+ carrega ``expansion_refs``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+ from collections.abc import Callable, Mapping
13
+ from dataclasses import dataclass
14
+ from typing import Literal
15
+
16
+ PACK_SCHEMA_VERSION = 2
17
+
18
+ # lf-0121 (Cycle 7 Ph13): a seção ``domain`` é campo ADITIVO de
19
+ # ``ContextPack`` — o envelope continua schema v2 (consumidores v2
20
+ # ignoram a chave nova); a seção carrega a própria versão.
21
+ DOMAIN_PACK_SCHEMA_VERSION = 1
22
+
23
+ Level = Literal["orientation", "focus", "deep"]
24
+
25
+ # Prioridade de seleção (spec lf-0040): menor = mais prioritário.
26
+ PRIORITY_ESSENTIAL = 0 # evidências essenciais
27
+ PRIORITY_DIRECT = 1 # símbolos diretos (match da task)
28
+ PRIORITY_CONTRACT = 2 # contratos (defs com docstring/signature)
29
+ PRIORITY_DEP = 3 # dependências comprovadas (arestas)
30
+ PRIORITY_SNIPPET = 4 # snippets de código
31
+ PRIORITY_COMPLEMENT = 5 # complementar (vizinhos transitivos)
32
+ PRIORITY_TRANSITIVE = 6 # transitivos menos relevantes
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class ContextRequest:
37
+ """Pedido de pack — snapshot resolvido para o READY atual."""
38
+
39
+ task: str # query livre da tarefa
40
+ repository_root: str = ""
41
+ snapshot_id: int | None = None
42
+ level: Level = "focus"
43
+ budget_tokens: int = 4000
44
+ max_symbols: int = 50
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class DomainPackRequest:
49
+ """Pedido de pack semeado por entidade de domínio (lf-0121).
50
+
51
+ ``seed`` — formas aceitas, nesta ordem:
52
+
53
+ - ``dom:<identity_key>`` — chave lógica canônica da entidade
54
+ (``DomainIdentity.logical_key``), escopada ao repositório;
55
+ - ``entity:<row_id>`` (ou dígitos puros) — row id de
56
+ ``domain_entities`` **daquele** repositório;
57
+ - ``<qualified_name>`` — match exato em ``domain_entities`` do repo;
58
+ múltiplos matches viram ``seed-ambiguous`` honesto com as
59
+ ``identity_key``s candidatas — nunca escolha por sorte.
60
+
61
+ ``max_neighbors`` limita os vizinhos de ``domain_relations``
62
+ (top-N por confiança); os excedentes viram ``expansion_refs``
63
+ re-consultáveis — nunca silenciados.
64
+ """
65
+
66
+ seed: str
67
+ repository_root: str = ""
68
+ snapshot_id: int | None = None
69
+ level: Level = "focus"
70
+ budget_tokens: int = 4000
71
+ max_neighbors: int = 20
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class PackEvidence:
76
+ """Referência a evidência persistida — nunca inventada."""
77
+
78
+ kind: str # "occurrence"|"relation"|"diagnostic"
79
+ ref: str # id/canônico
80
+ detail: str = ""
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class ContextItem:
85
+ """Unidade de contexto com custo estimado e prioridade."""
86
+
87
+ item_id: str # estável p/ expansão/dedup
88
+ kind: str # "symbol"|"file"|"dependency"|"evidence"|"snippet"
89
+ priority: int # PRIORITY_*
90
+ title: str
91
+ body: str # texto indexável/contábil
92
+ est_tokens: int
93
+ symbol_key: str = ""
94
+ path: str = ""
95
+ evidence: tuple[PackEvidence, ...] = ()
96
+ expandable: bool = False # tem refs p/ nível mais profundo
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class ExpansionRef:
101
+ """Referência para disclosure progressivo (outro nível)."""
102
+
103
+ ref_id: str
104
+ kind: str # "symbol"|"file"|"dependency"
105
+ reason: str # por que está disponível p/ expandir
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class Budget:
110
+ """Orçamento declarado + reservas do envelope."""
111
+
112
+ limit: int
113
+ reserved_meta: int
114
+ reserved_evidence: int
115
+ reserved_diagnostics: int
116
+ available_items: int # limit - reservas
117
+ estimator: str # id do estimador (heurístico)
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class PackDiagnostics:
122
+ """Diagnósticos tipados do build."""
123
+
124
+ codes: tuple[str, ...] = ()
125
+ messages: tuple[str, ...] = ()
126
+ insufficient_budget: bool = False
127
+ mandatory_context_omitted: bool = False
128
+ suggested_minimum_budget: int = 0
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class PackManifest:
133
+ """Metadados do pack — auditoria do build."""
134
+
135
+ schema_version: int
136
+ repository: str
137
+ snapshot_id: int
138
+ level: Level
139
+ estimator: str
140
+ tokens_estimated: bool # True — nunca contagem exata
141
+ selected: int
142
+ omitted: int
143
+ truncated: bool
144
+ freshness: str # id do snapshot + estado
145
+ ranking_explanation: str
146
+ tokens_payload: int = 0 # estimativa do payload serializado
147
+
148
+
149
+ # ---------------------------------------------------------------------
150
+ # lf-0121 — seção de domínio (aditiva; envelope segue schema v2)
151
+ # ---------------------------------------------------------------------
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class DomainEntityCard:
156
+ """Cartão da entidade-semente — identidade + ancoragem no snapshot.
157
+
158
+ ``identity_key`` é o ref re-consultável (ver ``requery`` da seção).
159
+ """
160
+
161
+ entity_id: int # row id em domain_entities
162
+ occurrence_id: int # 1ª ocorrência da entidade no snapshot
163
+ occurrence_count: int
164
+ domain: str
165
+ kind: str # DomainEntityKind (data_table, …)
166
+ name: str
167
+ qualified_name: str
168
+ identity_key: str # dom:… lógica — ref estável
169
+ repository_id: int
170
+ snapshot_id: int
171
+ catalog: str = ""
172
+ namespace: str = ""
173
+ environment: str = ""
174
+ source_path: str = ""
175
+ extraction_status: str = ""
176
+
177
+
178
+ @dataclass(frozen=True)
179
+ class DomainNeighborRef:
180
+ """Vizinho de domínio incluído no pack — espelho estruturado do item.
181
+
182
+ ``ref`` é o identificador re-consultável: ``dom:<identity_key>``
183
+ para entidades (mesmo valor do ``ref_id`` de ``expansion_refs``) ou
184
+ ``sym:<qualified_name>`` para símbolo de código (ponte
185
+ domínio↔código via ``*_symbol_occ_id``).
186
+ """
187
+
188
+ item_id: str # == ref — item carrega o ref como id
189
+ ref: str # dom:… | sym:<qualified_name>
190
+ kind: str # "entity" | "symbol"
191
+ entity_kind: str # data_table… | kind do símbolo
192
+ display: str # qualified_name legível
193
+ direction: str # incoming | outgoing | both
194
+ relation_types: tuple[str, ...]
195
+ resolution_status: str # status da aresta de maior confiança
196
+ confidence_class: str # classe de confiança mais forte
197
+ edge_count: int
198
+ evidence_ids: tuple[int, ...] = ()
199
+
200
+
201
+ @dataclass(frozen=True)
202
+ class DomainPackSection:
203
+ """Seção aditiva de domínio no ``ContextPack`` (schema próprio v1).
204
+
205
+ ``status`` é a palavra honesta do build:
206
+
207
+ - ``ok`` — seed resolvida, seção completa;
208
+ - ``absent`` — snapshot sem ocorrências de domínio (nenhuma entidade
209
+ observada — repo indexado sem adapters de domínio ou snapshot
210
+ anterior à v9);
211
+ - ``seed-not-found`` — seed não casa nenhuma entidade do repo;
212
+ - ``seed-ambiguous`` — seed por ``qualified_name`` casou >1
213
+ entidade; ``candidates`` lista as ``identity_key``s;
214
+ - ``seed-not-in-snapshot`` — entidade existe no repo mas sem
215
+ ocorrência neste snapshot.
216
+
217
+ ``counts`` agrega TODAS as arestas do snapshot que tocam a seed
218
+ (não só as incluídas): ``incoming``/``outgoing`` por direção,
219
+ ``resolved``/``ambiguous``/``external`` por status,
220
+ ``unresolved`` exclui alvos com marcador de honestidade
221
+ (``dynamic:``, ``${dynamic}``, ``environment:``) — esses contam em
222
+ ``dynamic`` à parte.
223
+ """
224
+
225
+ schema_version: int
226
+ status: str
227
+ entity: DomainEntityCard | None
228
+ neighbors: tuple[DomainNeighborRef, ...] # incluídos (top-N ∩ budget)
229
+ neighbors_total: int # candidatos dedupados
230
+ neighbors_omitted: int # total - incluídos
231
+ counts: Mapping[str, int]
232
+ unresolved_targets: tuple[str, ...] # amostra de alvos pendentes
233
+ ambiguous_targets: tuple[str, ...] # amostra de alvos ambíguos
234
+ summary: str # domains.summarize — fatos
235
+ requery: str # contrato dos expansion refs
236
+ candidates: tuple[str, ...] = () # seeds ambíguas
237
+
238
+
239
+ @dataclass(frozen=True)
240
+ class DomainPackPart:
241
+ """Saída do ``domain_factory`` — construída DENTRO do loop de
242
+ enforcement de budget, logo tudo aqui entra na estimativa do
243
+ payload serializado."""
244
+
245
+ section: DomainPackSection
246
+ extra_expansion_refs: tuple[ExpansionRef, ...] = () # vizinhos além do top-N
247
+ extra_unresolved: tuple[str, ...] = () # → unresolved_refs
248
+ ranking_explanation: str = "" # override do manifest
249
+
250
+
251
+ DomainSectionFactory = Callable[[tuple[ContextItem, ...]], "DomainPackPart | None"]
252
+ """Monta a seção a partir dos itens SELECIONADOS de uma iteração do
253
+ enforcement — garante que ``section.neighbors`` espelha exatamente o
254
+ que o pack entrega (nunca anuncia vizinho despejado por budget)."""
255
+
256
+
257
+ @dataclass(frozen=True)
258
+ class ContextPack:
259
+ """Envelop versionado — o artefato final do engine.
260
+
261
+ ``domain`` é aditivo (lf-0121): ``None`` para packs de símbolo;
262
+ ``DomainPackSection`` para packs semeados por entidade de domínio.
263
+ """
264
+
265
+ manifest: PackManifest
266
+ budget: Budget
267
+ items: tuple[ContextItem, ...]
268
+ selected_symbols: tuple[str, ...]
269
+ relevant_files: tuple[str, ...]
270
+ dependencies: tuple[PackEvidence, ...]
271
+ unresolved_refs: tuple[str, ...]
272
+ expansion_refs: tuple[ExpansionRef, ...]
273
+ diagnostics: PackDiagnostics
274
+ domain: DomainPackSection | None = None
275
+
276
+ def to_dict(self) -> dict[str, object]:
277
+ return dataclasses.asdict(self)