tscode-kg 0.5.0__tar.gz → 0.6.0__tar.gz

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 (36) hide show
  1. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/PKG-INFO +6 -6
  2. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/README.md +3 -3
  3. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/pyproject.toml +4 -40
  4. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/__init__.py +1 -1
  5. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/analysis.py +94 -2
  6. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/mcp_server.py +19 -0
  7. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/LICENSE +0 -0
  8. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/__main__.py +0 -0
  9. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/app.py +0 -0
  10. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/bridge.py +0 -0
  11. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/centrality.py +0 -0
  12. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/__init__.py +0 -0
  13. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_analyze.py +0 -0
  14. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_bridges.py +0 -0
  15. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_build.py +0 -0
  16. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_centrality.py +0 -0
  17. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_explain.py +0 -0
  18. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_framework_nodes.py +0 -0
  19. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_hooks.py +0 -0
  20. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_init.py +0 -0
  21. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_mcp.py +0 -0
  22. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_model.py +0 -0
  23. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_query.py +0 -0
  24. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_snapshot.py +0 -0
  25. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/cmd_viz.py +0 -0
  26. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/cli/main.py +0 -0
  27. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/coderank.py +0 -0
  28. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/config.py +0 -0
  29. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/explain.py +0 -0
  30. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/extractor.py +0 -0
  31. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/framework_detector.py +0 -0
  32. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/kg.py +0 -0
  33. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/layout3d.py +0 -0
  34. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/snapshots.py +0 -0
  35. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/viz3d.py +0 -0
  36. {tscode_kg-0.5.0 → tscode_kg-0.6.0}/src/tscode_kg/viz3d_timeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tscode-kg
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Knowledge graph for TypeScript/JavaScript codebases — AST extraction, hybrid semantic+structural search
5
5
  License-Expression: Elastic-2.0
6
6
  License-File: LICENSE
@@ -19,9 +19,9 @@ Provides-Extra: viz
19
19
  Provides-Extra: viz3d
20
20
  Requires-Dist: PyQt5 (>=5.15.11,<5.16) ; extra == "viz3d"
21
21
  Requires-Dist: click (>=8.1.0,<9)
22
- Requires-Dist: kgmodule-utils[semantic,sqlite-vec] (>=0.20.0)
22
+ Requires-Dist: kgmodule-utils[semantic,sqlite-vec] (>=0.23.0)
23
23
  Requires-Dist: markdown (>=3.10,<3.11) ; extra == "viz3d"
24
- Requires-Dist: mcp (>=1.0.0,<2)
24
+ Requires-Dist: mcp (>=1.3.0,<2)
25
25
  Requires-Dist: networkx (>=3.0)
26
26
  Requires-Dist: param (>=2.4,<2.5) ; extra == "viz3d"
27
27
  Requires-Dist: plotly (>=6.8.0,<6.9) ; extra == "viz"
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
  [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue.svg)](https://www.python.org/)
43
43
  [![License: Elastic-2.0](https://img.shields.io/badge/License-Elastic%202.0-blue.svg)](https://www.elastic.co/licensing/elastic-license)
44
44
  [![PyPI](https://img.shields.io/pypi/v/tscode-kg.svg)](https://pypi.org/project/tscode-kg/)
45
- [![Version](https://img.shields.io/badge/version-0.5.0-blue.svg)](https://github.com/Flux-Frontiers/tscode_kg/releases)
45
+ [![Version](https://img.shields.io/badge/version-0.6.0-blue.svg)](https://github.com/Flux-Frontiers/tscode_kg/releases)
46
46
  [![CI](https://github.com/Flux-Frontiers/tscode_kg/actions/workflows/ci.yml/badge.svg)](https://github.com/Flux-Frontiers/tscode_kg/actions/workflows/ci.yml)
47
47
  [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
48
48
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.22559450-blue.svg)](https://doi.org/10.5281/zenodo.22559450)
@@ -195,13 +195,13 @@ If you use TypeScriptKG in your research or project, please cite it:
195
195
 
196
196
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.22559450-blue.svg)](https://doi.org/10.5281/zenodo.22559450)
197
197
 
198
- > Suchanek, E. G. (2026). *TypeScriptKG: Semantic Knowledge Graph for TypeScript/JavaScript Codebases* (Version 0.5.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.22559450
198
+ > Suchanek, E. G. (2026). *TypeScriptKG: Semantic Knowledge Graph for TypeScript/JavaScript Codebases* (Version 0.6.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.22559450
199
199
 
200
200
  ```bibtex
201
201
  @software{suchanek_tscode_kg,
202
202
  author = {Suchanek, Eric G.},
203
203
  title = {{TypeScriptKG}: Semantic Knowledge Graph for TypeScript/JavaScript Codebases},
204
- version = {0.5.0},
204
+ version = {0.6.0},
205
205
  year = {2026},
206
206
  publisher = {Flux-Frontiers},
207
207
  doi = {10.5281/zenodo.22559450},
@@ -3,7 +3,7 @@
3
3
  [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue.svg)](https://www.python.org/)
4
4
  [![License: Elastic-2.0](https://img.shields.io/badge/License-Elastic%202.0-blue.svg)](https://www.elastic.co/licensing/elastic-license)
5
5
  [![PyPI](https://img.shields.io/pypi/v/tscode-kg.svg)](https://pypi.org/project/tscode-kg/)
6
- [![Version](https://img.shields.io/badge/version-0.5.0-blue.svg)](https://github.com/Flux-Frontiers/tscode_kg/releases)
6
+ [![Version](https://img.shields.io/badge/version-0.6.0-blue.svg)](https://github.com/Flux-Frontiers/tscode_kg/releases)
7
7
  [![CI](https://github.com/Flux-Frontiers/tscode_kg/actions/workflows/ci.yml/badge.svg)](https://github.com/Flux-Frontiers/tscode_kg/actions/workflows/ci.yml)
8
8
  [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
9
9
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.22559450-blue.svg)](https://doi.org/10.5281/zenodo.22559450)
@@ -156,13 +156,13 @@ If you use TypeScriptKG in your research or project, please cite it:
156
156
 
157
157
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.22559450-blue.svg)](https://doi.org/10.5281/zenodo.22559450)
158
158
 
159
- > Suchanek, E. G. (2026). *TypeScriptKG: Semantic Knowledge Graph for TypeScript/JavaScript Codebases* (Version 0.5.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.22559450
159
+ > Suchanek, E. G. (2026). *TypeScriptKG: Semantic Knowledge Graph for TypeScript/JavaScript Codebases* (Version 0.6.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.22559450
160
160
 
161
161
  ```bibtex
162
162
  @software{suchanek_tscode_kg,
163
163
  author = {Suchanek, Eric G.},
164
164
  title = {{TypeScriptKG}: Semantic Knowledge Graph for TypeScript/JavaScript Codebases},
165
- version = {0.5.0},
165
+ version = {0.6.0},
166
166
  year = {2026},
167
167
  publisher = {Flux-Frontiers},
168
168
  doi = {10.5281/zenodo.22559450},
@@ -38,7 +38,7 @@ packages = [{ include = "tscode_kg", from = "src" }]
38
38
 
39
39
  [project]
40
40
  name = "tscode-kg"
41
- version = "0.5.0"
41
+ version = "0.6.0"
42
42
  description = "Knowledge graph for TypeScript/JavaScript codebases — AST extraction, hybrid semantic+structural search"
43
43
  readme = "README.md"
44
44
  license = "Elastic-2.0"
@@ -68,7 +68,7 @@ dependencies = [
68
68
  # manager is now the package_name class attribute, where it used to be an
69
69
  # __init__ that only forwarded to super(). Against 0.19.x the manager
70
70
  # reports itself as "kg-utils" in every snapshot's tool field.
71
- "kgmodule-utils[semantic,sqlite-vec]>=0.20.0",
71
+ "kgmodule-utils[semantic,sqlite-vec]>=0.23.0",
72
72
  "click>=8.1.0,<9",
73
73
  # Upper-bounded: mcp 2.0 removed the bundled `mcp.server.fastmcp` module
74
74
  # (FastMCP was split out into the standalone `fastmcp` package) and rebuilt
@@ -76,7 +76,7 @@ dependencies = [
76
76
  # FastMCP from mcp, so an unbounded floor lets a clean install pull 2.x and
77
77
  # break `tscodekg-mcp` at import time. Lift this only alongside a port to
78
78
  # the standalone fastmcp package.
79
- "mcp>=1.0.0,<2",
79
+ "mcp>=1.3.0,<2",
80
80
  "networkx>=3.0",
81
81
  ]
82
82
 
@@ -123,43 +123,7 @@ pytest-cov = ">=5.0.0"
123
123
  # Capped below 0.16 to match the fleet: 0.16 formats Python inside Markdown
124
124
  # code blocks as stable behaviour. The floor was unbounded, so the lock could
125
125
  # drift away from the version .pre-commit-config.yaml pins.
126
- ruff = ">=0.4.0,<0.16"
127
-
128
- # ---------------------------------------------------------------------------
129
- # Maintainer tooling (Poetry group, not a PEP 621 extra)
130
- # ---------------------------------------------------------------------------
131
- # pycode-kg is a tool this repo *runs*, not a library it imports: PyCodeKG
132
- # indexes this repo's Python source from the outside, via the `pycodekg` CLI in
133
- # .git/hooks/pre-commit. It belongs in a Poetry group -- locked and installable,
134
- # but never written into the wheel, so no published extra acquires a sibling.
135
- #
136
- # doc-kg rides along for fleet uniformity: every repo's `kg` group carries the
137
- # same two CLIs, so `poetry install --with kg` means one thing everywhere. This
138
- # repo has no DocKG index today (no .mcp.json, no `dockg` invocation), so the
139
- # CLI is simply available rather than required -- and it is here, not in an
140
- # extra, precisely because nothing depends on it at runtime.
141
- #
142
- # poetry install --with kg # get the pycodekg CLI into .venv/bin
143
- # poetry install # default: group is optional, skipped
144
- #
145
- # This supersedes the standing "install it by hand" workaround. That note
146
- # described a real deadlock -- older pycode-kg capped transformers below 4.57
147
- # while kgmodule-utils >=0.9.0 needs >=5.5.0, which silently held this repo on
148
- # the pre-CVE transformers line. pycode-kg 0.21.4 no longer pins transformers at
149
- # all; it inherits the same kgmodule-utils[semantic] constraint this project
150
- # already uses, so the two no longer conflict. Verified: locking with this group
151
- # keeps transformers at 5.14.1.
152
- [tool.poetry.group.kg]
153
- optional = true
154
-
155
- [tool.poetry.group.kg.dependencies]
156
- # Both floors are the releases that retired these packages' own snapshot
157
- # overrides and moved them onto kgmodule-utils>=0.20.0, the same SDK this repo
158
- # now requires. Pinned lower, `poetry install --with kg` could resolve a dockg
159
- # or pycodekg predating the shared extension points into the same environment
160
- # as a tscode_kg that depends on them.
161
- doc-kg = ">=0.26.0"
162
- pycode-kg = ">=0.27.0"
126
+ ruff = ">=0.15,<0.16"
163
127
 
164
128
  [project.urls]
165
129
  Homepage = "https://github.com/Flux-Frontiers/tscode_kg"
@@ -18,7 +18,7 @@ KGExtractor SDK::
18
18
  from tscode_kg import TSCodeExtractor
19
19
  """
20
20
 
21
- __version__ = "0.5.0"
21
+ __version__ = "0.6.0"
22
22
  __author__ = "Eric G. Suchanek, PhD"
23
23
 
24
24
  from tscode_kg.extractor import TSCodeExtractor
@@ -166,17 +166,60 @@ class TSCodeKGAnalyzer:
166
166
  self.coderank_top_nodes: list[dict] = []
167
167
  self.concern_analysis: list[dict] = []
168
168
  self._phase_result: str = ""
169
+ #: Phases that could not run, with the reason. Rendered in the report
170
+ #: so a degraded run says what is missing rather than omitting it.
171
+ self.phase_failures: list[dict] = []
169
172
 
170
173
  # ------------------------------------------------------------------
171
174
  # Phase runner
172
175
  # ------------------------------------------------------------------
173
176
 
177
+ def _take_phase_result(self) -> str:
178
+ """Read and clear the one-line summary the phase just set.
179
+
180
+ Phases report through ``self._phase_result`` rather than a return
181
+ value, because they are called as bare ``Callable[[], None]``. Reading
182
+ it here rather than inline in :meth:`_run_phase` also keeps the read
183
+ away from that method's own ``= ""`` assignment, which a type checker
184
+ otherwise narrows to a literal it can never see ``fn()`` change.
185
+ """
186
+ result = self._phase_result
187
+ self._phase_result = ""
188
+ return f" {result}" if result else ""
189
+
174
190
  def _run_phase(self, num: int, name: str, fn: Callable[[], None]) -> None:
191
+ """Run one analysis phase, reporting rather than propagating a failure.
192
+
193
+ A phase that cannot run must not take the other thirteen and the report
194
+ with it. The individual phases already guard the errors they expect
195
+ (``AttributeError``, ``ValueError``, ``RuntimeError``), but the ones
196
+ worth surviving are the ones nobody predicted -- the concrete case
197
+ being ``sqlite3.OperationalError: no such table: vec_nodes`` after
198
+ ``tscodekg build-sqlite``, a supported command that deliberately builds
199
+ the graph without the vector index. Only phase 4 seeds on a semantic
200
+ query; the other thirteen are pure SQL and have everything they need.
201
+ Aborting the run gave the user no report at all, and an error naming
202
+ an internal table rather than the missing step.
203
+
204
+ The failure is recorded in :attr:`phase_failures` and rendered in the
205
+ report, so a degraded run says which sections are missing and why
206
+ instead of quietly omitting them.
207
+ """
175
208
  self._phase_result = ""
176
209
  t0 = time.monotonic()
177
- fn()
210
+ try:
211
+ fn()
212
+ except Exception as exc: # noqa: BLE001 -- see docstring
213
+ elapsed = time.monotonic() - t0
214
+ self.phase_failures.append({"phase": num, "name": name, "error": str(exc)})
215
+ logger.warning("Phase %d (%s) failed: %s", num, name, exc)
216
+ self.console.print(
217
+ f" [cyan]▶ Phase {num:2d}/{self._TOTAL_PHASES}:[/cyan]"
218
+ f" {name} [yellow]skipped: {exc}[/yellow] [green]({elapsed:.1f}s)[/green]"
219
+ )
220
+ return
178
221
  elapsed = time.monotonic() - t0
179
- result = f" {self._phase_result}" if self._phase_result else ""
222
+ result = self._take_phase_result()
180
223
  self.console.print(
181
224
  f" [cyan]▶ Phase {num:2d}/{self._TOTAL_PHASES}:[/cyan]"
182
225
  f" {name}{result} [green]({elapsed:.1f}s)[/green]"
@@ -1404,6 +1447,10 @@ Most-called functions and methods — potential bottlenecks or core APIs.
1404
1447
  """
1405
1448
  )
1406
1449
 
1450
+ degraded = self._render_phase_failures()
1451
+ if degraded:
1452
+ report += "\n---\n\n" + degraded + "\n---\n"
1453
+
1407
1454
  for i, metrics in enumerate(
1408
1455
  sorted(self.function_metrics.values(), key=lambda m: m.fan_in, reverse=True)[:15], 1
1409
1456
  ):
@@ -1641,6 +1688,42 @@ Cohesion = incoming-callers / (incoming + outgoing + 1). Higher = more internall
1641
1688
  Path(report_path).write_text(report, encoding="utf-8")
1642
1689
  self.console.print(f"[green]✓[/green] Report written to {report_path}")
1643
1690
 
1691
+ def _render_phase_failures(self) -> str:
1692
+ """Render the degraded-run notice, or an empty string when all phases ran.
1693
+
1694
+ A phase that could not run leaves its section of the report empty. Said
1695
+ plainly, that is a missing build step; left unsaid, it looks like a
1696
+ finding about the codebase.
1697
+ """
1698
+ if not self.phase_failures:
1699
+ return ""
1700
+
1701
+ lines = ["## Incomplete Analysis\n"]
1702
+ lines.append(
1703
+ f"{len(self.phase_failures)} of {self._TOTAL_PHASES} phases could not run. "
1704
+ "The sections they produce are missing from this report — that is a gap in "
1705
+ "the analysis, not a finding about the code.\n"
1706
+ )
1707
+ lines.append("| Phase | Name | Reason |")
1708
+ lines.append("|---|---|---|")
1709
+ for failure in self.phase_failures:
1710
+ lines.append(f"| {failure['phase']} | {failure['name']} | `{failure['error']}` |")
1711
+
1712
+ if any(
1713
+ "vec_nodes" in f["error"]
1714
+ or "no such table" in f["error"]
1715
+ or "Vector store not found" in f["error"]
1716
+ for f in self.phase_failures
1717
+ ):
1718
+ lines.append(
1719
+ "\n> The semantic index is missing. Only the fan-out phase needs it; "
1720
+ "every other phase reads the SQLite graph directly, which is why the "
1721
+ "rest of this report is complete. Run `tscodekg build --repo <path>` "
1722
+ "(or `tscodekg build-index`) to build it, then re-run the analysis."
1723
+ )
1724
+ lines.append("")
1725
+ return "\n".join(lines)
1726
+
1644
1727
  def _compile_results(self) -> dict:
1645
1728
  """Compile all phase results into a serialisable dictionary."""
1646
1729
  sorted_fn = sorted(self.function_metrics.items(), key=lambda kv: kv[1].fan_in, reverse=True)
@@ -1676,6 +1759,10 @@ Cohesion = incoming-callers / (incoming + outgoing + 1). Higher = more internall
1676
1759
  ],
1677
1760
  "centrality_modules": self.centrality_modules,
1678
1761
  "coderank_top_nodes": self.coderank_top_nodes,
1762
+ # A caller reading the dict rather than the Markdown needs the same
1763
+ # signal the report carries: an empty section here means a phase did
1764
+ # not run, not that the codebase has nothing to show.
1765
+ "phase_failures": self.phase_failures,
1679
1766
  }
1680
1767
 
1681
1768
  def to_markdown(self) -> str:
@@ -1693,6 +1780,11 @@ Cohesion = incoming-callers / (incoming + outgoing + 1). Higher = more internall
1693
1780
  out.append(f"**Generated:** {datetime.datetime.now(datetime.UTC).isoformat()} \n")
1694
1781
  out.append("\n---\n")
1695
1782
 
1783
+ degraded = self._render_phase_failures()
1784
+ if degraded:
1785
+ out.append(degraded)
1786
+ out.append("\n---\n")
1787
+
1696
1788
  out.append("## Baseline Metrics\n")
1697
1789
  out.append("| Metric | Value |")
1698
1790
  out.append("|---|---|")
@@ -74,6 +74,8 @@ from __future__ import annotations
74
74
  import argparse
75
75
  import json
76
76
  import sys
77
+ from collections.abc import AsyncIterator
78
+ from contextlib import asynccontextmanager
77
79
  from pathlib import Path
78
80
 
79
81
  from kg_utils.semantic import DEFAULT_MODEL
@@ -142,6 +144,22 @@ def _snapshot_freshness(snapshot_total_nodes: int) -> dict:
142
144
  # MCP server
143
145
  # ---------------------------------------------------------------------------
144
146
 
147
+
148
+ @asynccontextmanager
149
+ async def _lifespan(_server: FastMCP) -> AsyncIterator[None]:
150
+ """Close the graph's SQLite connection(s) when the server shuts down.
151
+
152
+ ``main()`` sets the module-level ``_kg`` before ``mcp.run()`` calls into
153
+ this, and both the stdio and SSE transports route through the same
154
+ underlying ``Server.run()``, so this fires on either one.
155
+ """
156
+ try:
157
+ yield
158
+ finally:
159
+ if _kg is not None:
160
+ _kg.close()
161
+
162
+
145
163
  mcp = FastMCP(
146
164
  "tscodekg",
147
165
  instructions=(
@@ -202,6 +220,7 @@ mcp = FastMCP(
202
220
  "- **Track codebase evolution**: snapshot_list → snapshot_diff(key_a, key_b)\n"
203
221
  "- **Answer 'how does X work?'**: pack_snippets with a descriptive query\n"
204
222
  ),
223
+ lifespan=_lifespan,
205
224
  )
206
225
 
207
226
 
File without changes
File without changes