alphaengine 0.9.3__tar.gz → 0.9.4__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 (93) hide show
  1. {alphaengine-0.9.3 → alphaengine-0.9.4}/PKG-INFO +1 -1
  2. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/_version.py +5 -1
  3. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/tui/app.py +136 -41
  4. {alphaengine-0.9.3 → alphaengine-0.9.4}/.github/workflows/ci.yml +0 -0
  5. {alphaengine-0.9.3 → alphaengine-0.9.4}/.github/workflows/publish.yml +0 -0
  6. {alphaengine-0.9.3 → alphaengine-0.9.4}/.gitignore +0 -0
  7. {alphaengine-0.9.3 → alphaengine-0.9.4}/AGENTS.md +0 -0
  8. {alphaengine-0.9.3 → alphaengine-0.9.4}/LICENSE +0 -0
  9. {alphaengine-0.9.3 → alphaengine-0.9.4}/README.md +0 -0
  10. {alphaengine-0.9.3 → alphaengine-0.9.4}/SECURITY.md +0 -0
  11. {alphaengine-0.9.3 → alphaengine-0.9.4}/docs/assets/banner.png +0 -0
  12. {alphaengine-0.9.3 → alphaengine-0.9.4}/docs/assets/data_boundary.png +0 -0
  13. {alphaengine-0.9.3 → alphaengine-0.9.4}/docs/assets/session.png +0 -0
  14. {alphaengine-0.9.3 → alphaengine-0.9.4}/docs/figure_contract.md +0 -0
  15. {alphaengine-0.9.3 → alphaengine-0.9.4}/docs/workflows/evaluate_process.json +0 -0
  16. {alphaengine-0.9.3 → alphaengine-0.9.4}/docs/workflows/research_week.json +0 -0
  17. {alphaengine-0.9.3 → alphaengine-0.9.4}/docs/workflows/stress_study.json +0 -0
  18. {alphaengine-0.9.3 → alphaengine-0.9.4}/pyproject.toml +0 -0
  19. {alphaengine-0.9.3 → alphaengine-0.9.4}/scripts/gen_docs.py +0 -0
  20. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/__init__.py +0 -0
  21. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/__main__.py +0 -0
  22. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/agent/__init__.py +0 -0
  23. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/agent/answer.py +0 -0
  24. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/agent/driver.py +0 -0
  25. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/auth.py +0 -0
  26. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/book.py +0 -0
  27. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/charts.py +0 -0
  28. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/cli.py +0 -0
  29. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/client/__init__.py +0 -0
  30. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/client/agent.py +0 -0
  31. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/client/executor.py +0 -0
  32. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/client/session.py +0 -0
  33. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/commands.py +0 -0
  34. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/complete.py +0 -0
  35. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/connectors/__init__.py +0 -0
  36. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/__init__.py +0 -0
  37. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/active.py +0 -0
  38. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/allocate.py +0 -0
  39. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/backtest.py +0 -0
  40. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/covariance.py +0 -0
  41. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/cross_section.py +0 -0
  42. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/factors.py +0 -0
  43. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/pairs.py +0 -0
  44. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/panel.py +0 -0
  45. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/performance.py +0 -0
  46. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/process.py +0 -0
  47. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/profile.py +0 -0
  48. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/risk.py +0 -0
  49. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/screen.py +0 -0
  50. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/series_shapes.py +0 -0
  51. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/signals.py +0 -0
  52. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/stress.py +0 -0
  53. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/technical.py +0 -0
  54. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/trace.py +0 -0
  55. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/validation.py +0 -0
  56. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/core/walkforward.py +0 -0
  57. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/demo.py +0 -0
  58. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/demo_book.py +0 -0
  59. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/demo_returns.py +0 -0
  60. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/demo_signal.py +0 -0
  61. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/demo_universe.py +0 -0
  62. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/events.py +0 -0
  63. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/loaders.py +0 -0
  64. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/model.py +0 -0
  65. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/py.typed +0 -0
  66. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/repl.py +0 -0
  67. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/study/__init__.py +0 -0
  68. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/study/report.py +0 -0
  69. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/study/schema.py +0 -0
  70. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/sweep/__init__.py +0 -0
  71. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/sweep/runner.py +0 -0
  72. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/tui/__init__.py +0 -0
  73. {alphaengine-0.9.3 → alphaengine-0.9.4}/src/alphaengine/tui/dispatch.py +0 -0
  74. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_agent.py +0 -0
  75. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_answer.py +0 -0
  76. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_book.py +0 -0
  77. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_cli.py +0 -0
  78. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_client.py +0 -0
  79. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_commands.py +0 -0
  80. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_events.py +0 -0
  81. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_executor_ops.py +0 -0
  82. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_goldens.py +0 -0
  83. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_loaders.py +0 -0
  84. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_math_audit.py +0 -0
  85. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_model.py +0 -0
  86. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_process.py +0 -0
  87. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_quant_maths.py +0 -0
  88. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_screen.py +0 -0
  89. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_signals.py +0 -0
  90. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_smoke.py +0 -0
  91. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_sweep.py +0 -0
  92. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_trace.py +0 -0
  93. {alphaengine-0.9.3 → alphaengine-0.9.4}/tests/test_tui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: alphaengine
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Validated research tooling for investment strategies: deflation, overfitting detection, and honest trial counts.
5
5
  Project-URL: Homepage, https://github.com/quantOSC/alphaengine
6
6
  Project-URL: Documentation, https://github.com/quantOSC/alphaengine#readme
@@ -88,4 +88,8 @@ is what a changed figure costs. The API may still move underneath it.
88
88
  # 0.9.3 IS THE SESSION, NOT THE MATHS. No computed value moved. The math pane
89
89
  # is narrower. A sentence is shown the workflow it is choosing and what a step
90
90
  # already measured, and a finished step is not run again.
91
- __version__ = "0.9.3"
91
+ #
92
+ # 0.9.4 IS THE SESSION, NOT THE MATHS. No computed value moved. The session
93
+ # opens on the desk: what is loaded, the thesis on its own lines, and the
94
+ # next thing to type.
95
+ __version__ = "0.9.4"
@@ -8,6 +8,7 @@ import re
8
8
  import time
9
9
  from typing import Any
10
10
 
11
+ from rich.text import Text
11
12
  from textual.app import App, ComposeResult
12
13
  from textual.containers import Horizontal, Vertical
13
14
  from textual.widgets import DataTable, Input, ListItem, ListView, RichLog, Sparkline, Static
@@ -17,23 +18,18 @@ from .dispatch import Desk, submit
17
18
 
18
19
  _ANSI = re.compile(r"\x1b\[[0-9;]*m")
19
20
 
21
+ # Kept as whole lines so a short window still shows the desk above them.
20
22
  _COMMANDS = (
21
23
  "login",
22
24
  "enter model key",
23
25
  "load <name>",
24
26
  "thesis",
25
- "screen",
26
- "diagnose",
27
- "signal",
28
- "validate",
29
- "stress",
30
- "overlap",
31
- "size",
32
- "monitor",
27
+ "screen diagnose signal",
28
+ "validate stress overlap",
29
+ "size monitor",
33
30
  "run <name>",
34
31
  "workflows",
35
- "help",
36
- "quit",
32
+ "help quit",
37
33
  )
38
34
  _URL_AFTER = {
39
35
  "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_ENDPOINT",
@@ -58,12 +54,13 @@ Screen { background: #0b0d10; }
58
54
  color: #c5cdd6;
59
55
  padding: 1 1;
60
56
  overflow: auto;
57
+ border-right: solid #1c2128;
61
58
  }
62
59
  #transcript {
63
60
  width: 1fr;
64
61
  height: 1fr;
65
62
  background: #0b0d10;
66
- padding: 0 1;
63
+ padding: 1 2 0 2;
67
64
  }
68
65
  #inspector {
69
66
  width: 30;
@@ -84,9 +81,9 @@ Screen { background: #0b0d10; }
84
81
  height: 1;
85
82
  background: #0b0d10;
86
83
  color: #6b7380;
87
- padding: 0 1;
84
+ padding: 0 2;
88
85
  }
89
- #prompt { dock: bottom; margin: 0 1; }
86
+ #prompt { dock: bottom; margin: 0 1 0 1; }
90
87
  """
91
88
 
92
89
 
@@ -101,6 +98,23 @@ def _fit(text: str, width: int = 22) -> str:
101
98
  return text[: width - 1] + "…"
102
99
 
103
100
 
101
+ def _wrap(text: str, width: int) -> list[str]:
102
+ words = " ".join(str(text).split()).split(" ")
103
+ if not words or words == [""]:
104
+ return []
105
+ lines: list[str] = []
106
+ current = ""
107
+ for word in words:
108
+ if current and len(current) + 1 + len(word) > width:
109
+ lines.append(current)
110
+ current = word
111
+ else:
112
+ current = f"{current} {word}".strip()
113
+ if current:
114
+ lines.append(current)
115
+ return lines
116
+
117
+
104
118
  class QuantOSApp(App[None]):
105
119
  """Conversation first. The math pane opens when a run has a line to show."""
106
120
 
@@ -139,6 +153,7 @@ class QuantOSApp(App[None]):
139
153
  self.rail_text = ""
140
154
  self.formula_text = ""
141
155
  self.transcript: list[str] = []
156
+ self.turns = 0
142
157
 
143
158
  def compose(self) -> ComposeResult:
144
159
  yield Static("", id="status")
@@ -152,18 +167,12 @@ class QuantOSApp(App[None]):
152
167
  yield Static("", id="inputs")
153
168
  yield Sparkline([], id="spark")
154
169
  yield DataTable(id="series", zebra_stripes=True)
155
- yield Static("enter send · help · quit", id="hint")
170
+ yield Static("", id="hint")
156
171
  yield Input(placeholder="Ask in plain English, or run <workflow>", id="prompt")
157
172
 
158
173
  def on_mount(self) -> None:
159
174
  self.query_one("#series", DataTable).cursor_type = "row"
160
175
  self._refresh_rail()
161
- log = self.query_one("#transcript", RichLog)
162
- log.write("Ask in plain English.")
163
- log.write("A sentence picks a workflow. run <name> follows it exactly.")
164
- log.write("login signs you in. enter model key pastes whichever key you use.")
165
- log.write("")
166
- log.write("help lists what you can type. quit leaves.")
167
176
  self.query_one(Input).focus()
168
177
  self.set_interval(0.2, self._tick_working)
169
178
  self._sync_account()
@@ -192,46 +201,58 @@ class QuantOSApp(App[None]):
192
201
  else:
193
202
  loaded = "nothing"
194
203
 
195
- lines = ["commands", *[f" {name}" for name in _COMMANDS], ""]
196
- lines += [
204
+ lines = [
205
+ "desk",
197
206
  f"{on} the maths",
198
- " offline, always",
199
- "",
207
+ " offline",
200
208
  f"{on if keyed else off} workflows",
201
209
  " ready" if keyed else " sign in",
202
- "",
203
210
  f"{on if ask_on else off} ask",
204
211
  f" {ask_note}",
205
212
  "",
206
213
  "loaded",
207
214
  f" {loaded}",
208
- "",
209
215
  "thesis",
210
- f" {_fit(thesis_label(self.desk.thesis) if self.desk.thesis else 'none')}",
211
- "",
212
- "universes",
213
216
  ]
217
+ if self.desk.thesis:
218
+ for piece in _wrap(thesis_label(self.desk.thesis), 22)[:3]:
219
+ lines.append(f" {piece}")
220
+ else:
221
+ lines.append(" none")
222
+ lines += ["", "universes"]
214
223
  if self._universes:
215
224
  for name in self._universes[:12]:
216
225
  mark = " stored" if name in self._stored else ""
217
226
  lines.append(f" {_fit(name, 20 - len(mark))}{mark}")
218
227
  else:
219
228
  lines.append(" none in reach")
229
+ lines += ["", "commands", *[f" {name}" for name in _COMMANDS]]
220
230
  if not keyed:
221
231
  lines += ["", "login", "signs you in"]
222
232
  self.rail_text = "\n".join(lines)
223
233
  self.query_one("#rail", Static).update(self.rail_text)
224
234
 
225
- bits = ["quantOS", "signed in" if keyed else "not signed in"]
235
+ bar = Text()
236
+ bar.append("quantOS", style="bold #9ecbff")
237
+ bar.append(" ", style="")
238
+ bar.append("signed in" if keyed else "not signed in", style="#c5cdd6" if keyed else "#e6b450")
226
239
  if self.desk.loaded:
227
- bits.append(self.desk.loaded)
240
+ bar.append(" ", style="")
241
+ bar.append(self.desk.loaded, style="#e6edf3")
228
242
  elif self.desk.data is not None:
229
- bits.append("data loaded")
230
- else:
231
- bits.append("no data")
243
+ bar.append(" data loaded", style="#e6edf3")
232
244
  if ask_on:
233
- bits.append(models[0][0])
234
- self.query_one("#status", Static).update(" · ".join(bits))
245
+ bar.append(" ", style="")
246
+ bar.append(models[0][0], style="#8b949e")
247
+ elif self.desk.thesis:
248
+ bar.append(" ", style="")
249
+ bar.append(_fit(thesis_label(self.desk.thesis), 28), style="#8b949e")
250
+ self.query_one("#status", Static).update(bar)
251
+ hint = Text(self._next_line(), style="#6b7380")
252
+ self.query_one("#hint", Static).update(hint)
253
+ prompt = self.query_one(Input)
254
+ if not (self.desk.pending_secret or self.desk.pending_provider or self.desk.pending_url):
255
+ prompt.placeholder = self._placeholder(ask_on)
235
256
 
236
257
  def on_input_submitted(self, event: Input.Submitted) -> None:
237
258
  text = event.value.strip()
@@ -247,6 +268,7 @@ class QuantOSApp(App[None]):
247
268
  if self.desk.pending_url:
248
269
  self._accept_url(text)
249
270
  return
271
+ self.turns += 1
250
272
  self.busy = True
251
273
  self.activity = text
252
274
  self._started = time.monotonic()
@@ -368,6 +390,76 @@ class QuantOSApp(App[None]):
368
390
  label, model = ready[0]
369
391
  log.write(f"Model ready: {label}/{model}. Ask in plain English.")
370
392
 
393
+ def _placeholder(self, ask_on: bool) -> str:
394
+ if self.desk.thesis and (ask_on or self.desk.think is not None):
395
+ return "Ask about this thesis"
396
+ if ask_on or self.desk.think is not None:
397
+ return "Ask in plain English, or run <workflow>"
398
+ if not self.desk.keyed:
399
+ return "login"
400
+ return "enter model key"
401
+
402
+ def _next_line(self) -> str:
403
+ if not self.desk.keyed:
404
+ return "login signs you in. enter model key pastes whichever key you use."
405
+ if self.desk.thesis:
406
+ return "Ask about this thesis. screen ranks the book. run <name> follows a workflow."
407
+ if self.desk.data is not None:
408
+ return "Ask about the loaded data. screen ranks it. run <name> follows a workflow."
409
+ return "Ask in plain English. A sentence picks a workflow. run <name> follows it exactly."
410
+
411
+ def _paint_desk(self, notes: list[str]) -> None:
412
+ """The opening card. State first, then the one next move. Not a manual."""
413
+ from ..cli import thesis_text
414
+
415
+ log = self.query_one("#transcript", RichLog)
416
+ log.clear()
417
+ head = Text()
418
+ head.append("quantOS", style="bold #9ecbff")
419
+ head.append(" research session", style="#6b7380")
420
+ log.write(head)
421
+ log.write("")
422
+
423
+ state = Text()
424
+ state.append("signed in" if self.desk.keyed else "not signed in", style="#e6edf3")
425
+ if self.desk.loaded:
426
+ state.append(" ", style="")
427
+ state.append(self.desk.loaded, style="#9ecbff")
428
+ elif self.desk.data is not None:
429
+ state.append(" series loaded", style="#9ecbff")
430
+ log.write(state)
431
+ log.write("")
432
+
433
+ if self.desk.thesis:
434
+ log.write(Text("thesis", style="bold #8b949e"))
435
+ name = thesis_label(self.desk.thesis)
436
+ for piece in _wrap(name, 62):
437
+ log.write(Text(piece, style="#e6edf3"))
438
+ statement = thesis_text(self.desk.thesis)
439
+ if statement and statement != name:
440
+ log.write("")
441
+ for piece in _wrap(statement, 66)[:5]:
442
+ log.write(Text(piece, style="#8b949e"))
443
+ else:
444
+ log.write(Text("no thesis pinned", style="#6b7380"))
445
+ log.write(Text("thesis <name> chooses one, when the account has several.", style="#6b7380"))
446
+
447
+ shown = False
448
+ for note in notes:
449
+ plain = _plain(note).strip()
450
+ if not plain or plain.lower().startswith("thesis "):
451
+ continue
452
+ if not shown:
453
+ log.write("")
454
+ shown = True
455
+ for piece in _wrap(plain, 66):
456
+ log.write(Text(piece, style="#8b949e"))
457
+
458
+ log.write("")
459
+ log.write(Text("─" * 28, style="#1c2128"))
460
+ for piece in _wrap(self._next_line(), 66):
461
+ log.write(Text(piece, style="#c5cdd6"))
462
+
371
463
  def _sync_account(self) -> None:
372
464
  """Refresh portal universes. One stored book loads itself."""
373
465
  from ..cli import _narrow_to_universe, _stored_universes, narration_to
@@ -419,10 +511,13 @@ class QuantOSApp(App[None]):
419
511
  named = ", ".join(thesis_label(r) for r in theses[:8])
420
512
  notes.append(f"Theses on your account: {named}. Type thesis <name> to use one.")
421
513
  log = self.query_one("#transcript", RichLog)
422
- for note in notes:
423
- plain = _plain(note)
424
- if plain.strip():
425
- self.transcript.append(plain)
514
+ kept = [_plain(note).strip() for note in notes if _plain(note).strip()]
515
+ for plain in kept:
516
+ self.transcript.append(plain)
517
+ if self.turns == 0:
518
+ self._paint_desk(kept)
519
+ else:
520
+ for plain in kept:
426
521
  log.write(plain)
427
522
  self._refresh_rail()
428
523
 
@@ -482,7 +577,7 @@ class QuantOSApp(App[None]):
482
577
  prompt.placeholder = f"paste {self.desk.pending_url}"
483
578
  else:
484
579
  prompt.password = False
485
- prompt.placeholder = "Ask in plain English, or run <workflow>"
580
+ self._refresh_rail()
486
581
  self.busy = False
487
582
  self.activity = ""
488
583
  if self.desk.account_dirty:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes