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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. code_atlas/__init__.py +6 -0
  2. code_atlas/__main__.py +6 -0
  3. code_atlas/api/__init__.py +5 -0
  4. code_atlas/api/handlers.py +1514 -0
  5. code_atlas/api/server.py +394 -0
  6. code_atlas/cli.py +2084 -0
  7. code_atlas/config.py +149 -0
  8. code_atlas/context/__init__.py +66 -0
  9. code_atlas/context/contracts.py +277 -0
  10. code_atlas/context/domain.py +760 -0
  11. code_atlas/context/engine.py +442 -0
  12. code_atlas/context/expand.py +133 -0
  13. code_atlas/context/renderers.py +71 -0
  14. code_atlas/context/tokenizer.py +37 -0
  15. code_atlas/contracts/__init__.py +38 -0
  16. code_atlas/contracts/enums.py +67 -0
  17. code_atlas/contracts/models.py +175 -0
  18. code_atlas/contracts/ports.py +70 -0
  19. code_atlas/discovery/__init__.py +25 -0
  20. code_atlas/discovery/gitignore.py +119 -0
  21. code_atlas/discovery/identity.py +52 -0
  22. code_atlas/discovery/models.py +204 -0
  23. code_atlas/discovery/scanner.py +495 -0
  24. code_atlas/domains/__init__.py +107 -0
  25. code_atlas/domains/api/__init__.py +28 -0
  26. code_atlas/domains/api/adapter.py +405 -0
  27. code_atlas/domains/api/common.py +308 -0
  28. code_atlas/domains/api/consumers.py +224 -0
  29. code_atlas/domains/api/emit.py +231 -0
  30. code_atlas/domains/api/openapi.py +398 -0
  31. code_atlas/domains/api/routes.py +582 -0
  32. code_atlas/domains/api/yaml_subset.py +683 -0
  33. code_atlas/domains/aws/__init__.py +28 -0
  34. code_atlas/domains/aws/adapter.py +382 -0
  35. code_atlas/domains/aws/common.py +261 -0
  36. code_atlas/domains/aws/configs.py +1164 -0
  37. code_atlas/domains/aws/emit.py +411 -0
  38. code_atlas/domains/aws/pyfiles.py +2220 -0
  39. code_atlas/domains/aws/sqlbridge.py +448 -0
  40. code_atlas/domains/cicd/__init__.py +50 -0
  41. code_atlas/domains/cicd/adapter.py +762 -0
  42. code_atlas/domains/cicd/classify.py +136 -0
  43. code_atlas/domains/cicd/workflow.py +504 -0
  44. code_atlas/domains/cicd/yamlmini.py +946 -0
  45. code_atlas/domains/cli_surface.py +739 -0
  46. code_atlas/domains/contracts.py +274 -0
  47. code_atlas/domains/crossresolve.py +1208 -0
  48. code_atlas/domains/evidence.py +57 -0
  49. code_atlas/domains/identity.py +149 -0
  50. code_atlas/domains/model.py +202 -0
  51. code_atlas/domains/registry.py +250 -0
  52. code_atlas/domains/resolution.py +65 -0
  53. code_atlas/domains/spark/__init__.py +31 -0
  54. code_atlas/domains/spark/adapter.py +146 -0
  55. code_atlas/domains/spark/emit.py +344 -0
  56. code_atlas/domains/spark/extract.py +108 -0
  57. code_atlas/domains/spark/sqllit.py +192 -0
  58. code_atlas/domains/spark/walk.py +1943 -0
  59. code_atlas/domains/sql/__init__.py +30 -0
  60. code_atlas/domains/sql/adapter.py +382 -0
  61. code_atlas/domains/sql/extract.py +1271 -0
  62. code_atlas/domains/sql/nodes.py +264 -0
  63. code_atlas/domains/sql/parser.py +1510 -0
  64. code_atlas/domains/sql/tokens.py +318 -0
  65. code_atlas/domains/summarize.py +126 -0
  66. code_atlas/domains/terraform/__init__.py +20 -0
  67. code_atlas/domains/terraform/adapter.py +127 -0
  68. code_atlas/domains/terraform/catalog.py +43 -0
  69. code_atlas/domains/terraform/extract.py +1277 -0
  70. code_atlas/domains/terraform/hcl.py +538 -0
  71. code_atlas/domains/terraform/refs.py +181 -0
  72. code_atlas/domains/terraform/tfjson.py +163 -0
  73. code_atlas/domains/terraform/tfvars.py +98 -0
  74. code_atlas/domains/validation.py +120 -0
  75. code_atlas/env_override.py +27 -0
  76. code_atlas/eval/__init__.py +26 -0
  77. code_atlas/eval/domain_eval.py +681 -0
  78. code_atlas/eval/ground_truth.py +688 -0
  79. code_atlas/eval/impact_truth.py +758 -0
  80. code_atlas/eval/metrics.py +178 -0
  81. code_atlas/eval/pack_value.py +610 -0
  82. code_atlas/federation/__init__.py +72 -0
  83. code_atlas/federation/federation.py +676 -0
  84. code_atlas/federation/model.py +355 -0
  85. code_atlas/federation/store.py +194 -0
  86. code_atlas/gitbin.py +48 -0
  87. code_atlas/graph/__init__.py +20 -0
  88. code_atlas/graph/builder.py +378 -0
  89. code_atlas/graph/identity.py +54 -0
  90. code_atlas/graph/queries.py +47 -0
  91. code_atlas/graph/validation.py +92 -0
  92. code_atlas/impact/__init__.py +37 -0
  93. code_atlas/impact/blast_radius.py +171 -0
  94. code_atlas/impact/contract_analyzer.py +104 -0
  95. code_atlas/impact/contracts.py +315 -0
  96. code_atlas/impact/diff_reader.py +142 -0
  97. code_atlas/impact/domain_traversal.py +768 -0
  98. code_atlas/impact/engine.py +392 -0
  99. code_atlas/impact/packs.py +128 -0
  100. code_atlas/impact/report.py +136 -0
  101. code_atlas/impact/revision_binding.py +220 -0
  102. code_atlas/impact/risk_classifier.py +83 -0
  103. code_atlas/impact/snapshot_resolver.py +136 -0
  104. code_atlas/impact/symbol_diff.py +210 -0
  105. code_atlas/impact/test_selector.py +166 -0
  106. code_atlas/indexing/__init__.py +5 -0
  107. code_atlas/indexing/cache.py +396 -0
  108. code_atlas/indexing/change_detector.py +278 -0
  109. code_atlas/indexing/coordinator.py +1032 -0
  110. code_atlas/indexing/dependency_planner.py +268 -0
  111. code_atlas/indexing/persist.py +436 -0
  112. code_atlas/indexing/reuse.py +85 -0
  113. code_atlas/install/__init__.py +7 -0
  114. code_atlas/install/cli.py +168 -0
  115. code_atlas/install/lifecycle.py +286 -0
  116. code_atlas/mcp.py +674 -0
  117. code_atlas/observability/__init__.py +19 -0
  118. code_atlas/observability/logging.py +199 -0
  119. code_atlas/parsers/__init__.py +43 -0
  120. code_atlas/parsers/contracts.py +209 -0
  121. code_atlas/parsers/engine.py +125 -0
  122. code_atlas/parsers/languages/__init__.py +1 -0
  123. code_atlas/parsers/languages/python.py +391 -0
  124. code_atlas/parsers/languages/tsx.py +28 -0
  125. code_atlas/parsers/languages/typescript.py +909 -0
  126. code_atlas/parsers/registry.py +51 -0
  127. code_atlas/parsers/tree_sitter_runtime.py +118 -0
  128. code_atlas/resolvers/__init__.py +33 -0
  129. code_atlas/resolvers/bootstrap.py +246 -0
  130. code_atlas/resolvers/capabilities.py +33 -0
  131. code_atlas/resolvers/contracts.py +133 -0
  132. code_atlas/resolvers/coordinator.py +96 -0
  133. code_atlas/resolvers/enrichment.py +318 -0
  134. code_atlas/resolvers/lsp/__init__.py +26 -0
  135. code_atlas/resolvers/lsp/adapter.py +256 -0
  136. code_atlas/resolvers/lsp/lifecycle.py +251 -0
  137. code_atlas/resolvers/lsp/spawn.py +92 -0
  138. code_atlas/resolvers/lsp/transport.py +240 -0
  139. code_atlas/resolvers/registry.py +33 -0
  140. code_atlas/resolvers/scip/__init__.py +49 -0
  141. code_atlas/resolvers/scip/decoder.py +216 -0
  142. code_atlas/resolvers/scip/importer.py +248 -0
  143. code_atlas/resolvers/scip/loader.py +90 -0
  144. code_atlas/resolvers/scip/mapper.py +271 -0
  145. code_atlas/resolvers/scip/provenance.py +211 -0
  146. code_atlas/resolvers/scip/resolver.py +289 -0
  147. code_atlas/resolvers/scip/validation.py +101 -0
  148. code_atlas/resolvers/syntactic/__init__.py +8 -0
  149. code_atlas/resolvers/syntactic/resolver.py +254 -0
  150. code_atlas/search/__init__.py +32 -0
  151. code_atlas/search/contracts.py +176 -0
  152. code_atlas/search/domain.py +561 -0
  153. code_atlas/search/engine.py +530 -0
  154. code_atlas/search/federated.py +276 -0
  155. code_atlas/search/graph.py +282 -0
  156. code_atlas/search/hybrid.py +151 -0
  157. code_atlas/search/intent.py +48 -0
  158. code_atlas/search/lexical.py +176 -0
  159. code_atlas/search/refs.py +359 -0
  160. code_atlas/search/structural.py +505 -0
  161. code_atlas/search/tokenizer.py +43 -0
  162. code_atlas/search/vector/__init__.py +35 -0
  163. code_atlas/search/vector/contracts.py +76 -0
  164. code_atlas/search/vector/embeddings.py +96 -0
  165. code_atlas/search/vector/index.py +169 -0
  166. code_atlas/search/vector/provider.py +50 -0
  167. code_atlas/storage/__init__.py +14 -0
  168. code_atlas/storage/db.py +47 -0
  169. code_atlas/storage/migrations.py +552 -0
  170. code_atlas/storage/store.py +1045 -0
  171. code_atlas/ui/__init__.py +29 -0
  172. code_atlas/ui/i18n.py +143 -0
  173. code_atlas/ui/kit.py +689 -0
  174. code_atlas/ui/wizard.py +317 -0
  175. code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
  176. code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
  177. code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
  178. code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
  179. code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
  180. code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
  181. code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,683 @@
1
+ """Parser YAML de subconjunto delimitado — zero dependências (lf-0114).
2
+
3
+ O repositório proíbe PyYAML (deps de runtime = pilha Tree-sitter, ADR).
4
+ Este parser cobre apenas o subconjunto usado por specs OpenAPI/Swagger e
5
+ futuros artefatos CI/CD (lf-0116):
6
+
7
+ - mapeamentos por indentação (``key: value`` / ``key:`` + bloco aninhado);
8
+ - sequências (``- item``), incluindo itens-mapa inline (``- k: v``);
9
+ - escalares: aspas simples/duplas, inteiros, floats, ``true``/``false``,
10
+ ``null``/``~``;
11
+ - flow single-line ``[a, b]`` / ``{k: v}`` (profundidade limitada);
12
+ - escalares de bloco ``|``/``>`` com chomping ``-``/``+``;
13
+ - comentários ``#`` e marcadores de documento ``---``/``...`` (primeiro
14
+ documento apenas — multi-documento vira diagnóstico ``info``).
15
+
16
+ Fora do subconjunto — sempre diagnosticado, nunca silencioso:
17
+
18
+ - âncoras ``&x``, aliases ``*x``, merge keys ``<<:``, tags ``!``/``!!``;
19
+ - escalares plain multi-linha e flow multi-linha;
20
+ - indentação com TAB; sequência ao mesmo nível da chave (``key:`` +
21
+ ``- item`` sem indentação extra) — estilo raro fora de specs;
22
+ - chaves complexas ``? key``.
23
+
24
+ O parser nunca levanta exceção por conteúdo ruim: problemas viram
25
+ ``YamlIssue`` e o parse continua em melhor esforço (extração parcial
26
+ honesta — spec lf-0114).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import re
32
+ from collections.abc import Mapping
33
+ from dataclasses import dataclass
34
+
35
+ YamlScalar = str | int | float | bool | None
36
+ YamlValue = YamlScalar | list["YamlValue"] | dict[str, "YamlValue"]
37
+
38
+ _MAX_DEPTH = 40
39
+ _MAX_FLOW_LEN = 16_384
40
+ _BLOCK_HEADER = re.compile(r"^[|>][+-]?\d*$")
41
+ _INT = re.compile(r"^[-+]?\d+$")
42
+ _FLOAT = re.compile(r"^[-+]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][-+]?\d+)?$")
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class YamlIssue:
47
+ """Problema de parse YAML — diagnóstico honesto, nunca exceção."""
48
+
49
+ code: str # ex.: "yaml-anchor-unsupported"
50
+ severity: str # "info" | "warning" | "error"
51
+ message: str
52
+ line: int = 0 # 1-based; 0 = não localizado
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class YamlDocument:
57
+ """Resultado do parse: valor + issues + mapa key-path → linha.
58
+
59
+ ``key_lines`` indexa a linha (1-based) onde cada chave de mapeamento
60
+ foi declarada. O caminho é a tupla de chaves normalizadas (strings);
61
+ itens de sequência aparecem como índice string (``"0"``, ``"1"``…).
62
+ """
63
+
64
+ value: YamlValue
65
+ issues: tuple[YamlIssue, ...]
66
+ key_lines: Mapping[tuple[str, ...], int]
67
+
68
+
69
+ @dataclass
70
+ class _Line:
71
+ no: int # 1-based
72
+ indent: int
73
+ content: str # comentário removido, rstripped (sem indent)
74
+ ignored: bool # blank/comment-only/consumida por block scalar
75
+
76
+
77
+ class _FlowError(Exception):
78
+ """Falha interna de parse flow — convertida em YamlIssue."""
79
+
80
+
81
+ class _Parser:
82
+ def __init__(self, text: str) -> None:
83
+ self.raw = text.splitlines()
84
+ self.lines: list[_Line] = []
85
+ self.issues: list[YamlIssue] = []
86
+ self.key_lines: dict[tuple[str, ...], int] = {}
87
+ self._tab_reported = False
88
+ self._build_lines()
89
+
90
+ # -- infra ---------------------------------------------------------
91
+
92
+ def _issue(self, code: str, severity: str, message: str, line: int = 0) -> None:
93
+ self.issues.append(YamlIssue(code, severity, message, line))
94
+
95
+ def _strip_comment(self, s: str) -> str:
96
+ """Corta ``#`` iniciando comentário (pos 0 ou após espaço), fora de aspas."""
97
+ in_s = in_d = False
98
+ j = 0
99
+ while j < len(s):
100
+ ch = s[j]
101
+ if in_d:
102
+ if ch == "\\":
103
+ j += 2 # próximo char é escapado
104
+ continue
105
+ if ch == '"':
106
+ in_d = False
107
+ elif in_s:
108
+ if ch == "'":
109
+ in_s = False
110
+ elif ch == '"':
111
+ in_d = True
112
+ elif ch == "'":
113
+ in_s = True
114
+ elif ch == "#" and (j == 0 or s[j - 1] in " \t"):
115
+ return s[:j]
116
+ j += 1
117
+ return s
118
+
119
+ def _build_lines(self) -> None:
120
+ for no, raw in enumerate(self.raw, 1):
121
+ ws = raw[: len(raw) - len(raw.lstrip())]
122
+ if "\t" in ws and not self._tab_reported:
123
+ self._issue(
124
+ "tab-indent", "error",
125
+ "TAB in leading whitespace — line ignored", no,
126
+ )
127
+ self._tab_reported = True
128
+ if "\t" in ws:
129
+ self.lines.append(_Line(no, 0, "", ignored=True))
130
+ continue
131
+ indent = len(ws)
132
+ content = self._strip_comment(raw[indent:]).rstrip()
133
+ self.lines.append(_Line(no, indent, content, ignored=not content))
134
+
135
+ @property
136
+ def _n(self) -> int:
137
+ return len(self.lines)
138
+
139
+ def _eff(self, i: int) -> int:
140
+ """Próximo índice de linha efetiva (não-ignorada) >= i."""
141
+ while i < self._n and self.lines[i].ignored:
142
+ i += 1
143
+ return i
144
+
145
+ @staticmethod
146
+ def _is_doc_marker(content: str) -> bool:
147
+ return content == "---" or content == "..." or content.startswith("--- ")
148
+
149
+ @staticmethod
150
+ def _is_seq_item(content: str) -> bool:
151
+ return content == "-" or content.startswith("- ")
152
+
153
+ # -- entrada ---------------------------------------------------------
154
+
155
+ def parse(self) -> YamlDocument:
156
+ i = self._eff(0)
157
+ if i < self._n and self.lines[i].content == "---":
158
+ i = self._eff(i + 1)
159
+ if i >= self._n:
160
+ value: YamlValue = None
161
+ else:
162
+ value, i = self._parse_node(i, self.lines[i].indent, (), 0)
163
+ j = self._eff(i)
164
+ if j < self._n:
165
+ if self._is_doc_marker(self.lines[j].content):
166
+ self._issue(
167
+ "multi-document", "info",
168
+ "extra YAML documents ignored — first document parsed",
169
+ self.lines[j].no,
170
+ )
171
+ else:
172
+ self._issue(
173
+ "trailing-content", "warning",
174
+ "unparsed trailing content", self.lines[j].no,
175
+ )
176
+ return YamlDocument(
177
+ value=value, issues=tuple(self.issues),
178
+ key_lines=dict(self.key_lines),
179
+ )
180
+
181
+ # -- dispatch --------------------------------------------------------
182
+
183
+ def _parse_node(
184
+ self, i: int, indent: int, path: tuple[str, ...], depth: int
185
+ ) -> tuple[YamlValue, int]:
186
+ if depth > _MAX_DEPTH:
187
+ self._issue("max-depth", "error", "nesting depth exceeded", 0)
188
+ return None, i
189
+ i = self._eff(i)
190
+ if i >= self._n:
191
+ return None, i
192
+ li = self.lines[i]
193
+ if li.indent < indent or self._is_doc_marker(li.content):
194
+ return None, i
195
+ if self._is_seq_item(li.content):
196
+ return self._parse_seq(i, li.indent, path, depth)
197
+ if self._split_key(li.content) is not None:
198
+ return self._parse_map(i, li.indent, path, depth)
199
+ # escalar de linha única
200
+ val = self._scalar_or_tagged(li.content, li.no)
201
+ j = self._eff(i + 1)
202
+ if j < self._n and self.lines[j].indent > li.indent:
203
+ self._issue(
204
+ "multiline-plain-scalar", "warning",
205
+ "continuation of plain scalar ignored",
206
+ self.lines[j].no,
207
+ )
208
+ while j < self._n and self.lines[j].indent > li.indent:
209
+ self.lines[j].ignored = True
210
+ j = self._eff(j + 1)
211
+ return val, j
212
+ return val, i + 1
213
+
214
+ # -- mapeamento ------------------------------------------------------
215
+
216
+ @staticmethod
217
+ def _is_key_sep(content: str, j: int) -> bool:
218
+ return content[j] == ":" and (
219
+ j + 1 == len(content) or content[j + 1] in " \t"
220
+ )
221
+
222
+ def _split_key(self, content: str) -> tuple[str, str] | None:
223
+ """``key: rest`` no primeiro ``:`` seguido de espaço/EOL, depth 0."""
224
+ quote = ""
225
+ esc = False
226
+ depth = 0
227
+ for j, ch in enumerate(content):
228
+ if esc:
229
+ esc = False
230
+ continue
231
+ if quote:
232
+ esc, quote = _quote_step(ch, quote)
233
+ continue
234
+ if ch in "'\"":
235
+ quote = ch
236
+ elif ch in "[{":
237
+ depth += 1
238
+ elif ch in "]}":
239
+ depth = max(0, depth - 1)
240
+ elif self._is_key_sep(content, j):
241
+ return content[:j].strip(), content[j + 1:].strip()
242
+ return None
243
+
244
+ def _norm_key(self, raw: str, line: int) -> str:
245
+ if raw.startswith("? "):
246
+ self._issue(
247
+ "complex-key-unsupported", "warning",
248
+ "explicit complex key unsupported", line,
249
+ )
250
+ raw = raw[2:].strip()
251
+ if raw[:1] in ("&", "!"):
252
+ self._issue(
253
+ "yaml-tag-unsupported", "warning",
254
+ f"key tag/anchor ignored: {raw.split()[0]!r}", line,
255
+ )
256
+ parts = raw.split(None, 1)
257
+ raw = parts[1] if len(parts) > 1 else ""
258
+ if raw[:1] in ("'", '"'):
259
+ val, _ = self._quoted(raw, line)
260
+ return val if val else raw.strip("'\"")
261
+ return raw.strip()
262
+
263
+ def _parse_map(
264
+ self, i: int, indent: int, path: tuple[str, ...], depth: int
265
+ ) -> tuple[dict[str, YamlValue], int]:
266
+ out: dict[str, YamlValue] = {}
267
+ while True:
268
+ i = self._eff(i)
269
+ if i >= self._n:
270
+ break
271
+ li = self.lines[i]
272
+ if self._is_doc_marker(li.content):
273
+ break
274
+ if li.indent < indent:
275
+ break
276
+ if li.indent > indent:
277
+ self._issue(
278
+ "unexpected-indent", "warning",
279
+ "orphan indented line skipped", li.no,
280
+ )
281
+ i += 1
282
+ continue
283
+ if self._is_seq_item(li.content):
284
+ break # volta ao nível da sequência
285
+ kv = self._split_key(li.content)
286
+ if kv is None:
287
+ self._issue(
288
+ "not-a-mapping", "error",
289
+ f"line is not a mapping entry: {li.content[:60]!r}", li.no,
290
+ )
291
+ i += 1
292
+ continue
293
+ key = self._norm_key(kv[0], li.no)
294
+ self.key_lines.setdefault((*path, key), li.no)
295
+ if key == "<<":
296
+ self._issue(
297
+ "yaml-merge-key-unsupported", "warning",
298
+ "merge key '<<' parsed as literal key", li.no,
299
+ )
300
+ if key in out:
301
+ self._issue(
302
+ "duplicate-key", "warning",
303
+ f"duplicate key {key!r} — first value kept", li.no,
304
+ )
305
+ value, i = self._value(kv[1], i, li, (*path, key), depth)
306
+ out.setdefault(key, value)
307
+ return out, i
308
+
309
+ # -- sequência ---------------------------------------------------------
310
+
311
+ def _parse_seq(
312
+ self, i: int, indent: int, path: tuple[str, ...], depth: int
313
+ ) -> tuple[list[YamlValue], int]:
314
+ items: list[YamlValue] = []
315
+ idx = 0
316
+ while True:
317
+ i = self._eff(i)
318
+ if i >= self._n:
319
+ break
320
+ li = self.lines[i]
321
+ if self._is_doc_marker(li.content):
322
+ break
323
+ if li.indent < indent:
324
+ break
325
+ if li.indent > indent:
326
+ self._issue(
327
+ "unexpected-indent", "warning",
328
+ "orphan indented line skipped", li.no,
329
+ )
330
+ i += 1
331
+ continue
332
+ if not self._is_seq_item(li.content):
333
+ break
334
+ after = li.content[1:]
335
+ stripped = after.lstrip(" ")
336
+ item_col = li.indent + 1 + (len(after) - len(stripped))
337
+ key_path = (*path, str(idx))
338
+ if not stripped:
339
+ j = self._eff(i + 1)
340
+ if (
341
+ j < self._n
342
+ and self.lines[j].indent > li.indent
343
+ and not self._is_doc_marker(self.lines[j].content)
344
+ ):
345
+ v, i = self._parse_node(
346
+ j, self.lines[j].indent, key_path, depth + 1
347
+ )
348
+ else:
349
+ v, i = None, i + 1
350
+ items.append(v)
351
+ elif self._split_key(stripped) is not None:
352
+ # item-mapa inline: reescreve a linha como entrada de mapa
353
+ # em ``item_col`` e delega ao parser de mapa.
354
+ li.indent = item_col
355
+ li.content = stripped
356
+ v, i = self._parse_map(i, item_col, key_path, depth + 1)
357
+ items.append(v)
358
+ else:
359
+ v, i = self._value(stripped, i, li, key_path, depth)
360
+ items.append(v)
361
+ idx += 1
362
+ return items, i
363
+
364
+ # -- valores -----------------------------------------------------------
365
+
366
+ def _value(
367
+ self, rest: str, i: int, li: _Line, path: tuple[str, ...], depth: int
368
+ ) -> tuple[YamlValue, int]:
369
+ """Valor de um par ``key: rest`` ou item ``- rest``."""
370
+ if not rest:
371
+ j = self._eff(i + 1)
372
+ if (
373
+ j < self._n
374
+ and self.lines[j].indent > li.indent
375
+ and not self._is_doc_marker(self.lines[j].content)
376
+ ):
377
+ return self._parse_node(
378
+ j, self.lines[j].indent, path, depth + 1
379
+ )
380
+ return None, i + 1
381
+ if _BLOCK_HEADER.match(rest):
382
+ return self._block_scalar(i, li, rest)
383
+ if rest[0] in "[{":
384
+ return self._flow(rest, li.no), i + 1
385
+ return self._scalar_or_tagged(rest, li.no), i + 1
386
+
387
+ def _scalar_or_tagged(self, s: str, line: int) -> YamlValue:
388
+ while s[:1] in ("&", "!"):
389
+ tok, _, rem = s.partition(" ")
390
+ self._issue(
391
+ "yaml-anchor-unsupported" if s[0] == "&"
392
+ else "yaml-tag-unsupported",
393
+ "warning",
394
+ f"{tok!r} ignored — value kept without anchor/tag semantics",
395
+ line,
396
+ )
397
+ s = rem.strip()
398
+ if not s:
399
+ return None
400
+ if s.startswith("*"):
401
+ self._issue(
402
+ "yaml-alias-unsupported", "warning",
403
+ f"alias {s.split()[0]!r} unresolved — value null", line,
404
+ )
405
+ return None
406
+ if s[:1] in "[{":
407
+ return self._flow(s, line)
408
+ if s[:1] in ("|", ">"):
409
+ # header de block scalar fora de posição (ex.: após `-` inline)
410
+ self._issue(
411
+ "misplaced-block-scalar", "warning",
412
+ "block scalar header in unexpected position", line,
413
+ )
414
+ return None
415
+ if s[:1] in ("'", '"'):
416
+ val, end = self._quoted(s, line)
417
+ if s[end:].strip():
418
+ self._issue(
419
+ "trailing-junk", "warning",
420
+ f"trailing content after quoted scalar: {s[end:][:40]!r}",
421
+ line,
422
+ )
423
+ return val
424
+ return self._plain(s)
425
+
426
+ def _quoted(self, s: str, line: int) -> tuple[str, int]:
427
+ """Escalar entre aspas; devolve (valor, índice pós-fechamento)."""
428
+ if s[0] == "'":
429
+ return self._single_quoted(s, line)
430
+ return self._double_quoted(s, line)
431
+
432
+ def _single_quoted(self, s: str, line: int) -> tuple[str, int]:
433
+ out: list[str] = []
434
+ j = 1
435
+ while j < len(s):
436
+ if s[j] == "'":
437
+ if j + 1 < len(s) and s[j + 1] == "'":
438
+ out.append("'")
439
+ j += 2
440
+ continue
441
+ return "".join(out), j + 1
442
+ out.append(s[j])
443
+ j += 1
444
+ self._issue(
445
+ "unterminated-string", "error",
446
+ "unterminated single-quoted scalar", line,
447
+ )
448
+ return "".join(out), len(s)
449
+
450
+ def _dq_escape(self, s: str, j: int) -> tuple[str, int]:
451
+ """Escape ``\\x``/``\\u``/simples em posição j — (char, new j)."""
452
+ esc = {"n": "\n", "t": "\t", "r": "\r", '"': '"', "\\": "\\", "0": "\0"}
453
+ nxt = s[j + 1]
454
+ for mark, size in (("x", 4), ("u", 6)):
455
+ if nxt == mark and j + size - 1 < len(s):
456
+ try:
457
+ return chr(int(s[j + 2:j + size], 16)), j + size
458
+ except ValueError:
459
+ break
460
+ return esc.get(nxt, nxt), j + 2
461
+
462
+ def _double_quoted(self, s: str, line: int) -> tuple[str, int]:
463
+ out: list[str] = []
464
+ j = 1
465
+ while j < len(s):
466
+ ch = s[j]
467
+ if ch == "\\" and j + 1 < len(s):
468
+ piece, j = self._dq_escape(s, j)
469
+ out.append(piece)
470
+ continue
471
+ if ch == '"':
472
+ return "".join(out), j + 1
473
+ out.append(ch)
474
+ j += 1
475
+ self._issue(
476
+ "unterminated-string", "error",
477
+ "unterminated double-quoted scalar", line,
478
+ )
479
+ return "".join(out), len(s)
480
+
481
+ def _plain(self, s: str) -> YamlScalar:
482
+ v = s.strip()
483
+ low = v.lower()
484
+ if low in ("", "~", "null"):
485
+ return None
486
+ if low == "true":
487
+ return True
488
+ if low == "false":
489
+ return False
490
+ if _INT.match(v):
491
+ try:
492
+ return int(v)
493
+ except ValueError:
494
+ return v
495
+ if _FLOAT.match(v):
496
+ try:
497
+ return float(v)
498
+ except ValueError:
499
+ return v
500
+ return v
501
+
502
+ # -- block scalar -----------------------------------------------------
503
+
504
+ def _block_scalar(
505
+ self, i: int, li: _Line, header: str
506
+ ) -> tuple[str, int]:
507
+ chunk, j = self._collect_scalar_lines(i, li.indent)
508
+ while chunk and chunk[-1] == "":
509
+ chunk.pop()
510
+ value = _fold_lines(chunk) if header[0] == ">" else "\n".join(chunk)
511
+ if "-" not in header and value:
512
+ value += "\n"
513
+ return value, j
514
+
515
+ def _collect_scalar_lines(
516
+ self, i: int, parent_indent: int
517
+ ) -> tuple[list[str], int]:
518
+ """Linhas do block scalar (indent > parent); marca-as ignoradas."""
519
+ j = i + 1
520
+ block_indent: int | None = None
521
+ chunk: list[str] = []
522
+ while j < self._n:
523
+ raw = self.raw[j]
524
+ if not raw.strip():
525
+ chunk.append("")
526
+ j += 1
527
+ continue
528
+ ind = len(raw) - len(raw.lstrip(" "))
529
+ if ind <= parent_indent:
530
+ break
531
+ if block_indent is None:
532
+ block_indent = ind
533
+ if ind < block_indent:
534
+ break
535
+ chunk.append(raw[block_indent:])
536
+ self.lines[j].ignored = True
537
+ j += 1
538
+ return chunk, j
539
+
540
+ # -- flow --------------------------------------------------------------
541
+
542
+ def _flow(self, s: str, line: int) -> YamlValue:
543
+ if len(s) > _MAX_FLOW_LEN:
544
+ self._issue(
545
+ "flow-too-large", "error",
546
+ f"flow value exceeds {_MAX_FLOW_LEN} chars", line,
547
+ )
548
+ return None
549
+ try:
550
+ val, pos = self._flow_at(s, 0, 0, line)
551
+ except _FlowError as exc:
552
+ self._issue("flow-parse-error", "error", str(exc), line)
553
+ return None
554
+ if s[pos:].strip():
555
+ self._issue(
556
+ "trailing-junk", "warning",
557
+ f"trailing content after flow value: {s[pos:][:40]!r}", line,
558
+ )
559
+ return val
560
+
561
+ def _flow_at(
562
+ self, s: str, i: int, depth: int, line: int
563
+ ) -> tuple[YamlValue, int]:
564
+ if depth > 8:
565
+ raise _FlowError("flow nesting depth exceeded")
566
+ i = self._flow_ws(s, i)
567
+ if i >= len(s):
568
+ raise _FlowError("unterminated flow value")
569
+ ch = s[i]
570
+ if ch == "[":
571
+ return self._flow_seq(s, i + 1, depth, line)
572
+ if ch == "{":
573
+ return self._flow_map(s, i + 1, depth, line)
574
+ if ch in ("'", '"'):
575
+ val, used = self._quoted(s[i:], line)
576
+ return val, i + used
577
+ if ch == "*":
578
+ self._issue(
579
+ "yaml-alias-unsupported", "warning",
580
+ "alias in flow position — value null", line,
581
+ )
582
+ i += 1
583
+ while i < len(s) and s[i] not in ",]}":
584
+ i += 1
585
+ return None, i
586
+ # escalar plain em flow: até , ] } no mesmo nível
587
+ j = i
588
+ while j < len(s) and s[j] not in ",]}":
589
+ j += 1
590
+ return self._plain(s[i:j]), j
591
+
592
+ @staticmethod
593
+ def _flow_ws(s: str, i: int) -> int:
594
+ while i < len(s) and s[i] in " \t":
595
+ i += 1
596
+ return i
597
+
598
+ def _flow_seq(
599
+ self, s: str, i: int, depth: int, line: int
600
+ ) -> tuple[list[YamlValue], int]:
601
+ out: list[YamlValue] = []
602
+ i = self._flow_ws(s, i)
603
+ if i < len(s) and s[i] == "]":
604
+ return out, i + 1
605
+ while True:
606
+ v, i = self._flow_at(s, i, depth + 1, line)
607
+ out.append(v)
608
+ i = self._flow_ws(s, i)
609
+ if i >= len(s):
610
+ raise _FlowError("unterminated flow sequence")
611
+ if s[i] == "]":
612
+ return out, i + 1
613
+ if s[i] != ",":
614
+ raise _FlowError(f"expected ',' in flow seq, got {s[i]!r}")
615
+ i = self._flow_ws(s, i + 1)
616
+ if i < len(s) and s[i] == "]":
617
+ return out, i + 1
618
+
619
+ def _flow_map(
620
+ self, s: str, i: int, depth: int, line: int
621
+ ) -> tuple[dict[str, YamlValue], int]:
622
+ out: dict[str, YamlValue] = {}
623
+ i = self._flow_ws(s, i)
624
+ if i < len(s) and s[i] == "}":
625
+ return out, i + 1
626
+ while True:
627
+ i = self._flow_ws(s, i)
628
+ if i < len(s) and s[i] in ("'", '"'):
629
+ key_v, used = self._quoted(s[i:], line)
630
+ key = str(key_v)
631
+ i += used
632
+ else:
633
+ j = i
634
+ while j < len(s) and s[j] not in ":,}":
635
+ j += 1
636
+ key = s[i:j].strip()
637
+ i = j
638
+ i = self._flow_ws(s, i)
639
+ if i >= len(s) or s[i] != ":":
640
+ raise _FlowError("expected ':' in flow map")
641
+ v, i = self._flow_at(s, i + 1, depth + 1, line)
642
+ out[key] = v
643
+ i = self._flow_ws(s, i)
644
+ if i >= len(s):
645
+ raise _FlowError("unterminated flow map")
646
+ if s[i] == "}":
647
+ return out, i + 1
648
+ if s[i] != ",":
649
+ raise _FlowError(f"expected ',' in flow map, got {s[i]!r}")
650
+ i = self._flow_ws(s, i + 1)
651
+ if i < len(s) and s[i] == "}":
652
+ return out, i + 1
653
+
654
+
655
+ def _quote_step(ch: str, quote: str) -> tuple[bool, str]:
656
+ """Passo de scan dentro de aspas → ``(escapou?, quote_atual)``."""
657
+ if ch == "\\":
658
+ return True, quote
659
+ if ch == quote:
660
+ return False, ""
661
+ return False, quote
662
+
663
+
664
+ def _fold_lines(chunk: list[str]) -> str:
665
+ """Folding ``>``: linhas consecutivas viram frase, blanks separam."""
666
+ paras: list[str] = []
667
+ cur: list[str] = []
668
+ for line in chunk:
669
+ if line == "":
670
+ if cur:
671
+ paras.append(" ".join(cur))
672
+ cur = []
673
+ paras.append("")
674
+ else:
675
+ cur.append(line.strip())
676
+ if cur:
677
+ paras.append(" ".join(cur))
678
+ return "\n".join(paras)
679
+
680
+
681
+ def parse_yaml_subset(text: str) -> YamlDocument:
682
+ """Parseia o subconjunto YAML delimitado; nunca levanta."""
683
+ return _Parser(text).parse()