speclogician 0.0.0b1__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 (139) hide show
  1. speclogician/__init__.py +0 -0
  2. speclogician/commands/__init__.py +15 -0
  3. speclogician/commands/cmd_ch.py +616 -0
  4. speclogician/commands/cmd_find.py +256 -0
  5. speclogician/commands/cmd_view.py +202 -0
  6. speclogician/commands/runner.py +149 -0
  7. speclogician/commands/utils.py +101 -0
  8. speclogician/data/__init__.py +0 -0
  9. speclogician/data/artifact.py +63 -0
  10. speclogician/data/container.py +402 -0
  11. speclogician/data/mapping.py +88 -0
  12. speclogician/data/refs.py +24 -0
  13. speclogician/data/traces.py +26 -0
  14. speclogician/demos/.DS_Store +0 -0
  15. speclogician/demos/cmd_demo.py +278 -0
  16. speclogician/demos/loader.py +135 -0
  17. speclogician/demos/model.py +27 -0
  18. speclogician/demos/runner.py +51 -0
  19. speclogician/logic/__init__.py +11 -0
  20. speclogician/logic/api/__init__.py +29 -0
  21. speclogician/logic/api/client.py +606 -0
  22. speclogician/logic/api/decomp.py +67 -0
  23. speclogician/logic/api/scenario.py +102 -0
  24. speclogician/logic/api/traces.py +59 -0
  25. speclogician/logic/lib/__init__.py +19 -0
  26. speclogician/logic/lib/complement.py +107 -0
  27. speclogician/logic/lib/domain_model.py +59 -0
  28. speclogician/logic/lib/predicates.py +151 -0
  29. speclogician/logic/lib/scenarios.py +369 -0
  30. speclogician/logic/lib/traces.py +114 -0
  31. speclogician/logic/lib/transitions.py +104 -0
  32. speclogician/logic/main.py +246 -0
  33. speclogician/logic/strings.py +194 -0
  34. speclogician/logic/utils.py +135 -0
  35. speclogician/main.py +139 -0
  36. speclogician/modeling/__init__.py +31 -0
  37. speclogician/modeling/complement.py +104 -0
  38. speclogician/modeling/component.py +71 -0
  39. speclogician/modeling/conflict.py +26 -0
  40. speclogician/modeling/domain.py +349 -0
  41. speclogician/modeling/predicates.py +59 -0
  42. speclogician/modeling/scenario.py +162 -0
  43. speclogician/modeling/spec.py +306 -0
  44. speclogician/modeling/spec_stats.py +39 -0
  45. speclogician/presentation/__init__.py +0 -0
  46. speclogician/presentation/api.py +244 -0
  47. speclogician/presentation/builders/__init__.py +0 -0
  48. speclogician/presentation/builders/_links.py +44 -0
  49. speclogician/presentation/builders/container.py +53 -0
  50. speclogician/presentation/builders/data_artifact.py +42 -0
  51. speclogician/presentation/builders/domain.py +54 -0
  52. speclogician/presentation/builders/instances_list.py +38 -0
  53. speclogician/presentation/builders/predicate.py +51 -0
  54. speclogician/presentation/builders/recommendations.py +41 -0
  55. speclogician/presentation/builders/scenario.py +41 -0
  56. speclogician/presentation/builders/scenario_complement.py +82 -0
  57. speclogician/presentation/builders/smart_find.py +39 -0
  58. speclogician/presentation/builders/spec.py +39 -0
  59. speclogician/presentation/builders/state_diff.py +150 -0
  60. speclogician/presentation/builders/state_instance.py +42 -0
  61. speclogician/presentation/builders/state_instance_summary.py +84 -0
  62. speclogician/presentation/builders/trace.py +58 -0
  63. speclogician/presentation/ctx.py +38 -0
  64. speclogician/presentation/models/__init__.py +0 -0
  65. speclogician/presentation/models/container.py +44 -0
  66. speclogician/presentation/models/data_artifact.py +33 -0
  67. speclogician/presentation/models/domain.py +50 -0
  68. speclogician/presentation/models/instances_list.py +23 -0
  69. speclogician/presentation/models/predicate.py +60 -0
  70. speclogician/presentation/models/recommendations.py +34 -0
  71. speclogician/presentation/models/scenario.py +31 -0
  72. speclogician/presentation/models/scenario_complement.py +40 -0
  73. speclogician/presentation/models/smart_find.py +34 -0
  74. speclogician/presentation/models/spec.py +32 -0
  75. speclogician/presentation/models/state_diff.py +34 -0
  76. speclogician/presentation/models/state_instance.py +31 -0
  77. speclogician/presentation/models/state_instance_summary.py +102 -0
  78. speclogician/presentation/models/trace.py +42 -0
  79. speclogician/presentation/preview/__init__.py +13 -0
  80. speclogician/presentation/preview/cli.py +50 -0
  81. speclogician/presentation/preview/fixtures/__init__.py +205 -0
  82. speclogician/presentation/preview/fixtures/artifact_container.py +150 -0
  83. speclogician/presentation/preview/fixtures/data_artifact.py +144 -0
  84. speclogician/presentation/preview/fixtures/domain_model.py +162 -0
  85. speclogician/presentation/preview/fixtures/instances_list.py +162 -0
  86. speclogician/presentation/preview/fixtures/predicate.py +184 -0
  87. speclogician/presentation/preview/fixtures/scenario.py +84 -0
  88. speclogician/presentation/preview/fixtures/scenario_complement.py +81 -0
  89. speclogician/presentation/preview/fixtures/smart_find.py +140 -0
  90. speclogician/presentation/preview/fixtures/spec.py +95 -0
  91. speclogician/presentation/preview/fixtures/state_diff.py +158 -0
  92. speclogician/presentation/preview/fixtures/state_instance.py +128 -0
  93. speclogician/presentation/preview/fixtures/state_instance_summary.py +80 -0
  94. speclogician/presentation/preview/fixtures/trace.py +206 -0
  95. speclogician/presentation/preview/registry.py +42 -0
  96. speclogician/presentation/renderers/__init__.py +24 -0
  97. speclogician/presentation/renderers/container.py +136 -0
  98. speclogician/presentation/renderers/data_artifact.py +144 -0
  99. speclogician/presentation/renderers/domain.py +123 -0
  100. speclogician/presentation/renderers/instances_list.py +120 -0
  101. speclogician/presentation/renderers/predicate.py +180 -0
  102. speclogician/presentation/renderers/recommendations.py +90 -0
  103. speclogician/presentation/renderers/scenario.py +94 -0
  104. speclogician/presentation/renderers/scenario_complement.py +59 -0
  105. speclogician/presentation/renderers/smart_find.py +307 -0
  106. speclogician/presentation/renderers/spec.py +105 -0
  107. speclogician/presentation/renderers/state_diff.py +102 -0
  108. speclogician/presentation/renderers/state_instance.py +82 -0
  109. speclogician/presentation/renderers/state_instance_summary.py +143 -0
  110. speclogician/presentation/renderers/trace.py +122 -0
  111. speclogician/py.typed +0 -0
  112. speclogician/shell/app.py +170 -0
  113. speclogician/shell/shell_ch.py +263 -0
  114. speclogician/shell/shell_view.py +153 -0
  115. speclogician/state/__init__.py +0 -0
  116. speclogician/state/change.py +428 -0
  117. speclogician/state/change_result.py +32 -0
  118. speclogician/state/diff.py +191 -0
  119. speclogician/state/inst.py +574 -0
  120. speclogician/state/recommendation.py +13 -0
  121. speclogician/state/recommender.py +577 -0
  122. speclogician/state/state.py +465 -0
  123. speclogician/state/state_stats.py +133 -0
  124. speclogician/tui/__init__.py +0 -0
  125. speclogician/tui/app.py +257 -0
  126. speclogician/tui/app.tcss +160 -0
  127. speclogician/tui/demo.py +45 -0
  128. speclogician/tui/images/speclogician-full.png +0 -0
  129. speclogician/tui/images/speclogician-minimal.png +0 -0
  130. speclogician/tui/main_screen.py +454 -0
  131. speclogician/tui/splash_screen.py +51 -0
  132. speclogician/tui/stats_screen.py +125 -0
  133. speclogician/utils/__init__.py +78 -0
  134. speclogician/utils/load.py +166 -0
  135. speclogician/utils/prompt.md +325 -0
  136. speclogician/utils/testing.py +151 -0
  137. speclogician-0.0.0b1.dist-info/METADATA +116 -0
  138. speclogician-0.0.0b1.dist-info/RECORD +139 -0
  139. speclogician-0.0.0b1.dist-info/WHEEL +4 -0
speclogician/main.py ADDED
@@ -0,0 +1,139 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/main.py
5
+ #
6
+
7
+ __version__ = "1.0"
8
+
9
+ import typer
10
+ from typing import Final
11
+ from pathlib import Path
12
+
13
+ from speclogician.commands import ch_app, view_app, find_app
14
+ from speclogician.presentation.preview import preview_app
15
+ from speclogician.demos.cmd_demo import app as demo_app
16
+ from speclogician.utils import console, require_imandra_key
17
+ from speclogician.utils.load import load_state
18
+ from speclogician.state.state import State
19
+
20
+
21
+ SL_HELP : Final = """
22
+ :robot: [bold deep_sky_blue1][italic]SpecLogician[/italic] is an AI framework for data-driven
23
+ formal program specification synthesis, verification, and analysis.[/bold deep_sky_blue1] :rocket:
24
+
25
+ SpecLogician provides an LLM-native alternative to classical predicate abstraction and
26
+ Counterexample-Guided Abstraction Refinement (CEGAR).
27
+
28
+ Instead of requiring users or agents to discover and maintain global Boolean abstractions,
29
+ SpecLogician models behavior directly using structured scenarios composed from state and
30
+ action predicates. Specifications are built incrementally by defining a domain base
31
+ (types and core invariants), predicates, transition functions, and scenarios that capture
32
+ intent explicitly.
33
+
34
+ Each change creates a new immutable state, triggers precise logical analysis, and produces
35
+ a semantic diff against the previous state. Using ImandraX’s exact symbolic reasoning and
36
+ region decomposition, SpecLogician computes coverage, conflicts, reachability, and
37
+ behavioral complements—identifying real, reachable behaviors not yet explained by any
38
+ scenario.
39
+
40
+ Rather than reacting to spurious counterexamples, users and AI agents receive positive,
41
+ actionable refinement signals in the form of uncovered or inconsistent behavioral regions.
42
+ This enables scalable, explainable refinement toward correctness and completeness across
43
+ source code, tests, documentation, and real-world traces—using small, explicit change
44
+ commands instead of manual file editing.
45
+
46
+ Learn more at [bold italic deep_sky_blue1]https://www.speclogician.dev![/bold italic deep_sky_blue1]
47
+ """
48
+
49
+ app = typer.Typer(
50
+ name="SpecLogician",
51
+ help=SL_HELP,
52
+ rich_markup_mode="rich",
53
+ no_args_is_help=True
54
+ )
55
+
56
+ class AppCtx:
57
+ state_json_path: Path | None = None
58
+ state: State | None = None
59
+
60
+ @app.callback()
61
+ def _cli_entry() -> None:
62
+ """
63
+ SpecLogician CLI entry point.
64
+ """
65
+ require_imandra_key()
66
+
67
+ @app.callback()
68
+ def main(ctx: typer.Context, state_json: Path | None = typer.Option(None, "--state-json", help="...")):
69
+ ctx.obj = ctx.ensure_object(AppCtx)
70
+
71
+ ctx.obj.state_json_path = state_json
72
+
73
+ if state_json:
74
+ try:
75
+ ctx.obj.state = load_state(path=state_json)
76
+ except Exception:
77
+ ctx.obj.state = None
78
+ else:
79
+ ctx.obj.state = None # ✅ ensure attribute exists
80
+
81
+ app.add_typer(view_app , name="view" , help="View components of the state")
82
+ app.add_typer(ch_app , name="ch" , help="Apply change to the state")
83
+ app.add_typer(find_app , name="find" , help="Search for a components of the state")
84
+ app.add_typer(demo_app , name="demo" , help="A collection of SpecLogician demos")
85
+ app.add_typer(preview_app , name="preview" , hidden=True) # This is an internal tool for manually inspecting renderers
86
+
87
+
88
+ @app.command(help="Run the TUI")
89
+ def tui ():
90
+ """ Run the TUI to help us navigate and explore the state """
91
+ from speclogician.tui.app import SpecLogicianApp
92
+ tui_app = SpecLogicianApp()
93
+ tui_app.run()
94
+
95
+ @app.command(help="Generate a prompt for helping agents use SpecLogician")
96
+ def prompt(
97
+ out: Path | None = typer.Option(
98
+ None,
99
+ "--out",
100
+ help="Write the prompt to a file instead of stdout.",
101
+ ),
102
+ ):
103
+ """
104
+ Generate a prompt for helping CLI agents use SpecLogician.
105
+ """
106
+
107
+ here = Path(__file__).resolve()
108
+ repo_root = here.parents[0]
109
+ prompt_path = repo_root / "utils" / "prompt.md"
110
+
111
+ try:
112
+ txt = prompt_path.read_text()
113
+ except Exception as e:
114
+ console.print(f"🛑 Failed to read prompt file at {prompt_path}: {e}")
115
+ raise typer.Exit(2)
116
+
117
+ if out:
118
+ try:
119
+ out.write_text(txt)
120
+ except Exception as e:
121
+ console.print(f"🛑 Failed to write prompt to {out}: {e}")
122
+ raise typer.Exit(2)
123
+ typer.echo(str(out))
124
+ else:
125
+ typer.echo(txt)
126
+
127
+
128
+ @app.command()
129
+ def version(
130
+ json_only: bool = typer.Option(False, "--json", help="Output JSON"),
131
+ ):
132
+ """Print SpecLogician version"""
133
+ if json_only:
134
+ typer.echo(f'{{"version": "{__version__}"}}')
135
+ else:
136
+ typer.echo(__version__)
137
+
138
+ if __name__ == '__main__':
139
+ app()
@@ -0,0 +1,31 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/modeling/__init__.py
5
+ #
6
+
7
+
8
+ import re
9
+ from typing import List
10
+
11
+ IML_TYPE_REGEX = re.compile(
12
+ r"""
13
+ ^\s*type\s+ # 'type' keyword
14
+ (?P<name>[a-zA-Z_][a-zA-Z0-9_]*) # type name
15
+ \s*= # '=' sign
16
+ """,
17
+ re.MULTILINE | re.VERBOSE,
18
+ )
19
+
20
+ def strip_iml_comments(text: str) -> str:
21
+ """Remove OCaml-style (* ... *) and // comments."""
22
+ # Remove block comments
23
+ text = re.sub(r"\(\*.*?\*\)", "", text, flags=re.DOTALL)
24
+ # Remove line comments
25
+ text = re.sub(r"//.*", "", text)
26
+ return text
27
+
28
+ def extract_declared_types(iml_text: str) -> List[str]:
29
+ """Return all declared type names in an IML file."""
30
+ clean = strip_iml_comments(iml_text)
31
+ return [m.group("name") for m in IML_TYPE_REGEX.finditer(clean)]
@@ -0,0 +1,104 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/modeling/complement.py
5
+ #
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ from typing import Sequence, Iterable
12
+
13
+ from pydantic import BaseModel
14
+ from imandrax_api_models import RegionStr
15
+
16
+
17
+ class ScenarioComplement(BaseModel):
18
+ """Result of running region decomposition on all the combined scenarios."""
19
+ regions: Sequence[RegionStr]
20
+
21
+
22
+ class ScenarioComplementDiff(BaseModel):
23
+ """
24
+ Difference between two scenario complements (before -> after),
25
+ computed using stable fingerprints of regions.
26
+
27
+ We store only fingerprints by default (regions can be huge).
28
+ """
29
+ num_regions_before: int
30
+ num_regions_after: int
31
+ regions_added: list[str] # fingerprints that appear in after but not before
32
+ regions_removed: list[str] # fingerprints that appear in before but not after
33
+
34
+
35
+ def _canon_region_str(r: RegionStr) -> str:
36
+ """
37
+ Produce a deterministic string representation of a RegionStr.
38
+
39
+ We prefer a canonical JSON-ish blob rather than str(r) because:
40
+ - __repr__ / __str__ may change
41
+ - dict ordering may vary
42
+ """
43
+ # constraints_str is typically a list of strings
44
+ constraints = list(getattr(r, "constraints_str", []) or [])
45
+
46
+ invariant = getattr(r, "invariant_str", "") or ""
47
+ model_eval = getattr(r, "model_eval_str", "") or ""
48
+
49
+ # model_str can be dict-like (often {'s': '{x = 0}'}). Canonicalize ordering.
50
+ model = getattr(r, "model_str", None)
51
+ if isinstance(model, dict):
52
+ # stringify values to avoid non-jsonable shapes
53
+ model_items = {str(k): str(v) for k, v in sorted(model.items(), key=lambda kv: str(kv[0]))}
54
+ elif model is None:
55
+ model_items = {}
56
+ else:
57
+ # fallback: stringify
58
+ model_items = {"_": str(model)}
59
+
60
+ payload = {
61
+ "constraints": constraints,
62
+ "invariant": invariant,
63
+ "model": model_items,
64
+ "model_eval": model_eval,
65
+ }
66
+
67
+ # separators for stable compact encoding
68
+ return json.dumps(payload, sort_keys=True, separators=(",", ":"))
69
+
70
+
71
+ def _fingerprint_region(r: RegionStr | str) -> str:
72
+ """
73
+ Stable-ish fingerprint for a RegionStr (or a raw string region).
74
+ """
75
+ if isinstance(r, RegionStr):
76
+ s = _canon_region_str(r)
77
+ else:
78
+ s = str(r)
79
+
80
+ h = hashlib.sha256(s.encode("utf-8")).hexdigest()
81
+ # short prefix is plenty for diffs + UI, but keep full if you prefer
82
+ return h[:16]
83
+
84
+
85
+ def _fingerprints(regions: Iterable[RegionStr | str]) -> set[str]:
86
+ return {_fingerprint_region(r) for r in regions}
87
+
88
+
89
+ def diff_scenario_complement(before: ScenarioComplement, after: ScenarioComplement) -> ScenarioComplementDiff:
90
+ before_regions = list(before.regions or [])
91
+ after_regions = list(after.regions or [])
92
+
93
+ bf = _fingerprints(before_regions)
94
+ af = _fingerprints(after_regions)
95
+
96
+ added = sorted(list(af - bf))
97
+ removed = sorted(list(bf - af))
98
+
99
+ return ScenarioComplementDiff(
100
+ num_regions_before=len(before_regions),
101
+ num_regions_after=len(after_regions),
102
+ regions_added=added,
103
+ regions_removed=removed,
104
+ )
@@ -0,0 +1,71 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/modeling/component.py
5
+ #
6
+
7
+ import uuid
8
+ import re
9
+ from datetime import datetime, timezone
10
+ from pydantic import BaseModel, Field, field_validator, ConfigDict
11
+ from ..utils import IMLValidity
12
+
13
+ _NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
14
+
15
+ # Conservative reserved set (OCaml-ish + common IML surface syntax).
16
+ # Add more as you run into them.
17
+ _IML_RESERVED: set[str] = {
18
+ # core keywords / syntax
19
+ "let", "in", "and", "rec",
20
+ "type", "match", "with", "function",
21
+ "if", "then", "else",
22
+ "try", "raise",
23
+ "module", "open", "include",
24
+ "begin", "end",
25
+ "as", "of", "when",
26
+
27
+ # booleans / common literals
28
+ "true", "false",
29
+
30
+ # common types / constructors that are often special-cased
31
+ "some", "none",
32
+ }
33
+
34
+ def validate_iml_identifier(name: str) -> str:
35
+ if not _NAME_RE.fullmatch(name):
36
+ raise ValueError(
37
+ f"Invalid name '{name}'. Must start with a lowercase letter and contain only "
38
+ "lowercase letters, digits, and underscores."
39
+ )
40
+ if name in _IML_RESERVED:
41
+ raise ValueError(f"Invalid name '{name}': reserved keyword in IML/OCaml surface syntax.")
42
+ return name
43
+
44
+ class ModelComponent(BaseModel):
45
+ """
46
+ Base class for all model components (Predicates, transitions and scenarios)
47
+ """
48
+ model_config = ConfigDict(validate_assignment=True) # optional but recommended
49
+
50
+ comp_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
51
+ name: str
52
+
53
+ # Track last update time (UTC)
54
+ last_updated: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
55
+
56
+ @field_validator("name")
57
+ @classmethod
58
+ def validate_name(cls, v: str) -> str:
59
+ return validate_iml_identifier(v)
60
+
61
+ def touch(self) -> None:
62
+ self.last_updated = datetime.now(timezone.utc)
63
+
64
+ class SrcComponent(ModelComponent):
65
+ src_code: str = ""
66
+ is_iml_valid : IMLValidity = IMLValidity.UNKNOWN
67
+ iml_error: str | None = None
68
+
69
+ def set_src_code(self, src_code: str) -> None:
70
+ self.src_code = src_code
71
+ self.touch()
@@ -0,0 +1,26 @@
1
+ #
2
+ # Imandra Inc.
3
+ #
4
+ # speclogician/modeling/conflict.py
5
+ #
6
+
7
+ from pydantic import BaseModel
8
+ from typing import Literal, TypeAlias
9
+
10
+ class ScenarioOverlap(BaseModel):
11
+ """ Two scenarios overlap in constraints """
12
+ kind: Literal["overlap"] = "overlap"
13
+ scenario_name1 : str
14
+ scenario_name2 : str
15
+
16
+ details : str
17
+
18
+ class ScenarioConsumed(BaseModel):
19
+ """ One scenario fully absorbs another """
20
+ kind: Literal["consumed"] = "consumed"
21
+ scenario_name1 : str
22
+ scenario_name2 : str
23
+
24
+ details : str
25
+
26
+ ScenarioConflict : TypeAlias = ScenarioOverlap | ScenarioConsumed