java-codebase-rag 0.11.2__py3-none-any.whl → 0.12.1__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 (73) hide show
  1. java_codebase_rag-0.12.1.dist-info/METADATA +35 -0
  2. java_codebase_rag-0.12.1.dist-info/RECORD +4 -0
  3. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.1.dist-info}/WHEEL +1 -1
  4. java_codebase_rag/_fdlimit.py +0 -56
  5. java_codebase_rag/_stdio.py +0 -32
  6. java_codebase_rag/_version.py +0 -35
  7. java_codebase_rag/absence/__init__.py +0 -0
  8. java_codebase_rag/absence/absence_diagnosis.py +0 -700
  9. java_codebase_rag/absence/absence_types.py +0 -124
  10. java_codebase_rag/absence/absence_vocab.py +0 -460
  11. java_codebase_rag/analysis/__init__.py +0 -0
  12. java_codebase_rag/analysis/pr_analysis.py +0 -563
  13. java_codebase_rag/analysis/resolve_service.py +0 -740
  14. java_codebase_rag/ast/__init__.py +0 -0
  15. java_codebase_rag/ast/ast_java.py +0 -2825
  16. java_codebase_rag/ast/brownfield_events.py +0 -58
  17. java_codebase_rag/ast/chunk_heuristics.py +0 -62
  18. java_codebase_rag/cli.py +0 -1215
  19. java_codebase_rag/cli_format.py +0 -85
  20. java_codebase_rag/cli_progress.py +0 -94
  21. java_codebase_rag/config.py +0 -833
  22. java_codebase_rag/eval/__init__.py +0 -1
  23. java_codebase_rag/eval/ground_truth.py +0 -100
  24. java_codebase_rag/eval/metrics.py +0 -107
  25. java_codebase_rag/eval/runner.py +0 -556
  26. java_codebase_rag/graph/__init__.py +0 -0
  27. java_codebase_rag/graph/build_ast_graph.py +0 -4471
  28. java_codebase_rag/graph/graph_enrich.py +0 -1937
  29. java_codebase_rag/graph/graph_types.py +0 -224
  30. java_codebase_rag/graph/java_ontology.py +0 -465
  31. java_codebase_rag/graph/ladybug_queries.py +0 -2213
  32. java_codebase_rag/graph/path_filtering.py +0 -477
  33. java_codebase_rag/index/__init__.py +0 -0
  34. java_codebase_rag/index/java_index_flow_lancedb.py +0 -734
  35. java_codebase_rag/index/java_index_v1_common.py +0 -33
  36. java_codebase_rag/install_data/__init__.py +0 -0
  37. java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -108
  38. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
  39. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
  40. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
  41. java_codebase_rag/installer.py +0 -2188
  42. java_codebase_rag/jrag.py +0 -4531
  43. java_codebase_rag/jrag_envelope.py +0 -1107
  44. java_codebase_rag/jrag_hints.py +0 -204
  45. java_codebase_rag/jrag_render.py +0 -926
  46. java_codebase_rag/lance_optimize.py +0 -264
  47. java_codebase_rag/mcp/__init__.py +0 -0
  48. java_codebase_rag/mcp/mcp_hints.py +0 -932
  49. java_codebase_rag/mcp/mcp_v2.py +0 -1916
  50. java_codebase_rag/mcp/server.py +0 -884
  51. java_codebase_rag/pipeline.py +0 -531
  52. java_codebase_rag/progress.py +0 -570
  53. java_codebase_rag/read_payloads.py +0 -781
  54. java_codebase_rag/search/__init__.py +0 -0
  55. java_codebase_rag/search/index_common.py +0 -10
  56. java_codebase_rag/search/search_lancedb.py +0 -1296
  57. java_codebase_rag/search/search_lexical.py +0 -449
  58. java_codebase_rag/search/search_scoring.py +0 -523
  59. java_codebase_rag/watch/__init__.py +0 -0
  60. java_codebase_rag/watch/client.py +0 -230
  61. java_codebase_rag/watch/daemon.py +0 -396
  62. java_codebase_rag/watch/lock.py +0 -201
  63. java_codebase_rag/watch/paths.py +0 -76
  64. java_codebase_rag/watch/protocol.py +0 -122
  65. java_codebase_rag/watch/server.py +0 -273
  66. java_codebase_rag/watch/warm.py +0 -105
  67. java_codebase_rag/watch/watcher.py +0 -370
  68. java_codebase_rag-0.11.2.dist-info/METADATA +0 -331
  69. java_codebase_rag-0.11.2.dist-info/RECORD +0 -71
  70. java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
  71. java_codebase_rag-0.11.2.dist-info/licenses/LICENSE +0 -21
  72. java_codebase_rag-0.11.2.dist-info/top_level.txt +0 -1
  73. /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.1.dist-info/top_level.txt +0 -0
@@ -1,477 +0,0 @@
1
- """Layered path ignore rules for Java indexing and graph enrichment (B5).
2
-
3
- Resolution order (later overrides earlier; innermost nested wins among peers):
4
-
5
- 1. ``builtin_default`` — legacy ``COMMON_EXCLUDED_PATH_PATTERNS`` (gitignore-style).
6
- 2. ``project_root`` — ``<project>/.java-codebase-rag/ignore``.
7
- 3. ``nested`` — each ``<dir>/.java-codebase-rag/ignore`` along the path from project root
8
- to the file's parent (outer dirs first, inner dirs last).
9
- 4. ``gitignore`` — each ``.gitignore`` from project root down to the file's parent
10
- (when ``use_gitignore`` is true), using :class:`pathspec.GitIgnoreSpec`.
11
-
12
- Paths outside ``project_root`` are never ignored by this object.
13
- """
14
- from __future__ import annotations
15
-
16
- import fnmatch
17
- import os
18
- import warnings
19
- from collections.abc import Iterator, Sequence
20
- from dataclasses import dataclass
21
- from pathlib import Path
22
- from typing import overload
23
-
24
- from pathspec import GitIgnoreSpec
25
-
26
- # Pruning for LocalFile sources: skip VCS, build outputs, dependency trees, and
27
- # test sources (we currently index prod Java only to keep the semantic index clean).
28
- # Also avoids EMFILE under default ulimits when the engine traverses in parallel.
29
- #
30
- # Note on build-output dir names: ``out``, ``build`` and ``target`` are also legal
31
- # Java package names (e.g. ``com.example.out.api``). The unconditional ``**/out/**``
32
- # pattern that previously lived here false-matched such packages and silently
33
- # dropped real source files. These dirs are now pruned only when they sit next to
34
- # a build-tool indicator (``pom.xml``, ``build.gradle``, ``build.gradle.kts``,
35
- # ``settings.gradle``, ``settings.gradle.kts``) — see ``_is_build_output_dir``
36
- # and ``BUILD_DIR_NAMES``. If you genuinely need to skip an arbitrary nested
37
- # directory, add a ``.java-codebase-rag/ignore`` entry at the project or subtree root.
38
- COMMON_EXCLUDED_PATH_PATTERNS: list[str] = [
39
- "**/.*",
40
- "**/.git/**",
41
- "**/.idea/**",
42
- "**/.venv/**",
43
- "**/node_modules/**",
44
- "**/*.class",
45
- "**/src/test/java/**",
46
- "**/src/test/resources/**",
47
- ]
48
-
49
- # Directory names that are pruned ONLY when they sit next to a build-tool indicator.
50
- # The check is ``parent_dir`` contains any of ``BUILD_TOOL_INDICATORS``.
51
- BUILD_DIR_NAMES: tuple[str, ...] = ("target", "build", "out")
52
-
53
- # Files whose presence in a directory marks it as a JVM build module. When one
54
- # of these sits next to a ``BUILD_DIR_NAMES`` entry, that entry is treated as
55
- # build output and pruned from the walk.
56
- BUILD_TOOL_INDICATORS: tuple[str, ...] = (
57
- "pom.xml",
58
- "build.gradle",
59
- "build.gradle.kts",
60
- "settings.gradle",
61
- "settings.gradle.kts",
62
- )
63
-
64
- # Directory names always pruned regardless of siblings (universal nuisance dirs;
65
- # never a legal package name in practice).
66
- UNCONDITIONAL_PRUNE_DIRS: frozenset[str] = frozenset({
67
- ".git",
68
- ".idea",
69
- ".venv",
70
- "node_modules",
71
- })
72
-
73
-
74
- def _is_build_output_dir(parent_dir: str, dirname: str) -> bool:
75
- """True iff ``<parent_dir>/<dirname>`` looks like a JVM build-output directory.
76
-
77
- A name in :data:`BUILD_DIR_NAMES` is build output only when its parent
78
- directory contains a build-tool indicator (Maven/Gradle marker file).
79
- Otherwise, names like ``out`` are treated as ordinary subdirectories so
80
- Java sources under packages such as ``com.example.out.api`` survive the walk.
81
- """
82
- if dirname not in BUILD_DIR_NAMES:
83
- return False
84
- try:
85
- with os.scandir(parent_dir) as it:
86
- siblings = {entry.name for entry in it}
87
- except OSError:
88
- return False
89
- return any(marker in siblings for marker in BUILD_TOOL_INDICATORS)
90
-
91
-
92
- def compile_excluded_glob_patterns(
93
- patterns: Sequence[str] | tuple[str, ...],
94
- ) -> list[str]:
95
- """Store exclude patterns in list form; same as ast-graph ``index`` compile step."""
96
- return list(patterns)
97
-
98
-
99
- def is_relative_path_excluded(rel_posix: str, exclude_globs: list[str]) -> bool:
100
- """True if a project-relative path matches an exclude glob (incl. ``**/<path>``)."""
101
- for pat in exclude_globs:
102
- if fnmatch.fnmatch(rel_posix, pat):
103
- return True
104
- if fnmatch.fnmatch(f"**/{rel_posix}", pat):
105
- return True
106
- return False
107
-
108
-
109
- @dataclass(frozen=True)
110
- class IgnoreLayer:
111
- """One ignore configuration anchored at ``root`` (patterns apply under this dir)."""
112
-
113
- root: Path
114
- spec: GitIgnoreSpec
115
- source: str
116
- ignore_file: Path | None = None
117
-
118
-
119
- def _read_ignore_lines(path: Path) -> list[str]:
120
- try:
121
- text = path.read_text(encoding="utf-8", errors="replace")
122
- except OSError:
123
- return []
124
- return text.splitlines()
125
-
126
-
127
- def _line_has_negation(lines: Sequence[str]) -> bool:
128
- for raw in lines:
129
- s = raw.strip()
130
- if not s or s.startswith("#"):
131
- continue
132
- if s.startswith("\\!"):
133
- continue
134
- if s.startswith("!"):
135
- return True
136
- return False
137
-
138
-
139
- def _scan_negation_any_bundle_ignore(project_root: Path) -> bool:
140
- """Return True if any ``.java-codebase-rag/ignore`` contains a negation (``!``) line.
141
-
142
- Runs one ``rglob`` at :class:`LayeredIgnore` construction. Fine for typical
143
- repos; very large monorepos pay a full-tree walk on every new ``LayeredIgnore``
144
- instance (same for :func:`_scan_negation_any_gitignore`).
145
- """
146
- root = project_root.resolve()
147
- try:
148
- for p in root.rglob(".java-codebase-rag"):
149
- if not p.is_dir():
150
- continue
151
- ign = p / "ignore"
152
- if ign.is_file() and _line_has_negation(_read_ignore_lines(ign)):
153
- return True
154
- except OSError:
155
- return False
156
- return False
157
-
158
-
159
- def _scan_negation_any_gitignore(project_root: Path) -> bool:
160
- """See :func:`_scan_negation_any_bundle_ignore` (also uses ``rglob``)."""
161
- root = project_root.resolve()
162
- try:
163
- for p in root.rglob(".gitignore"):
164
- if p.is_file() and _line_has_negation(_read_ignore_lines(p)):
165
- return True
166
- except OSError:
167
- return False
168
- return False
169
-
170
-
171
- def _prefix_line_to_project(
172
- prefix_posix: str,
173
- raw_line: str,
174
- ) -> str | None:
175
- """Map a gitignore line from a subdirectory anchor to project-root-relative."""
176
- line = raw_line.strip()
177
- if not line or line.startswith("#"):
178
- return None
179
- neg = line.startswith("!")
180
- body = line[1:] if neg else line
181
- if body.startswith("\\#") or body.startswith("\\!"):
182
- body = body[1:]
183
- anchored = body.startswith("/")
184
- if anchored:
185
- body = body[1:]
186
- if prefix_posix:
187
- mapped = f"{prefix_posix}/{body}" if body else prefix_posix
188
- else:
189
- mapped = body
190
- return f"!{mapped}" if neg else mapped
191
-
192
-
193
- def _mega_build_for_rel(
194
- self_root: Path,
195
- rel_project: str,
196
- *,
197
- use_gitignore: bool,
198
- builtin_lines: list[str],
199
- project_ignore_path: Path,
200
- project_lines: list[str] | None,
201
- ) -> tuple[list[str], list[tuple[str, Path | None, int, str]]]:
202
- """Mega gitignore lines (project-relative) + (source, file, line_no, pattern_text)."""
203
- mega: list[str] = []
204
- meta: list[tuple[str, Path | None, int, str]] = []
205
-
206
- def extend_builtin() -> None:
207
- for i, raw in enumerate(builtin_lines, start=1):
208
- s = raw.strip()
209
- if not s or s.startswith("#"):
210
- continue
211
- mega.append(raw.rstrip("\n"))
212
- meta.append(("builtin_default", None, i, s))
213
-
214
- def extend_file(source: str, path: Path, lines: Sequence[str]) -> None:
215
- for lineno, raw in enumerate(lines, start=1):
216
- s = raw.strip()
217
- if not s or s.startswith("#"):
218
- continue
219
- mega.append(raw.rstrip("\n"))
220
- meta.append((source, path, lineno, s))
221
-
222
- extend_builtin()
223
- if project_lines is not None:
224
- extend_file("project_root", project_ignore_path, project_lines)
225
-
226
- parts = Path(rel_project).parts
227
- dir_parts = parts[:-1] if len(parts) > 1 else ()
228
- for i in range(1, len(dir_parts) + 1):
229
- anchor = self_root.joinpath(*dir_parts[:i])
230
- nested_path = anchor / ".java-codebase-rag" / "ignore"
231
- if not nested_path.is_file():
232
- continue
233
- prefix = anchor.relative_to(self_root).as_posix()
234
- nlines = _read_ignore_lines(nested_path)
235
- for lineno, raw in enumerate(nlines, start=1):
236
- mapped = _prefix_line_to_project(prefix, raw)
237
- if mapped is None:
238
- continue
239
- mega.append(mapped)
240
- meta.append(("nested", nested_path, lineno, raw.strip()))
241
-
242
- if use_gitignore:
243
- for i in range(len(dir_parts) + 1):
244
- anchor = self_root if i == 0 else self_root.joinpath(*dir_parts[:i])
245
- git_path = anchor / ".gitignore"
246
- if not git_path.is_file():
247
- continue
248
- prefix = anchor.relative_to(self_root).as_posix() if i > 0 else ""
249
- glines = _read_ignore_lines(git_path)
250
- for lineno, raw in enumerate(glines, start=1):
251
- mapped = _prefix_line_to_project(prefix, raw)
252
- if mapped is None:
253
- continue
254
- mega.append(mapped)
255
- meta.append(("gitignore", git_path, lineno, raw.strip()))
256
-
257
- return mega, meta
258
-
259
-
260
- def _winning_row(
261
- rel: str,
262
- mega: list[str],
263
- meta: list[tuple[str, Path | None, int, str]],
264
- ) -> tuple[str, Path | None, int, str]:
265
- """The last rule line that changes the cumulative match result (git semantics)."""
266
- if not mega:
267
- return "builtin_default", None, 1, ""
268
- state = False
269
- last_idx = 0
270
- for i in range(len(mega)):
271
- cur = GitIgnoreSpec.from_lines(mega[: i + 1]).match_file(rel)
272
- if cur != state:
273
- last_idx = i
274
- state = cur
275
- return meta[last_idx]
276
-
277
-
278
- class LayeredIgnore:
279
- """Evaluate layered ignore rules anchored at a single project root."""
280
-
281
- def __init__(
282
- self,
283
- project_root: Path | str,
284
- *,
285
- use_gitignore: bool = True,
286
- builtin_patterns: Sequence[str] | None = None,
287
- ) -> None:
288
- self.project_root = Path(project_root).expanduser().resolve()
289
- self.use_gitignore = use_gitignore
290
- self._builtin_lines = (
291
- list(builtin_patterns)
292
- if builtin_patterns is not None
293
- else list(COMMON_EXCLUDED_PATH_PATTERNS)
294
- )
295
- self._project_ignore_path = self.project_root / ".java-codebase-rag" / "ignore"
296
- self._project_lines: list[str] | None = None
297
- if self._project_ignore_path.is_file():
298
- self._project_lines = _read_ignore_lines(self._project_ignore_path)
299
- self._permissive_coco_walk = (
300
- _scan_negation_any_bundle_ignore(self.project_root)
301
- or (use_gitignore and _scan_negation_any_gitignore(self.project_root))
302
- )
303
- self._mega_cache: dict[str, tuple[list[str], GitIgnoreSpec, list[tuple[str, Path | None, int, str]]]] = {}
304
-
305
- def cocoindex_excluded_patterns(self) -> list[str]:
306
- """Patterns for CocoIndex ``PatternFilePathMatcher.excluded_patterns``.
307
-
308
- Matches pre-B5 behaviour when no negation rules exist anywhere under the
309
- project that could un-ignore paths under pruned directories. Otherwise
310
- returns an empty list and callers must filter each path with
311
- :meth:`is_ignored`.
312
- """
313
- if self._permissive_coco_walk:
314
- return []
315
- return list(self._builtin_lines)
316
-
317
- def _rel_project(self, path: Path) -> str | None:
318
- try:
319
- return path.resolve().relative_to(self.project_root).as_posix()
320
- except ValueError:
321
- return None
322
-
323
- def _path_for_display(self, path: Path | None) -> str:
324
- """Project-relative POSIX path when under ``project_root``; else best-effort short path."""
325
- if path is None:
326
- return ""
327
- try:
328
- return path.resolve().relative_to(self.project_root).as_posix()
329
- except ValueError:
330
- try:
331
- return path.resolve().relative_to(Path.cwd()).as_posix()
332
- except ValueError:
333
- return path.as_posix()
334
-
335
- def _mega(self, rel_project: str) -> tuple[list[str], GitIgnoreSpec, list[tuple[str, Path | None, int, str]]]:
336
- # Cache by directory (parent of rel_project). _mega_build_for_rel reads only dir_parts,
337
- # so files in the same directory share the same mega/spec/meta tuple.
338
- cache_key = Path(rel_project).parent.as_posix()
339
- if cache_key in self._mega_cache:
340
- return self._mega_cache[cache_key]
341
- mega, meta = _mega_build_for_rel(
342
- self.project_root,
343
- rel_project,
344
- use_gitignore=self.use_gitignore,
345
- builtin_lines=self._builtin_lines,
346
- project_ignore_path=self._project_ignore_path,
347
- project_lines=self._project_lines,
348
- )
349
- result = (mega, GitIgnoreSpec.from_lines(mega), meta)
350
- self._mega_cache[cache_key] = result
351
- return result
352
-
353
- def is_ignored(self, path: Path) -> bool:
354
- """Return whether ``path`` is ignored by any configured layer.
355
-
356
- Boolean-only fast path for the per-file index walk. It deliberately does
357
- not compute *which* layer/source last matched: that attribution is
358
- O(rules²) via :func:`_winning_row` (one ``GitIgnoreSpec`` rebuild per
359
- rule prefix) and is only needed for ``diagnose-ignore``, so it lives in
360
- :meth:`diagnose_dict` and is never paid on the hot path.
361
- """
362
- rel = self._rel_project(path)
363
- if rel is None:
364
- return False
365
- mega, spec, _ = self._mega(rel)
366
- if not mega:
367
- return False
368
- return spec.match_file(rel)
369
-
370
- def diagnose(self, path: Path) -> str:
371
- """Human-readable, multi-line explanation of the ignore decision."""
372
- d = self.diagnose_dict(path)
373
- expl = d.get("explanation", "")
374
- layer = d.get("layer")
375
- ign = d.get("ignored")
376
- mp = d.get("matching_pattern")
377
- lines = [
378
- f"ignored={ign}",
379
- f"layer={layer!r}",
380
- f"matching_pattern={mp!r}",
381
- str(expl),
382
- ]
383
- return "\n".join(lines)
384
-
385
- def diagnose_dict(self, path: Path) -> dict[str, object]:
386
- """Structured diagnose payload for MCP ``diagnose_ignore``."""
387
- rel = self._rel_project(path)
388
- if rel is None:
389
- return {
390
- "ignored": False,
391
- "layer": None,
392
- "matching_pattern": None,
393
- "explanation": (
394
- f"Path {self._path_for_display(path)!r} is outside the configured "
395
- "project root — not ignored."
396
- ),
397
- }
398
- mega, spec, meta = self._mega(rel)
399
- if not mega:
400
- return {
401
- "ignored": False,
402
- "layer": None,
403
- "matching_pattern": None,
404
- "explanation": f"Path {rel!r} is not ignored by any configured layer.",
405
- }
406
- ignored = spec.match_file(rel)
407
- if not ignored:
408
- return {
409
- "ignored": False,
410
- "layer": None,
411
- "matching_pattern": None,
412
- "explanation": f"Path {rel!r} is not ignored by any configured layer.",
413
- }
414
- src, fp, ln, pat = _winning_row(rel, mega, meta)
415
- if fp is not None:
416
- expl = (
417
- f"Excluded by {self._path_for_display(fp)} ({src}) at line {ln}: {pat!r}"
418
- )
419
- else:
420
- expl = f"Excluded by builtin default ({src}) at builtin line {ln}: {pat!r}"
421
- return {
422
- "ignored": True,
423
- "layer": src,
424
- "matching_pattern": pat,
425
- "explanation": expl,
426
- }
427
-
428
-
429
- @overload
430
- def iter_java_source_files(root: Path, exclude_globs: list[str]) -> Iterator[Path]: ...
431
-
432
-
433
- @overload
434
- def iter_java_source_files(root: Path, *, ignore: LayeredIgnore) -> Iterator[Path]: ...
435
-
436
-
437
- def iter_java_source_files(
438
- root: Path,
439
- exclude_globs: list[str] | None = None,
440
- *,
441
- ignore: LayeredIgnore | None = None,
442
- ) -> Iterator[Path]:
443
- """Walk ``root`` for ``*.java``, honouring prunes and layered ignore rules."""
444
- if exclude_globs is not None and ignore is not None:
445
- raise TypeError("pass either exclude_globs or ignore=, not both")
446
- if exclude_globs is not None:
447
- warnings.warn(
448
- "iter_java_source_files(root, exclude_globs) is deprecated; "
449
- "use iter_java_source_files(root, ignore=LayeredIgnore(root, ...)).",
450
- DeprecationWarning,
451
- stacklevel=2,
452
- )
453
- ignore_ctx = LayeredIgnore(root, builtin_patterns=exclude_globs, use_gitignore=False)
454
- elif ignore is not None:
455
- ignore_ctx = ignore
456
- else:
457
- ignore_ctx = LayeredIgnore(root)
458
- root = root.resolve()
459
- for dirpath, dirnames, filenames in os.walk(root):
460
- # Universal nuisance dirs (VCS, IDE, deps) are pruned unconditionally.
461
- # Build-output dirs (``out`` / ``build`` / ``target``) are pruned only when
462
- # they sit alongside a build-tool indicator file — otherwise names like
463
- # ``out`` belong to a Java package (e.g. ``com.example.out.api``) and must
464
- # be walked. See ``_is_build_output_dir``.
465
- dirnames[:] = [
466
- d
467
- for d in dirnames
468
- if d not in UNCONDITIONAL_PRUNE_DIRS
469
- and not _is_build_output_dir(dirpath, d)
470
- ]
471
- for fn in filenames:
472
- if not fn.endswith(".java"):
473
- continue
474
- p = Path(dirpath) / fn
475
- if ignore_ctx.is_ignored(p):
476
- continue
477
- yield p
File without changes