speclogician 0.0.0b1__py3-none-any.whl → 0.0.0.dev1__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 (153) hide show
  1. speclogician/agent/funcs.py +29 -0
  2. speclogician/cmd/agent_cmd.py +89 -0
  3. speclogician/cmd/data_cmd.py +24 -0
  4. speclogician/cmd/model_cmd.py +42 -0
  5. speclogician/cmd/overlay_cmd.py +30 -0
  6. speclogician/cmd/scenario_cmd.py +61 -0
  7. speclogician/cmd/state_cmd.py +52 -0
  8. speclogician/data/artifact.py +8 -50
  9. speclogician/data/container.py +18 -384
  10. speclogician/data/mapping.py +18 -17
  11. speclogician/data/refs.py +12 -11
  12. speclogician/data/reports.py +11 -0
  13. speclogician/data/traces.py +15 -6
  14. speclogician/llms/llmtools.py +102 -0
  15. speclogician/llms/overlay.py +264 -0
  16. speclogician/main.py +36 -102
  17. speclogician/modeling/__init__.py +0 -31
  18. speclogician/modeling/component.py +4 -60
  19. speclogician/modeling/conflict.py +5 -19
  20. speclogician/modeling/domain.py +93 -280
  21. speclogician/modeling/model.py +206 -0
  22. speclogician/modeling/predicates.py +20 -22
  23. speclogician/modeling/report.py +33 -0
  24. speclogician/modeling/scenario.py +119 -87
  25. speclogician/sl_cmd.py +76 -0
  26. speclogician/state/change.py +98 -378
  27. speclogician/state/state.py +183 -399
  28. speclogician/tui/box.tcss +10 -0
  29. speclogician/tui/tui.py +131 -0
  30. speclogician/utils/__init__.py +1 -70
  31. speclogician/utils/imx.py +195 -0
  32. speclogician/utils/load.py +25 -147
  33. speclogician/utils/prompt.md +1 -325
  34. speclogician-0.0.0.dev1.dist-info/METADATA +21 -0
  35. speclogician-0.0.0.dev1.dist-info/RECORD +43 -0
  36. speclogician/commands/__init__.py +0 -15
  37. speclogician/commands/cmd_ch.py +0 -616
  38. speclogician/commands/cmd_find.py +0 -256
  39. speclogician/commands/cmd_view.py +0 -202
  40. speclogician/commands/runner.py +0 -149
  41. speclogician/commands/utils.py +0 -101
  42. speclogician/demos/.DS_Store +0 -0
  43. speclogician/demos/cmd_demo.py +0 -278
  44. speclogician/demos/loader.py +0 -135
  45. speclogician/demos/model.py +0 -27
  46. speclogician/demos/runner.py +0 -51
  47. speclogician/logic/__init__.py +0 -11
  48. speclogician/logic/api/__init__.py +0 -29
  49. speclogician/logic/api/client.py +0 -606
  50. speclogician/logic/api/decomp.py +0 -67
  51. speclogician/logic/api/scenario.py +0 -102
  52. speclogician/logic/api/traces.py +0 -59
  53. speclogician/logic/lib/__init__.py +0 -19
  54. speclogician/logic/lib/complement.py +0 -107
  55. speclogician/logic/lib/domain_model.py +0 -59
  56. speclogician/logic/lib/predicates.py +0 -151
  57. speclogician/logic/lib/scenarios.py +0 -369
  58. speclogician/logic/lib/traces.py +0 -114
  59. speclogician/logic/lib/transitions.py +0 -104
  60. speclogician/logic/main.py +0 -246
  61. speclogician/logic/strings.py +0 -194
  62. speclogician/logic/utils.py +0 -135
  63. speclogician/modeling/complement.py +0 -104
  64. speclogician/modeling/spec.py +0 -306
  65. speclogician/modeling/spec_stats.py +0 -39
  66. speclogician/presentation/api.py +0 -244
  67. speclogician/presentation/builders/_links.py +0 -44
  68. speclogician/presentation/builders/container.py +0 -53
  69. speclogician/presentation/builders/data_artifact.py +0 -42
  70. speclogician/presentation/builders/domain.py +0 -54
  71. speclogician/presentation/builders/instances_list.py +0 -38
  72. speclogician/presentation/builders/predicate.py +0 -51
  73. speclogician/presentation/builders/recommendations.py +0 -41
  74. speclogician/presentation/builders/scenario.py +0 -41
  75. speclogician/presentation/builders/scenario_complement.py +0 -82
  76. speclogician/presentation/builders/smart_find.py +0 -39
  77. speclogician/presentation/builders/spec.py +0 -39
  78. speclogician/presentation/builders/state_diff.py +0 -150
  79. speclogician/presentation/builders/state_instance.py +0 -42
  80. speclogician/presentation/builders/state_instance_summary.py +0 -84
  81. speclogician/presentation/builders/trace.py +0 -58
  82. speclogician/presentation/ctx.py +0 -38
  83. speclogician/presentation/models/container.py +0 -44
  84. speclogician/presentation/models/data_artifact.py +0 -33
  85. speclogician/presentation/models/domain.py +0 -50
  86. speclogician/presentation/models/instances_list.py +0 -23
  87. speclogician/presentation/models/predicate.py +0 -60
  88. speclogician/presentation/models/recommendations.py +0 -34
  89. speclogician/presentation/models/scenario.py +0 -31
  90. speclogician/presentation/models/scenario_complement.py +0 -40
  91. speclogician/presentation/models/smart_find.py +0 -34
  92. speclogician/presentation/models/spec.py +0 -32
  93. speclogician/presentation/models/state_diff.py +0 -34
  94. speclogician/presentation/models/state_instance.py +0 -31
  95. speclogician/presentation/models/state_instance_summary.py +0 -102
  96. speclogician/presentation/models/trace.py +0 -42
  97. speclogician/presentation/preview/__init__.py +0 -13
  98. speclogician/presentation/preview/cli.py +0 -50
  99. speclogician/presentation/preview/fixtures/__init__.py +0 -205
  100. speclogician/presentation/preview/fixtures/artifact_container.py +0 -150
  101. speclogician/presentation/preview/fixtures/data_artifact.py +0 -144
  102. speclogician/presentation/preview/fixtures/domain_model.py +0 -162
  103. speclogician/presentation/preview/fixtures/instances_list.py +0 -162
  104. speclogician/presentation/preview/fixtures/predicate.py +0 -184
  105. speclogician/presentation/preview/fixtures/scenario.py +0 -84
  106. speclogician/presentation/preview/fixtures/scenario_complement.py +0 -81
  107. speclogician/presentation/preview/fixtures/smart_find.py +0 -140
  108. speclogician/presentation/preview/fixtures/spec.py +0 -95
  109. speclogician/presentation/preview/fixtures/state_diff.py +0 -158
  110. speclogician/presentation/preview/fixtures/state_instance.py +0 -128
  111. speclogician/presentation/preview/fixtures/state_instance_summary.py +0 -80
  112. speclogician/presentation/preview/fixtures/trace.py +0 -206
  113. speclogician/presentation/preview/registry.py +0 -42
  114. speclogician/presentation/renderers/__init__.py +0 -24
  115. speclogician/presentation/renderers/container.py +0 -136
  116. speclogician/presentation/renderers/data_artifact.py +0 -144
  117. speclogician/presentation/renderers/domain.py +0 -123
  118. speclogician/presentation/renderers/instances_list.py +0 -120
  119. speclogician/presentation/renderers/predicate.py +0 -180
  120. speclogician/presentation/renderers/recommendations.py +0 -90
  121. speclogician/presentation/renderers/scenario.py +0 -94
  122. speclogician/presentation/renderers/scenario_complement.py +0 -59
  123. speclogician/presentation/renderers/smart_find.py +0 -307
  124. speclogician/presentation/renderers/spec.py +0 -105
  125. speclogician/presentation/renderers/state_diff.py +0 -102
  126. speclogician/presentation/renderers/state_instance.py +0 -82
  127. speclogician/presentation/renderers/state_instance_summary.py +0 -143
  128. speclogician/presentation/renderers/trace.py +0 -122
  129. speclogician/shell/app.py +0 -170
  130. speclogician/shell/shell_ch.py +0 -263
  131. speclogician/shell/shell_view.py +0 -153
  132. speclogician/state/change_result.py +0 -32
  133. speclogician/state/diff.py +0 -191
  134. speclogician/state/inst.py +0 -574
  135. speclogician/state/recommendation.py +0 -13
  136. speclogician/state/recommender.py +0 -577
  137. speclogician/state/state_stats.py +0 -133
  138. speclogician/tui/__init__.py +0 -0
  139. speclogician/tui/app.py +0 -257
  140. speclogician/tui/app.tcss +0 -160
  141. speclogician/tui/demo.py +0 -45
  142. speclogician/tui/images/speclogician-full.png +0 -0
  143. speclogician/tui/images/speclogician-minimal.png +0 -0
  144. speclogician/tui/main_screen.py +0 -454
  145. speclogician/tui/splash_screen.py +0 -51
  146. speclogician/tui/stats_screen.py +0 -125
  147. speclogician/utils/testing.py +0 -151
  148. speclogician-0.0.0b1.dist-info/METADATA +0 -116
  149. speclogician-0.0.0b1.dist-info/RECORD +0 -139
  150. /speclogician/{presentation → agent}/__init__.py +0 -0
  151. /speclogician/{presentation/builders → cmd}/__init__.py +0 -0
  152. /speclogician/{presentation/models → llms}/__init__.py +0 -0
  153. {speclogician-0.0.0b1.dist-info → speclogician-0.0.0.dev1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,131 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # tui.py
5
+ #
6
+
7
+ from textual.app import App, ComposeResult
8
+ from textual.widgets import (
9
+ Footer, Header, TabbedContent, TabPane,Static, ListView, ListItem, Label
10
+ )
11
+ from textual.reactive import reactive
12
+ from textual.containers import VerticalScroll
13
+
14
+ from ..modeling.model import TestFormalization, TLState, Scenario
15
+ from pathlib import Path
16
+
17
+ class FileItem (ListItem):
18
+ def __init__(self, label: str) -> None:
19
+ super().__init__()
20
+ self.label = label
21
+
22
+ def compose( self ) -> ComposeResult:
23
+ yield Label(Path(self.label).name)
24
+
25
+
26
+ class DetailView(Static):
27
+ """
28
+ """
29
+
30
+ tf : reactive[TestFormalization | None] = reactive(None, always_update=True)
31
+
32
+ def watch_tf(self, _, tf:TestFormalization|None) -> None:
33
+ """
34
+ """
35
+ if tf is None: return
36
+ if not hasattr(self, 'view'): return
37
+
38
+ self.view.update(tf.__rich__())
39
+
40
+ def compose (self) -> ComposeResult:
41
+ """
42
+ """
43
+ with VerticalScroll():
44
+ self.view = Static("N/A")
45
+
46
+ yield self.view
47
+
48
+ class PredicateList(Static):
49
+ """
50
+ """
51
+
52
+ pred_list : reactive[ScenarioPredList | None] = reactive(None, always_update=True)
53
+
54
+ def compose(self) -> ComposeResult:
55
+ """
56
+ """
57
+ self.view =
58
+
59
+ yield self.view
60
+
61
+ class SpecLogicianApp(App):
62
+ """
63
+ """
64
+ CSS_PATH = "box.tcss"
65
+
66
+ def __init__ (self, tl_state : TLState, **kwargs):
67
+ super().__init__ (**kwargs)
68
+
69
+ self.tl_state = tl_state
70
+
71
+ def compose(self) -> ComposeResult:
72
+ """
73
+ """
74
+
75
+ yield Header()
76
+
77
+ with TabbedContent(initial="scenarios"):
78
+
79
+ with TabPane("Scenarios", id="scenarios"):
80
+ self.items = ListView (
81
+ *[FileItem(t.filepath) for t in self.tl_state.tfs()],
82
+ classes="box",
83
+ id="items"
84
+ )
85
+ yield self.items
86
+
87
+ self.detail = DetailView("hello", id="detail", classes="box")
88
+ yield self.detail
89
+
90
+ with TabPane("Domain Modle", id="model"):
91
+ self.domain_model = Static(self.tl_state.rich_model())
92
+ yield self.domain_model
93
+
94
+ with TabPane("Concrete Scenarios", id="concrete"):
95
+ self.concrete_scenarios = PredicateList()
96
+ self.concrete_scenarios.pred_list = self.tl_state.concrete()
97
+ yield self.concrete_scenarios
98
+
99
+ with TabPane("Predicate Scenarios", id="predicates"):
100
+ self.predicate_scenarios = PredicateList()
101
+ self.predciate_scenarios.pred_list = self.tl_state.predicates()
102
+ yield self.predicate_scenarios
103
+
104
+ with TabPane("Abstract Scenarios", id="abstract"):
105
+ self.abstract_predicates = PredicateList()
106
+ self.abstract_predicates.pred_list = self.tl_state.abstract()
107
+ yield self.abstract_predicates()
108
+
109
+ yield Footer()
110
+
111
+ # Initialize with first model
112
+ if len(self.tl_state.tfs()) > 0:
113
+ self.detail.tf = self.tl_state.tf_by_index(0)
114
+
115
+ def action_show_tab(self, tab: str) -> None:
116
+ """Switch to a new tab."""
117
+ self.get_child_by_type(TabbedContent).active = tab
118
+
119
+ def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
120
+ """ """
121
+ self.detail.tf = self.tl_state.tf_by_filepath(event.item.label)
122
+
123
+ def on_list_view_selected(self, event: ListView.Selected) -> None:
124
+ """ """
125
+ self.detail.tf = self.tl_state.tf_by_index(event.index)
126
+
127
+ if __name__ == "__main__":
128
+ sample_state = "../data/broadridge/integration/split_tests/tf_state.json"
129
+ state = TLState.fromJSON(Path(sample_state).read_text())
130
+ app = TestLogicianApp(state)
131
+ app.run()
@@ -4,75 +4,6 @@
4
4
  # utils/__init__.py
5
5
  #
6
6
 
7
- from __future__ import annotations
8
-
9
- import os
10
- import json
11
- import typer
12
- from typing import Any, TypeAlias
13
- from enum import Enum, StrEnum
14
7
  from rich.console import Console
15
- from rich.theme import Theme
16
-
17
- IMANDRA_KEY_ENV = "IMANDRA_UNI_KEY"
18
-
19
-
20
- console = Console(
21
- force_terminal=True,
22
- color_system="truecolor",
23
- theme=Theme({"info": "cyan", "ok": "green", "warn": "yellow", "err": "red"})
24
- )
25
-
26
- class IMLValidity (StrEnum):
27
- VALID = 'valid'
28
- INVALID = 'invalid'
29
- UNKNOWN = 'unknown'
30
-
31
-
32
- JSONScalar: TypeAlias = str | int | float | bool | None
33
- JSONValue: TypeAlias = JSONScalar | list["JSONValue"] | dict[str, "JSONValue"]
34
- JSONObject: TypeAlias = dict[str, JSONValue]
35
-
36
-
37
- def emit_json(obj: JSONObject) -> None:
38
- """
39
- Print JSON (never exits).
40
- Use this only at the CLI boundary or for debugging.
41
- """
42
- typer.echo(json.dumps(obj, indent=2, default=_json_default))
43
-
44
-
45
- def _json_default(o: Any) -> Any:
46
- # Enums (BaseStatus, PredicateType, etc)
47
- if isinstance(o, Enum):
48
- return o.value
49
- # datetime, Path, etc (best-effort string)
50
- try:
51
- return str(o)
52
- except Exception:
53
- return repr(o)
54
-
55
- def require_imandra_key() -> None:
56
- """
57
- Ensure IMANDRA_UNI_KEY is present in the environment.
58
- Exit the CLI with a clear message if not.
59
- """
60
- key = os.getenv(IMANDRA_KEY_ENV)
61
-
62
- if not key:
63
- console.print(
64
- "[red]✖ ImandraX API key missing[/red]\n\n"
65
- "SpecLogician requires an Imandra Universe API key to run.\n\n"
66
- "Please set the environment variable:\n\n"
67
- f" export {IMANDRA_KEY_ENV}=<your-api-key>\n\n"
68
- "You can obtain a key from Imandra Universe."
69
- )
70
- raise typer.Exit(code=2)
71
8
 
72
- __all__ = [
73
- "require_imandra_key",
74
- "emit_json",
75
- "_json_default",
76
- "IMLValidity",
77
- "console"
78
- ]
9
+ console = Console()
@@ -0,0 +1,195 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # imx.py
5
+ #
6
+
7
+ import asyncio
8
+ import sys
9
+ from typing import Literal, TypedDict, assert_never
10
+
11
+ import typer
12
+ from imandrax_api_models import DecomposeRes, InstanceRes, VerifyRes
13
+ from imandrax_api_models.client import (
14
+ ImandraXAsyncClient,
15
+ get_imandrax_async_client,
16
+ get_imandrax_client,
17
+ )
18
+ from imandrax_api_models.context_utils import (
19
+ format_decomp_res,
20
+ format_eval_res,
21
+ #format_vg_res,
22
+ )
23
+ from iml_query.processing import (
24
+ extract_decomp_reqs,
25
+ extract_instance_reqs,
26
+ extract_verify_reqs,
27
+ )
28
+ from iml_query.processing.decomp import DecompReqArgs
29
+ from iml_query.tree_sitter_utils import get_parser
30
+
31
+ from enum import StrEnum
32
+ class IMX_Status(StrEnum):
33
+ """
34
+ """
35
+ UNKNOWN = 'Unkown'
36
+ ADMITTED = 'Admitted'
37
+ ERROR = 'Error'
38
+
39
+
40
+ def check_model(model : str) -> bool:
41
+ """
42
+ Check that the model is admitted to ImandraX
43
+ """
44
+ c = get_imandrax_client()
45
+ eval_res = c.eval_model(src=model, with_vgs=False, with_decomps=False)
46
+
47
+ return format_eval_res(eval_res)
48
+
49
+ def run_eval(model : str, eval_str : str) -> str:
50
+ """
51
+ Run evaluation
52
+ """
53
+
54
+ c = get_imandrax_client()
55
+ eval_res = c.eval_src(model)
56
+
57
+ return ""
58
+
59
+
60
+ class VGItem(TypedDict):
61
+ kind: Literal["verify", "instance"]
62
+ src: str
63
+ start_point: tuple[int, int]
64
+ end_point: tuple[int, int]
65
+
66
+ def _collect_vgs(iml: str) -> list[VGItem]:
67
+ tree = get_parser().parse(iml.encode("utf-8"))
68
+ iml, tree, verify_reqs, verify_req_ranges = extract_verify_reqs(iml, tree)
69
+ iml, tree, instance_reqs, instance_req_ranges = extract_instance_reqs(iml, tree)
70
+
71
+ # Collect
72
+ vg_items: list[VGItem] = []
73
+ for req, req_range in zip(verify_reqs, verify_req_ranges, strict=True):
74
+ vg_items.append(
75
+ {
76
+ "kind": "verify",
77
+ "src": req["src"],
78
+ "start_point": (req_range.start_point[0], req_range.start_point[1]),
79
+ "end_point": (req_range.end_point[0], req_range.end_point[1]),
80
+ }
81
+ )
82
+ for req, req_range in zip(instance_reqs, instance_req_ranges, strict=False):
83
+ vg_items.append(
84
+ {
85
+ "kind": "instance",
86
+ "src": req["src"],
87
+ "start_point": (req_range.start_point[0], req_range.start_point[1]),
88
+ "end_point": (req_range.end_point[0], req_range.end_point[1]),
89
+ }
90
+ )
91
+ vg_items.sort(key=lambda x: x["start_point"])
92
+ return vg_items
93
+
94
+ def check_instance (model : str, instance_query : str) -> bool:
95
+ """
96
+ Run the instance request and return True/False if the instance is Sat
97
+ """
98
+ async def _async_check_vg() -> list[VerifyRes | InstanceRes]:
99
+ iml = model + "\n" + instance_query
100
+ vgs = _collect_vgs(iml)
101
+
102
+ vg_with_idx: list[tuple[int, VGItem]] = [
103
+ (i, vg) for (i, vg) in enumerate(vgs, 1)
104
+ ]
105
+
106
+ async def _check_vg(
107
+ vg: VGItem,
108
+ i: int,
109
+ c: ImandraXAsyncClient,
110
+ ) -> VerifyRes | InstanceRes:
111
+ match vg["kind"]:
112
+ case "verify":
113
+ res = await c.verify_src(src=vg["src"])
114
+ case "instance":
115
+ res = await c.instance_src(src=vg["src"])
116
+ case _:
117
+ assert_never(vg["kind"])
118
+ #print(f"{i}: {vg['kind']} ({vg['src']})")
119
+ #print(format_vg_res(res))
120
+ return res
121
+
122
+ async with get_imandrax_async_client() as c:
123
+ eval_res = await c.eval_model(src=iml)
124
+ #print(format_eval_res(eval_res, iml))
125
+ if eval_res.has_errors:
126
+ print("Error(s) found in IML file. Exiting.")
127
+ sys.exit(1)
128
+ return
129
+ #print("\n" + "=" * 5 + "VG" + "=" * 5 + "\n")
130
+ tasks = [_check_vg(vg, i, c) for (i, vg) in vg_with_idx]
131
+ return await asyncio.gather(*tasks)
132
+
133
+ vg_res_list = asyncio.run(_async_check_vg())
134
+
135
+ if len(vg_res_list) == 0:
136
+ raise Exception(f"Failed to get response")
137
+
138
+ return vg_res_list[0].res_type == 'sat'
139
+
140
+ class DecompItem(TypedDict):
141
+ req_args: DecompReqArgs
142
+ start_point: tuple[int, int]
143
+ end_point: tuple[int, int]
144
+
145
+ def _collect_decomps(iml: str) -> list[DecompItem]:
146
+ tree = get_parser().parse(iml.encode("utf-8"))
147
+ iml, tree, decomp_reqs, ranges = extract_decomp_reqs(iml, tree)
148
+
149
+ decomp_items: list[DecompItem] = [
150
+ DecompItem(
151
+ req_args=req,
152
+ start_point=range_.start_point,
153
+ end_point=range_.end_point,
154
+ )
155
+ for req, range_ in zip(decomp_reqs, ranges, strict=True)
156
+ ]
157
+
158
+ decomp_items.sort(key=lambda x: x["start_point"])
159
+ return decomp_items
160
+
161
+ def run_decomp(model : str, decomp_request : str):
162
+ """
163
+ Run the decomp request
164
+ """
165
+ async def _async_check_decomp() -> list[DecomposeRes]:
166
+ iml = model + "\n" + decomp_request
167
+ decomps = _collect_decomps(iml)
168
+
169
+ decomp_with_idx: list[tuple[int, DecompItem]] = [
170
+ (i, decomp) for (i, decomp) in enumerate(decomps, 1)
171
+ ]
172
+
173
+ async def _check_decomp(
174
+ decomp: DecompItem, i: int, c: ImandraXAsyncClient
175
+ ) -> DecomposeRes:
176
+ #print(f"{i}: decompose {decomp['req_args']['name']}")
177
+ res = await c.decompose(**decomp["req_args"])
178
+ #print(format_decomp_res(res))
179
+ return res
180
+
181
+ async with get_imandrax_async_client() as c:
182
+ eval_res = await c.eval_model(src=iml)
183
+ #print(format_eval_res(eval_res, iml))
184
+ if eval_res.has_errors:
185
+ typer.echo("Error(s) found in IML file. Exiting.")
186
+ sys.exit(1)
187
+ return
188
+
189
+ #print("\n" + "=" * 5 + "Decomp" + "=" * 5 + "\n")
190
+ tasks = [_check_decomp(decomp, i, c) for (i, decomp) in decomp_with_idx]
191
+ return await asyncio.gather(*tasks)
192
+
193
+ decomp_res_list = asyncio.run(_async_check_decomp())
194
+
195
+ return decomp_res_list[0]
@@ -1,166 +1,44 @@
1
1
  #
2
2
  # Imandra Inc.
3
3
  #
4
- # speclogician/utils/load.py
4
+ # load.py
5
5
  #
6
6
 
7
- import json
8
7
  import os
9
- from pathlib import Path
10
-
11
- import typer
12
- from typing import NoReturn
8
+ import sys
13
9
  from rich.prompt import Prompt
10
+ from pathlib import Path
14
11
 
15
12
  from ..state.state import State
13
+ from ..llms.overlay import Overlays
16
14
  from .__init__ import console
17
15
 
18
-
19
- def _emit_json(obj: dict, exit_code: int = 0) -> NoReturn:
20
- """Print JSON and exit (useful for CLI / tests)."""
21
- typer.echo(json.dumps(obj, indent=2))
22
- raise typer.Exit(code=exit_code)
23
-
24
-
25
- def load_state(path: Path | None = None, json: bool = False) -> State:
26
- """
27
- Load a State either from an explicit JSON path or from the local directory.
28
-
29
- - If `path` is provided:
30
- * if it exists: load JSON from it
31
- * if it doesn't exist: ask to create it (unless json)
32
- - If `path` is None:
33
- * load from current directory via State.from_dir()
34
- * if missing: ask to create it (unless json)
35
-
36
- If json=True:
37
- - emit JSON on success/error and Exit (no prompts).
38
- """
39
- cwd = Path(os.getcwd())
40
-
41
- def create_new_state(target: Path | None) -> State:
42
- """
43
- Create a new state and persist it.
44
- NOTE: Your State.save() appears to accept a directory path; we use:
45
- - cwd when target is None
46
- - target.parent when target is a file path
47
- """
48
- state = State()
49
-
50
- if target is None:
51
- state.save(str(cwd))
52
- return state
53
-
54
- target = target.expanduser()
55
- target.parent.mkdir(parents=True, exist_ok=True)
56
- state.save(str(target.parent))
57
- return state
58
-
59
- # -------------------------
60
- # Case 1: explicit file path
61
- # -------------------------
62
- if path is not None:
63
- path = path.expanduser()
64
-
65
- if path.exists():
66
- try:
67
- if path.is_dir():
68
- new_state = State.from_dir(dirpath=path)
69
- else:
70
- new_state = State.from_json(path.read_text())
71
- except Exception as e:
72
- if json:
73
- _emit_json(
74
- {
75
- "ok": False,
76
- "error": "failed_to_load_state_json",
77
- "path": str(path),
78
- "message": str(e),
79
- },
80
- exit_code=2,
81
- )
82
- console.print(f"[red]Failed to load state JSON:[/] {path}\n{e}")
83
- raise
84
-
85
- if json:
86
- _emit_json(
87
- {"ok": True, "source": "state_json", "path": str(path)},
88
- exit_code=0,
89
- )
90
- return new_state
91
-
92
- # File doesn't exist
93
- if json:
94
- _emit_json(
95
- {
96
- "ok": False,
97
- "error": "state_json_not_found",
98
- "path": str(path),
99
- "message": "State JSON file does not exist.",
100
- },
101
- exit_code=2,
102
- )
103
-
16
+ def load_state():
17
+ """ """
18
+ new_state = State.from_dir(dirpath=os.getcwd())
19
+ if new_state is None:
104
20
  answer = Prompt.ask(
105
- prompt=f"⚠️ State file not found at {path}. Create a new one?",
106
- choices=["Yes", "No"],
21
+ prompt="⚠️ Couldnt find a state in current directory. Create a new one?",
22
+ choices = ["Yes", "No"],
107
23
  console=console,
108
24
  case_sensitive=False,
109
- default="Yes",
110
- )
111
- if answer.lower() == "no":
112
- console.print("Goodbye!")
113
- raise typer.Exit(0)
25
+ default="Yes"
26
+ )
114
27
 
28
+ if answer == 'No':
29
+ console.print("Goodbye!")
30
+ sys.exit(0)
31
+
115
32
  console.print("Creating a new state!")
116
- return create_new_state(path)
117
33
 
118
- # -------------------------
119
- # Case 2: default local dir
120
- # -------------------------
121
- try:
122
- new_state = State.from_dir(dirpath=str(cwd))
123
- except Exception as e:
124
- if json:
125
- _emit_json(
126
- {
127
- "ok": False,
128
- "error": "failed_to_load_state_from_dir",
129
- "dir": str(cwd),
130
- "message": str(e),
131
- },
132
- exit_code=2,
133
- )
134
- console.print(f"[red]Failed to load state from directory:[/] {cwd}\n{e}")
135
- raise
136
-
137
- if new_state is not None:
138
- if json:
139
- _emit_json({"ok": True, "source": "dir", "dir": str(cwd)}, exit_code=0)
34
+ state = State()
35
+ state.save(os.getcwd())
36
+ return state
37
+
38
+ else:
140
39
  return new_state
141
40
 
142
- # Not found in cwd
143
- if json:
144
- _emit_json(
145
- {
146
- "ok": False,
147
- "error": "state_not_found_in_dir",
148
- "dir": str(cwd),
149
- "message": "No state found in current directory.",
150
- },
151
- exit_code=2,
152
- )
153
-
154
- answer = Prompt.ask(
155
- prompt="⚠️ Couldn't find a state in current directory. Create a new one?",
156
- choices=["Yes", "No"],
157
- console=console,
158
- case_sensitive=False,
159
- default="Yes",
160
- )
161
- if answer.lower() == "no":
162
- console.print("Goodbye!")
163
- raise typer.Exit(0)
164
-
165
- console.print("Creating a new state!")
166
- return create_new_state(None)
41
+ def load_overlays():
42
+ """
43
+ """
44
+ return Overlays.from_dir(str(Path(os.path.abspath(__file__)).parent / "../../overlays"))