dialectical-framework 0.4.4__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 (84) hide show
  1. dialectical_framework/__init__.py +43 -0
  2. dialectical_framework/ai_dto/__init__.py +0 -0
  3. dialectical_framework/ai_dto/causal_cycle_assessment_dto.py +16 -0
  4. dialectical_framework/ai_dto/causal_cycle_dto.py +16 -0
  5. dialectical_framework/ai_dto/causal_cycles_deck_dto.py +11 -0
  6. dialectical_framework/ai_dto/dialectical_component_dto.py +16 -0
  7. dialectical_framework/ai_dto/dialectical_components_deck_dto.py +12 -0
  8. dialectical_framework/ai_dto/dto_mapper.py +103 -0
  9. dialectical_framework/ai_dto/reciprocal_solution_dto.py +24 -0
  10. dialectical_framework/analyst/__init__.py +1 -0
  11. dialectical_framework/analyst/audit/__init__.py +0 -0
  12. dialectical_framework/analyst/consultant.py +30 -0
  13. dialectical_framework/analyst/decorator_action_reflection.py +28 -0
  14. dialectical_framework/analyst/decorator_discrete_spiral.py +30 -0
  15. dialectical_framework/analyst/domain/__init__.py +0 -0
  16. dialectical_framework/analyst/domain/assessable_cycle.py +128 -0
  17. dialectical_framework/analyst/domain/cycle.py +133 -0
  18. dialectical_framework/analyst/domain/interpretation.py +16 -0
  19. dialectical_framework/analyst/domain/rationale.py +116 -0
  20. dialectical_framework/analyst/domain/spiral.py +47 -0
  21. dialectical_framework/analyst/domain/transformation.py +24 -0
  22. dialectical_framework/analyst/domain/transition.py +160 -0
  23. dialectical_framework/analyst/domain/transition_cell_to_cell.py +29 -0
  24. dialectical_framework/analyst/domain/transition_segment_to_segment.py +16 -0
  25. dialectical_framework/analyst/strategic_consultant.py +32 -0
  26. dialectical_framework/analyst/think_action_reflection.py +217 -0
  27. dialectical_framework/analyst/think_constructive_convergence.py +87 -0
  28. dialectical_framework/analyst/wheel_builder_transition_calculator.py +157 -0
  29. dialectical_framework/brain.py +81 -0
  30. dialectical_framework/dialectical_analysis.py +14 -0
  31. dialectical_framework/dialectical_component.py +111 -0
  32. dialectical_framework/dialectical_components_deck.py +48 -0
  33. dialectical_framework/dialectical_reasoning.py +105 -0
  34. dialectical_framework/directed_graph.py +419 -0
  35. dialectical_framework/enums/__init__.py +0 -0
  36. dialectical_framework/enums/causality_type.py +10 -0
  37. dialectical_framework/enums/di.py +11 -0
  38. dialectical_framework/enums/dialectical_reasoning_mode.py +9 -0
  39. dialectical_framework/enums/predicate.py +9 -0
  40. dialectical_framework/protocols/__init__.py +0 -0
  41. dialectical_framework/protocols/assessable.py +141 -0
  42. dialectical_framework/protocols/causality_sequencer.py +111 -0
  43. dialectical_framework/protocols/content_fidelity_evaluator.py +16 -0
  44. dialectical_framework/protocols/has_brain.py +18 -0
  45. dialectical_framework/protocols/has_config.py +18 -0
  46. dialectical_framework/protocols/ratable.py +79 -0
  47. dialectical_framework/protocols/reloadable.py +7 -0
  48. dialectical_framework/protocols/thesis_extractor.py +15 -0
  49. dialectical_framework/settings.py +77 -0
  50. dialectical_framework/synthesis.py +7 -0
  51. dialectical_framework/synthesist/__init__.py +1 -0
  52. dialectical_framework/synthesist/causality/__init__.py +0 -0
  53. dialectical_framework/synthesist/causality/causality_sequencer_balanced.py +398 -0
  54. dialectical_framework/synthesist/causality/causality_sequencer_desirable.py +55 -0
  55. dialectical_framework/synthesist/causality/causality_sequencer_feasible.py +55 -0
  56. dialectical_framework/synthesist/causality/causality_sequencer_realistic.py +56 -0
  57. dialectical_framework/synthesist/concepts/__init__.py +0 -0
  58. dialectical_framework/synthesist/concepts/thesis_extractor_basic.py +135 -0
  59. dialectical_framework/synthesist/polarity/__init__.py +0 -0
  60. dialectical_framework/synthesist/polarity/polarity_reasoner.py +700 -0
  61. dialectical_framework/synthesist/polarity/reason_blind.py +62 -0
  62. dialectical_framework/synthesist/polarity/reason_conversational.py +91 -0
  63. dialectical_framework/synthesist/polarity/reason_fast.py +177 -0
  64. dialectical_framework/synthesist/polarity/reason_fast_and_simple.py +52 -0
  65. dialectical_framework/synthesist/polarity/reason_fast_polarized_conflict.py +55 -0
  66. dialectical_framework/synthesist/reverse_engineer.py +401 -0
  67. dialectical_framework/synthesist/wheel_builder.py +337 -0
  68. dialectical_framework/utils/__init__.py +1 -0
  69. dialectical_framework/utils/dc_replace.py +42 -0
  70. dialectical_framework/utils/decompose_probability_uniformly.py +15 -0
  71. dialectical_framework/utils/extend_tpl.py +12 -0
  72. dialectical_framework/utils/gm.py +12 -0
  73. dialectical_framework/utils/is_async.py +13 -0
  74. dialectical_framework/utils/use_brain.py +80 -0
  75. dialectical_framework/validator/__init__.py +1 -0
  76. dialectical_framework/validator/basic_checks.py +75 -0
  77. dialectical_framework/validator/check.py +12 -0
  78. dialectical_framework/wheel.py +395 -0
  79. dialectical_framework/wheel_segment.py +203 -0
  80. dialectical_framework/wisdom_unit.py +185 -0
  81. dialectical_framework-0.4.4.dist-info/LICENSE +21 -0
  82. dialectical_framework-0.4.4.dist-info/METADATA +123 -0
  83. dialectical_framework-0.4.4.dist-info/RECORD +84 -0
  84. dialectical_framework-0.4.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,395 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Union, Any, Dict
4
+
5
+ from tabulate import tabulate
6
+
7
+ from dialectical_framework.analyst.domain.cycle import Cycle
8
+ from dialectical_framework.analyst.domain.spiral import Spiral
9
+ from dialectical_framework.analyst.domain.transition import Transition
10
+ from dialectical_framework.dialectical_component import DialecticalComponent
11
+ from dialectical_framework.protocols.assessable import Assessable
12
+ from dialectical_framework.utils.gm import gm_with_zeros_and_nones_handled
13
+ from dialectical_framework.wheel_segment import WheelSegment
14
+ from dialectical_framework.wisdom_unit import WisdomUnit
15
+
16
+ WheelSegmentReference = Union[int, WheelSegment, str, DialecticalComponent]
17
+
18
+
19
+ class Wheel(Assessable):
20
+ def __init__(self, *wisdom_units, t_cycle: Cycle, ta_cycle: Cycle, **kwargs):
21
+ super().__init__(**kwargs)
22
+
23
+ # One iterable argument → use it directly
24
+ if len(wisdom_units) == 1 and not isinstance(wisdom_units[0], WisdomUnit):
25
+ self._wisdom_units: list[WisdomUnit] = list(wisdom_units[0])
26
+ else:
27
+ self._wisdom_units: list[WisdomUnit] = list(wisdom_units)
28
+
29
+ self._ta_cycle: Cycle = ta_cycle
30
+ self._t_cycle: Cycle = t_cycle
31
+ self._spiral: Spiral = Spiral()
32
+
33
+ @property
34
+ def order(self) -> int:
35
+ """The order of the wheel (number of wisdom units in the dialectical structure)"""
36
+ if len(self._wisdom_units) == 0:
37
+ raise ValueError("The wheel is empty, therefore order is undefined.")
38
+ return len(self._wisdom_units)
39
+
40
+ @property
41
+ def degree(self) -> int:
42
+ """The degree of the wheel (total number of segments = 2 × order)"""
43
+ return self.order * 2
44
+
45
+ def _get_sub_assessables(self) -> list[Assessable]:
46
+ result = super()._get_sub_assessables()
47
+ result.extend(self._wisdom_units)
48
+ result.append(self._t_cycle)
49
+ result.append(self._ta_cycle)
50
+ result.append(self._spiral)
51
+ return result
52
+
53
+ def _calculate_contextual_fidelity_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
54
+ """
55
+ Calculates the WheelFidelity as the geometric mean of:
56
+ 1. All individual DialecticalComponent contextual fidelity scores across the entire wheel
57
+ 2. All wheel-level rationales/opinions (weighted by their rating)
58
+
59
+ Components with contextual_fidelity of 0.0 or None are excluded from the calculation.
60
+ """
61
+ parts = []
62
+
63
+ # Collect from wisdom units
64
+ for wu in self._wisdom_units:
65
+ fidelity = wu.calculate_contextual_fidelity(mutate=mutate)
66
+ parts.append(fidelity)
67
+
68
+ # Collect transitions from cycles without overlaps
69
+ unique_transitions: Dict[Any, Transition] = {}
70
+
71
+ # Get transitions from t_cycle (most generic)
72
+ if self._t_cycle.graph and not self._t_cycle.graph.is_empty():
73
+ for transition in self._t_cycle.graph.get_all_transitions():
74
+ unique_transitions[transition.get_key()] = transition
75
+
76
+ # Get transitions from ta_cycle (more specific than t_cycle, so we prefer this)
77
+ if self._ta_cycle.graph and not self._ta_cycle.graph.is_empty():
78
+ for transition in self._ta_cycle.graph.get_all_transitions():
79
+ if transition.get_key() in unique_transitions:
80
+ ta_fidelity = transition.calculate_contextual_fidelity(mutate=mutate)
81
+ if ta_fidelity == 1.0:
82
+ # no effect, we can stick to moe generic level, whatever fidelity will be there, it will be (more) correct
83
+ pass
84
+ else:
85
+ # calculate the one which is being overwritten
86
+ unique_transitions[transition.get_key()].calculate_contextual_fidelity(mutate=mutate)
87
+ # take the more specific one
88
+ unique_transitions[transition.get_key()] = transition
89
+
90
+ # Get transitions from spiral (even more specific, than ta_cycle, so we prefer this)
91
+ if self._spiral.graph and not self._spiral.graph.is_empty():
92
+ for transition in self._spiral.graph.get_all_transitions():
93
+ if transition.get_key() in unique_transitions:
94
+ sp_fidelity = transition.calculate_contextual_fidelity(mutate=mutate)
95
+ if sp_fidelity == 1.0:
96
+ # no effect, we can stick to moe generic level, whatever fidelity will be there, it will be (more) correct
97
+ pass
98
+ else:
99
+ # calculate the one which is being overwritten
100
+ unique_transitions[transition.get_key()].calculate_contextual_fidelity(mutate=mutate)
101
+ # take the more specific one
102
+ unique_transitions[transition.get_key()] = transition
103
+
104
+ # Extract fidelity scores from unique transitions
105
+ for transition in unique_transitions.values():
106
+ transition_fidelity = transition.calculate_contextual_fidelity(mutate=mutate)
107
+ if transition_fidelity is not None and transition_fidelity > 0.0:
108
+ parts.append(transition_fidelity)
109
+
110
+
111
+ return parts
112
+
113
+ def calculate_probability(self, *, mutate: bool = True) -> float | None:
114
+ """
115
+ Wheel Pr = GM over a fixed canonical set of cycle probabilities (T, TA, Spiral).
116
+ - Keep 0.0 (impossibility ⇒ 0).
117
+ - Skip None (unknown).
118
+ - If all canonical cycles are None ⇒ None.
119
+ Optionally add a single summarized term for unit transformations (see note).
120
+ """
121
+ # 1) Canonical cycles (ensure these are EXTERNAL transitions only)
122
+ canonical_vals = []
123
+ for cyc in (self._t_cycle, self._ta_cycle, self._spiral):
124
+ p = cyc.calculate_probability(mutate=mutate)
125
+ if p is not None:
126
+ canonical_vals.append(p)
127
+
128
+ # 2) Optional: summarize unit transformations ONCE (only if not already in canonical cycles)
129
+ internal_summary = None
130
+ unit_vals = []
131
+ for wu in self._wisdom_units:
132
+ p = wu.calculate_probability(mutate=mutate) # Pr of the unit’s internal 2-edge Transformation
133
+ if p is not None:
134
+ unit_vals.append(p)
135
+ if unit_vals:
136
+ internal_summary = gm_with_zeros_and_nones_handled(unit_vals)
137
+
138
+ # Build final list (omit internal_summary if your canonical cycles include internal edges)
139
+ all_terms = list(canonical_vals)
140
+ if internal_summary is not None:
141
+ all_terms.append(internal_summary)
142
+
143
+ probability = gm_with_zeros_and_nones_handled(all_terms)
144
+
145
+ if mutate:
146
+ self.probability = probability
147
+ return probability
148
+
149
+ @property
150
+ def wisdom_units(self) -> list[WisdomUnit]:
151
+ return self._wisdom_units
152
+
153
+ @property
154
+ def main_wisdom_unit(self) -> WisdomUnit:
155
+ if len(self._wisdom_units) > 0:
156
+ return self._wisdom_units[0]
157
+ else:
158
+ raise ValueError("The wheel is empty.")
159
+
160
+ def is_set(self, s: str | DialecticalComponent | WheelSegment) -> bool:
161
+ try:
162
+ self.wisdom_unit_at(s)
163
+ except ValueError:
164
+ return False
165
+ else:
166
+ return True
167
+
168
+ def is_same_structure(self, other: Wheel) -> bool:
169
+ if len(self.wisdom_units) != len(other.wisdom_units):
170
+ return False
171
+ for wu in self.wisdom_units:
172
+ if not other.is_set(wu):
173
+ return False
174
+
175
+ return self.t_cycle.is_same_structure(
176
+ other.t_cycle
177
+ ) and self.cycle.is_same_structure(other.cycle)
178
+
179
+ def wisdom_unit_at(self, i: WheelSegmentReference) -> WisdomUnit:
180
+ """
181
+ Determines and retrieves a WisdomUnit based on the input index or key.
182
+
183
+ This method identifies and returns a specific WisdomUnit from the collection
184
+ of WisdomUnits maintained by the object. The input can be provided as an integer
185
+ index, string key, or an instance of the WheelSegment, with distinct lookup
186
+ logic applied for each type. If the input does not correspond to
187
+ a valid WisdomUnit, an exception is raised.
188
+
189
+ Parameters:
190
+ i : int | str | WheelSegment
191
+ The input used to locate a specific WisdomUnit. Can be an integer
192
+ index of a wisdom unit, a string alias of a dialectical component, or an instance of the WheelSegment.
193
+
194
+ Returns:
195
+ WisdomUnit
196
+ The WisdomUnit corresponding to the provided input.
197
+
198
+ Raises:
199
+ IndexError
200
+ If the integer input is out of range for the collection of WisdomUnits.
201
+ ValueError
202
+ If the input of type WheelSegment or string does not correspond to
203
+ a valid WisdomUnit in the collection.
204
+ """
205
+ if isinstance(i, WisdomUnit):
206
+ for wu in self.wisdom_units:
207
+ if wu.is_same(i):
208
+ return wu
209
+ elif isinstance(i, WheelSegment):
210
+ for wu in self.wisdom_units:
211
+ if wu.extract_segment_t().is_same(i) or wu.extract_segment_a().is_same(
212
+ i
213
+ ):
214
+ return wu
215
+ raise ValueError(f"Cannot find wisdom unit at: {i.t.alias}")
216
+ elif isinstance(i, str) or isinstance(i, DialecticalComponent):
217
+ for wu in self.wisdom_units:
218
+ if wu.is_set(i):
219
+ return wu
220
+ elif isinstance(i, int):
221
+ if i < 0 or i >= len(self.wisdom_units):
222
+ raise IndexError(
223
+ f"index {i} out of range for wheel of length {len(self.wisdom_units)}"
224
+ )
225
+ return self.wisdom_units[i]
226
+
227
+ raise ValueError(f"Cannot find wisdom unit at: {i}")
228
+
229
+ def wheel_segment_at(self, i: int | str | DialecticalComponent) -> WheelSegment:
230
+ """
231
+ Retrieves a specific wheel segment from the wisdom units based on the provided index or component.
232
+
233
+ The method allows accessing a wheel segment by an integer index, a string, or a DialecticalComponent.
234
+ If an integer index is provided, it is validated to ensure it lies within the appropriate range and then
235
+ used to determine the specific segment from a sequence of wisdom units. For string or DialecticalComponent
236
+ inputs, the method searches through the wisdom units to locate and return the segment containing the
237
+ specified component.
238
+
239
+ Raises:
240
+ IndexError: If the integer index is out of the valid range.
241
+ ValueError: If no wisdom unit or segment corresponding to the given identifier is found.
242
+
243
+ Args:
244
+ i: The index of the wheel segment as an integer or the identifier of the component
245
+ as a string or DialecticalComponent.
246
+
247
+ Returns:
248
+ The matching WheelSegment extracted from the appropriate wisdom unit.
249
+ """
250
+ if isinstance(i, int):
251
+ total_segments = self.degree
252
+ if i < 0 or i >= total_segments:
253
+ raise IndexError(
254
+ f"index {i} out of range for wheel of {total_segments} segments"
255
+ )
256
+ wu_index = i % self.order
257
+ wu = self.wisdom_units[wu_index]
258
+ return wu.extract_segment_t() if i < self.order else wu.extract_segment_a()
259
+ elif isinstance(i, str) or isinstance(i, DialecticalComponent):
260
+ for wu in self.wisdom_units:
261
+ if wu.is_set(i):
262
+ segment_t = wu.extract_segment_t()
263
+ if segment_t.is_set(i):
264
+ return segment_t
265
+ segment_a = wu.extract_segment_a()
266
+ if segment_a.is_set(i):
267
+ return segment_a
268
+ raise ValueError(f"Cannot find wisdom unit at: {i}")
269
+
270
+ @property
271
+ def orthogonal_wisdom_unit(self) -> WisdomUnit:
272
+ """
273
+ Raises:
274
+ ValueError: If the number of segments is not even.
275
+
276
+ Returns:
277
+ WisdomUnit: The orthogonal segment
278
+ """
279
+ n = len(self._wisdom_units)
280
+ if n == 0:
281
+ raise ValueError(
282
+ "The wheel is empty, therefore no orthogonal segment exists."
283
+ )
284
+ if n % 2 == 0:
285
+ return self._wisdom_units[n // 2]
286
+ else:
287
+ raise ValueError("The wheel is not balanced orthogonally.")
288
+
289
+ def spin(
290
+ self,
291
+ offset: int = 1,
292
+ ) -> list[WisdomUnit]:
293
+ # TODO: do we ned to also adjust the cycle and spiral?
294
+ """
295
+ Rotate the synthesis-pair list by ``offset`` positions.
296
+
297
+ Parameters
298
+ ----------
299
+ offset : int
300
+ How far to rotate. Positive values rotate left; negative values
301
+ rotate right.
302
+ """
303
+ n = len(self._wisdom_units)
304
+ if n == 0:
305
+ raise ValueError("Cannot spin an empty wheel")
306
+
307
+ if not -n <= offset < n:
308
+ raise IndexError(
309
+ f"spin offset {offset} out of range for list of length {n}"
310
+ )
311
+
312
+ offset %= n # bring offset into the list's range
313
+
314
+ def rot(lst: List) -> List:
315
+ return lst[offset:] + lst[:offset]
316
+
317
+ self._wisdom_units[:] = rot(self._wisdom_units)
318
+
319
+ return self._wisdom_units
320
+
321
+ @property
322
+ def cycle(self) -> Cycle:
323
+ return self._ta_cycle
324
+
325
+ @property
326
+ def t_cycle(self) -> Cycle:
327
+ return self._t_cycle
328
+
329
+ @property
330
+ def spiral(self) -> Spiral:
331
+ return self._spiral
332
+
333
+ def index_of(self, ws: WheelSegment) -> int:
334
+ for i, wu in enumerate(self._wisdom_units):
335
+ if wu.extract_segment_t().is_same(ws):
336
+ return i
337
+ if wu.extract_segment_a().is_same(ws):
338
+ return i + self.order
339
+ # Should never happen
340
+ return -1
341
+
342
+ def __str__(self):
343
+ main_segment = self.main_wisdom_unit
344
+ output = (
345
+ "\n---\n"
346
+ + self.t_cycle.pretty(
347
+ skip_dialectical_component_explanation=True, start_alias=main_segment.t
348
+ )
349
+ + "\n---\n"
350
+ + "\n---\n"
351
+ + self.cycle.pretty(
352
+ skip_dialectical_component_explanation=True, start_alias=main_segment.t
353
+ )
354
+ + "\n---\n"
355
+ + self._print_wheel_tabular()
356
+ + "\n---\n"
357
+ + self.spiral.pretty(start_wheel_segment=main_segment)
358
+ + "\n---\n"
359
+ )
360
+
361
+ return output
362
+
363
+ def _print_wheel_tabular(self) -> str:
364
+ roles = [
365
+ ("t_minus", "T-"),
366
+ ("t", "T"),
367
+ ("t_plus", "T+"),
368
+ ("a_plus", "A+"),
369
+ ("a", "A"),
370
+ ("a_minus", "A-"),
371
+ ]
372
+
373
+ n_units = len(self._wisdom_units)
374
+
375
+ # Create headers: WU1_alias, WU1_statement, (transition1), WU2_alias, ...
376
+ headers = []
377
+ for i in range(n_units):
378
+ headers.extend([f"Alias (WU{i + 1})", f"Statement (WU{i + 1})"])
379
+
380
+ table = []
381
+ # Build the table: alternate wisdom unit cells and transitions
382
+ for role_attr, role_label in roles:
383
+ row = []
384
+ for i, wu in enumerate(self._wisdom_units):
385
+ # Wisdom unit columns
386
+ component = getattr(wu, role_attr, None)
387
+ row.append(component.alias if component else "")
388
+ row.append(component.statement if component else "")
389
+ table.append(row)
390
+
391
+ return tabulate(
392
+ table,
393
+ # headers=headers,
394
+ tablefmt="plain",
395
+ )
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import ConfigDict, Field
4
+ from pydantic.fields import FieldInfo
5
+
6
+ from dialectical_framework.dialectical_component import DialecticalComponent
7
+ from dialectical_framework.protocols.assessable import Assessable
8
+
9
+ ALIAS_T = "T"
10
+ ALIAS_T_PLUS = "T+"
11
+ ALIAS_T_MINUS = "T-"
12
+
13
+
14
+ class WheelSegment(Assessable):
15
+
16
+ model_config = ConfigDict(
17
+ extra="forbid",
18
+ populate_by_name=True,
19
+ )
20
+
21
+ def __setattr__(self, name, value):
22
+ # If the attribute name is an alias, use the corresponding field name
23
+ if name in self.alias_to_field:
24
+ super().__setattr__(self.alias_to_field[name], value)
25
+ else:
26
+ # Otherwise use the default behavior
27
+ super().__setattr__(name, value)
28
+
29
+ t_minus: DialecticalComponent | None = Field(
30
+ default=None,
31
+ description="The negative side of the thesis: T-",
32
+ alias=ALIAS_T_MINUS,
33
+ )
34
+ t: DialecticalComponent | None = Field(
35
+ default=None, description="The major thesis of the input: T", alias=ALIAS_T
36
+ )
37
+ t_plus: DialecticalComponent | None = Field(
38
+ default=None,
39
+ description="The positive side of the thesis: T+",
40
+ alias=ALIAS_T_PLUS,
41
+ )
42
+
43
+ def _get_sub_assessables(self) -> list[Assessable]:
44
+ result = super()._get_sub_assessables()
45
+ if self.t:
46
+ result.append(self.t)
47
+ if self.t_minus:
48
+ result.append(self.t_minus)
49
+ if self.t_plus:
50
+ result.append(self.t_plus)
51
+ return result
52
+
53
+ def _calculate_contextual_fidelity_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
54
+ """
55
+ Calculates the context fidelity score for this wheel segment as the geometric mean
56
+ of its constituent DialecticalComponent's scores, including those from its synthesis,
57
+ and weighted rationale opinions.
58
+ Components with a context_fidelity_score of 0.0 or None are excluded from the calculation.
59
+ """
60
+ parts = []
61
+
62
+ # Collect from dialectical components
63
+ for f in self.field_to_alias.keys():
64
+ dc = getattr(self, f)
65
+ if isinstance(dc, DialecticalComponent):
66
+ fidelity = dc.calculate_contextual_fidelity(mutate=mutate)
67
+ parts.append(fidelity)
68
+
69
+ return parts
70
+
71
+ def calculate_probability(self, *, mutate: bool = True) -> float | None:
72
+ """
73
+ There are no transitions, so we treat it as a fact
74
+ """
75
+
76
+ # A statement is a fact, so probability is always 1.0
77
+ probability = self.probability if self.probability is not None else 1.0
78
+
79
+ if mutate:
80
+ self.probability = probability
81
+
82
+ return probability
83
+
84
+ def _get_dialectical_fields(self) -> dict[str, FieldInfo]:
85
+ """Get only fields that contain DialecticalComponent instances and have aliases."""
86
+ return {
87
+ field_name: field_info
88
+ for field_name, field_info in self.__pydantic_fields__.items()
89
+ if hasattr(field_info, "alias") and field_info.alias is not None
90
+ }
91
+
92
+ def is_complete(self):
93
+ return all(v is not None for v in self.model_dump(exclude_none=False).values())
94
+
95
+ def is_same(self, other: WheelSegment) -> bool:
96
+ if self == other:
97
+ return True
98
+ if type(self) != type(other):
99
+ return False
100
+
101
+ for field in self.alias_to_field.values():
102
+ dc_self = self.get(field)
103
+ dc_other = other.get(field)
104
+ if dc_self is None and dc_other is None:
105
+ continue
106
+ if dc_self is None or dc_other is None:
107
+ return False
108
+ if not dc_self.is_same(dc_other):
109
+ return False
110
+ return True
111
+
112
+ def is_set(self, key: str | DialecticalComponent) -> bool:
113
+ """
114
+ True if the given field/alias exists **and** its value is not ``None``.
115
+ >>> ws = WheelSegment()
116
+ >>> ws.is_set("T")
117
+ >>> ws.is_set("t")
118
+ """
119
+ if isinstance(key, DialecticalComponent):
120
+ return (
121
+ any(
122
+ getattr(self, field).is_same(key)
123
+ for field in self.alias_to_field.values()
124
+ if getattr(self, field) is not None
125
+ )
126
+ or self.get(key.alias, None) is not None
127
+ )
128
+ else:
129
+ return self.get(key, None) is not None
130
+
131
+ def get(
132
+ self, key: str, default: object | None = None
133
+ ) -> DialecticalComponent | None:
134
+ """
135
+ Dictionary-style accessor that understands both *field names* and *aliases*.
136
+
137
+ Examples
138
+ --------
139
+ >>> ws = WheelSegment()
140
+ >>> ws.get("t") # by field name
141
+ >>> ws.get("T") # by alias
142
+ """
143
+ field_name: str = self.alias_to_field.get(key, key)
144
+ if hasattr(self, field_name):
145
+ value = getattr(self, field_name)
146
+ return value if value is not None else default
147
+ return default
148
+
149
+ @property
150
+ def field_to_alias(self) -> dict[str, str]:
151
+ return {
152
+ field_name: field_info.alias
153
+ for field_name, field_info in self._get_dialectical_fields().items()
154
+ }
155
+
156
+ @property
157
+ def alias_to_field(self) -> dict[str, str]:
158
+ return {
159
+ field_info.alias: field_name
160
+ for field_name, field_info in self._get_dialectical_fields().items()
161
+ }
162
+
163
+ def find_component_by_alias(self, alias: str) -> DialecticalComponent | None:
164
+ """
165
+ Helper method to find a dialectical component within a wheel segment by its alias.
166
+ """
167
+ for f, a in self.field_to_alias.items():
168
+ dc = getattr(self, f)
169
+ if isinstance(dc, DialecticalComponent):
170
+ if dc.alias == alias:
171
+ return dc
172
+
173
+ return None
174
+
175
+ def add_indexes_to_aliases(self, human_friendly_index: int):
176
+ """
177
+ Updates the aliases of dialectical components with an index to make them unique.
178
+
179
+ This method iterates over the field-to-alias mapping of the object and modifies the alias
180
+ of attributes that are instances of DialecticalComponent. The modified alias includes a
181
+ human-friendly index appended to its base while preserving any existing sign.
182
+
183
+ If the index was there, it will be overwritten.
184
+
185
+ Parameters:
186
+ human_friendly_index: int
187
+ An index to append to the base of the alias to ensure uniqueness.
188
+ """
189
+ for f, a in self.field_to_alias.items():
190
+ dc = getattr(self, f)
191
+ if isinstance(dc, DialecticalComponent):
192
+ dc.set_human_friendly_index(human_friendly_index)
193
+
194
+ def pretty(self) -> str:
195
+ ws_formatted = []
196
+ for f, a in self.field_to_alias.items():
197
+ dc = getattr(self, f)
198
+ if isinstance(dc, DialecticalComponent):
199
+ ws_formatted.append(dc.pretty())
200
+ return "\n\n".join(ws_formatted)
201
+
202
+ def __str__(self):
203
+ return self.pretty()