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,278 @@
|
|
|
1
|
+
"""Detector de mudanças manifesto-vs-snapshot (lf-0019).
|
|
2
|
+
|
|
3
|
+
Compara o scan atual com o inventário do último snapshot READY e
|
|
4
|
+
produz um ``ChangeSet`` tipado — insumo do ``InvalidationPlanner``.
|
|
5
|
+
|
|
6
|
+
Regras inegociáveis:
|
|
7
|
+
|
|
8
|
+
- ``RENAMED`` **somente** quando um path deletado e um adicionado têm
|
|
9
|
+
sha256 idêntico — evidência de conteúdo, nunca heurística de nome.
|
|
10
|
+
- Bump de versão de parser/extrator/grammar reclassifica os arquivos da
|
|
11
|
+
linguagem afetada como ``MODIFIED``.
|
|
12
|
+
- Rename-of-case está reservado para lf-0021 e não é tratado aqui.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
|
|
21
|
+
from code_atlas.discovery.models import ScanResult, SourceFile
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChangeKind(StrEnum):
|
|
25
|
+
ADDED = "added"
|
|
26
|
+
MODIFIED = "modified"
|
|
27
|
+
DELETED = "deleted"
|
|
28
|
+
RENAMED = "renamed"
|
|
29
|
+
UNCHANGED = "unchanged"
|
|
30
|
+
ANALYZER_CHANGED = "analyzer-changed"
|
|
31
|
+
RESOLVER_CHANGED = "resolver-changed"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class FileChange:
|
|
36
|
+
"""Mudança tipada de um arquivo, com razão auditável."""
|
|
37
|
+
|
|
38
|
+
path: str
|
|
39
|
+
kind: ChangeKind
|
|
40
|
+
reason: str
|
|
41
|
+
old_path: str = "" # preenchido para RENAMED (path deletado)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ChangeSet:
|
|
46
|
+
"""Resultado do diff manifesto ↔ snapshot anterior."""
|
|
47
|
+
|
|
48
|
+
changes: tuple[FileChange, ...]
|
|
49
|
+
analyzer_changes: tuple[str, ...] = () # chaves de versão alteradas
|
|
50
|
+
resolver_changes: tuple[str, ...] = ()
|
|
51
|
+
|
|
52
|
+
def by_kind(self, kind: ChangeKind) -> tuple[FileChange, ...]:
|
|
53
|
+
return tuple(c for c in self.changes if c.kind == kind)
|
|
54
|
+
|
|
55
|
+
def for_path(self, path: str) -> FileChange | None:
|
|
56
|
+
for c in self.changes:
|
|
57
|
+
if c.path == path:
|
|
58
|
+
return c
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def counts(self) -> dict[str, int]:
|
|
62
|
+
out: dict[str, int] = {}
|
|
63
|
+
for c in self.changes:
|
|
64
|
+
out[str(c.kind)] = out.get(str(c.kind), 0) + 1
|
|
65
|
+
return dict(sorted(out.items()))
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict[str, object]:
|
|
68
|
+
return {
|
|
69
|
+
"changes": [
|
|
70
|
+
{
|
|
71
|
+
"path": c.path,
|
|
72
|
+
"kind": str(c.kind),
|
|
73
|
+
"reason": c.reason,
|
|
74
|
+
**({"old_path": c.old_path} if c.old_path else {}),
|
|
75
|
+
}
|
|
76
|
+
for c in sorted(self.changes, key=lambda x: (x.path, str(x.kind)))
|
|
77
|
+
],
|
|
78
|
+
"analyzer_changes": sorted(self.analyzer_changes),
|
|
79
|
+
"resolver_changes": sorted(self.resolver_changes),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def to_json(self) -> str:
|
|
83
|
+
return json.dumps(self.to_dict(), sort_keys=True)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class _PriorFile:
|
|
88
|
+
path: str
|
|
89
|
+
language: str
|
|
90
|
+
sha256: str | None
|
|
91
|
+
parse_status: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _affected_languages(
|
|
95
|
+
prior_versions: dict[str, str], current_versions: dict[str, str]
|
|
96
|
+
) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
97
|
+
"""Chaves de versão alteradas → (analyzer_keys, resolver_keys).
|
|
98
|
+
|
|
99
|
+
Versões ``parser:<lang>`` classificam arquivos daquela linguagem;
|
|
100
|
+
``resolver:<id>`` afeta relações de todas as linguagens suportadas.
|
|
101
|
+
"""
|
|
102
|
+
analyzer: list[str] = []
|
|
103
|
+
resolver: list[str] = []
|
|
104
|
+
for key in sorted(set(prior_versions) | set(current_versions)):
|
|
105
|
+
if prior_versions.get(key) == current_versions.get(key):
|
|
106
|
+
continue
|
|
107
|
+
if key.startswith("resolver:"):
|
|
108
|
+
resolver.append(key)
|
|
109
|
+
else:
|
|
110
|
+
analyzer.append(key)
|
|
111
|
+
return tuple(analyzer), tuple(resolver)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def detect_changes(
|
|
115
|
+
scan: ScanResult,
|
|
116
|
+
prior_inventory: list[dict[str, object]],
|
|
117
|
+
prior_versions: dict[str, str],
|
|
118
|
+
current_versions: dict[str, str],
|
|
119
|
+
) -> ChangeSet:
|
|
120
|
+
"""Diff do manifesto atual contra o inventário do snapshot anterior.
|
|
121
|
+
|
|
122
|
+
``prior_inventory`` vem de ``Store.snapshot_inventory`` — dicts com
|
|
123
|
+
``path``/``language``/``sha256``/``parse_status``.
|
|
124
|
+
"""
|
|
125
|
+
prior = {
|
|
126
|
+
str(r["path"]): _PriorFile(
|
|
127
|
+
path=str(r["path"]),
|
|
128
|
+
language=str(r["language"]),
|
|
129
|
+
sha256=r["sha256"] if isinstance(r["sha256"], str) else None,
|
|
130
|
+
parse_status=str(r.get("parse_status", "unparsed")),
|
|
131
|
+
)
|
|
132
|
+
for r in prior_inventory
|
|
133
|
+
}
|
|
134
|
+
current = {f.path: f for f in scan.files}
|
|
135
|
+
|
|
136
|
+
analyzer_keys, resolver_keys = _affected_languages(
|
|
137
|
+
prior_versions, current_versions
|
|
138
|
+
)
|
|
139
|
+
bumped_langs = {
|
|
140
|
+
key.split(":", 1)[1] for key in analyzer_keys if key.startswith("parser:")
|
|
141
|
+
}
|
|
142
|
+
# analyzer não-linguístico mudou (scanner/scan_mode): sem prova de
|
|
143
|
+
# extração idêntica → todos os arquivos pré-existentes reclassificam.
|
|
144
|
+
global_bump = any(not k.startswith("parser:") for k in analyzer_keys)
|
|
145
|
+
|
|
146
|
+
changes, added = _classify_present(
|
|
147
|
+
current, prior, bumped_langs, global_bump,
|
|
148
|
+
analyzer_keys, prior_versions, current_versions,
|
|
149
|
+
)
|
|
150
|
+
deleted = {p: pf for p, pf in prior.items() if p not in current}
|
|
151
|
+
_mark_deleted(changes, deleted)
|
|
152
|
+
_mark_renames(changes, added, deleted)
|
|
153
|
+
_mark_added_bumps(changes, added, bumped_langs)
|
|
154
|
+
|
|
155
|
+
return ChangeSet(
|
|
156
|
+
changes=tuple(sorted(changes, key=lambda c: (c.path, str(c.kind)))),
|
|
157
|
+
analyzer_changes=analyzer_keys,
|
|
158
|
+
resolver_changes=resolver_keys,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _classify_present(
|
|
163
|
+
current: dict[str, SourceFile],
|
|
164
|
+
prior: dict[str, _PriorFile],
|
|
165
|
+
bumped_langs: set[str],
|
|
166
|
+
global_bump: bool,
|
|
167
|
+
analyzer_keys: tuple[str, ...],
|
|
168
|
+
prior_versions: dict[str, str],
|
|
169
|
+
current_versions: dict[str, str],
|
|
170
|
+
) -> tuple[list[FileChange], dict[str, SourceFile]]:
|
|
171
|
+
"""Classifica arquivos presentes: ADDED / MODIFIED / UNCHANGED."""
|
|
172
|
+
changes: list[FileChange] = []
|
|
173
|
+
added: dict[str, SourceFile] = {}
|
|
174
|
+
for path in sorted(current):
|
|
175
|
+
f = current[path]
|
|
176
|
+
prev = prior.get(path)
|
|
177
|
+
if prev is None:
|
|
178
|
+
added[path] = f
|
|
179
|
+
changes.append(
|
|
180
|
+
FileChange(path, ChangeKind.ADDED, "not in previous snapshot")
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
if global_bump or prev.language in bumped_langs:
|
|
184
|
+
key = (
|
|
185
|
+
f"parser:{prev.language}"
|
|
186
|
+
if prev.language in bumped_langs
|
|
187
|
+
else analyzer_keys[0]
|
|
188
|
+
)
|
|
189
|
+
changes.append(
|
|
190
|
+
FileChange(
|
|
191
|
+
path, ChangeKind.MODIFIED,
|
|
192
|
+
f"analyzer version changed "
|
|
193
|
+
f"({prior_versions.get(key)} -> "
|
|
194
|
+
f"{current_versions.get(key)})",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
elif f.sha256 is None or prev.sha256 is None:
|
|
198
|
+
# sem hash não há prova de conteúdo idêntico → conservador
|
|
199
|
+
changes.append(
|
|
200
|
+
FileChange(
|
|
201
|
+
path, ChangeKind.MODIFIED,
|
|
202
|
+
"content hash unavailable (cannot prove identical)",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
elif prev.sha256 != f.sha256:
|
|
206
|
+
changes.append(
|
|
207
|
+
FileChange(path, ChangeKind.MODIFIED, "content hash differs")
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
changes.append(
|
|
211
|
+
FileChange(path, ChangeKind.UNCHANGED, "identical content hash")
|
|
212
|
+
)
|
|
213
|
+
return changes, added
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _mark_deleted(
|
|
217
|
+
changes: list[FileChange], deleted: dict[str, _PriorFile]
|
|
218
|
+
) -> None:
|
|
219
|
+
for path in sorted(deleted):
|
|
220
|
+
changes.append(
|
|
221
|
+
FileChange(path, ChangeKind.DELETED, "absent from current manifest")
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _swap_added(changes: list[FileChange], path: str, new: FileChange) -> None:
|
|
226
|
+
"""Substitui a entrada ADDED de ``path`` (rename/bump a reclassifica).
|
|
227
|
+
No-op se o path já foi reclassificado (ex.: virou RENAMED)."""
|
|
228
|
+
for i, c in enumerate(changes):
|
|
229
|
+
if c.path == path and c.kind == ChangeKind.ADDED:
|
|
230
|
+
changes[i] = new
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _mark_renames(
|
|
235
|
+
changes: list[FileChange],
|
|
236
|
+
added: dict[str, SourceFile],
|
|
237
|
+
deleted: dict[str, _PriorFile],
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Renames — evidência estrita: hash deletado == hash adicionado.
|
|
240
|
+
Um path deletado casa no máximo um adicionado (primeiro em ordem)."""
|
|
241
|
+
sha_to_deleted: dict[str, list[str]] = {}
|
|
242
|
+
for p in sorted(deleted):
|
|
243
|
+
sha = deleted[p].sha256
|
|
244
|
+
if sha:
|
|
245
|
+
sha_to_deleted.setdefault(sha, []).append(p)
|
|
246
|
+
for path in sorted(added):
|
|
247
|
+
sha = added[path].sha256
|
|
248
|
+
if not sha:
|
|
249
|
+
continue
|
|
250
|
+
cands = sha_to_deleted.get(sha) or []
|
|
251
|
+
if cands:
|
|
252
|
+
old = cands.pop(0)
|
|
253
|
+
_swap_added(
|
|
254
|
+
changes, path,
|
|
255
|
+
FileChange(
|
|
256
|
+
path, ChangeKind.RENAMED,
|
|
257
|
+
f"content-identical to deleted {old}", old_path=old,
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _mark_added_bumps(
|
|
263
|
+
changes: list[FileChange],
|
|
264
|
+
added: dict[str, SourceFile],
|
|
265
|
+
bumped_langs: set[str],
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Arquivos novos de linguagem com bump levam razão enriquecida."""
|
|
268
|
+
for path in sorted(added):
|
|
269
|
+
f = added[path]
|
|
270
|
+
if f.language in bumped_langs:
|
|
271
|
+
_swap_added(
|
|
272
|
+
changes, path,
|
|
273
|
+
FileChange(
|
|
274
|
+
path, ChangeKind.ADDED,
|
|
275
|
+
"not in previous snapshot; analyzer bumped for "
|
|
276
|
+
f"{f.language}",
|
|
277
|
+
),
|
|
278
|
+
)
|