dialectical-framework 2.0.0__tar.gz → 2.0.2__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 (228) hide show
  1. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/PKG-INFO +1 -1
  2. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/pyproject.toml +1 -1
  3. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/consultant/consultant.py +47 -11
  4. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/conversation_facilitator.py +6 -0
  5. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/control_statements_check.py +189 -23
  6. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/tetrad_candidates.py +79 -34
  7. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/view_sketch.py +38 -8
  8. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/views.py +6 -0
  9. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/bedrock_provider.py +80 -5
  10. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/LICENSE +0 -0
  11. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/PATENTS +0 -0
  12. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/README.md +0 -0
  13. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/__init__.py +0 -0
  14. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/__init__.py +0 -0
  15. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/__init__.py +0 -0
  16. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/advisor.py +0 -0
  17. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/build_policy.py +0 -0
  18. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/migration.py +0 -0
  19. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/reply_hygiene.py +0 -0
  20. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/system_prompts.py +0 -0
  21. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/__init__.py +0 -0
  22. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/anchor.py +0 -0
  23. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/deepen.py +0 -0
  24. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/explore.py +0 -0
  25. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/ingest.py +0 -0
  26. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/note.py +0 -0
  27. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/record_decision.py +0 -0
  28. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/scoped.py +0 -0
  29. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/advisor/tools/sync.py +0 -0
  30. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/agent_context.py +0 -0
  31. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/__init__.py +0 -0
  32. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/analyst.py +0 -0
  33. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/skills/__init__.py +0 -0
  34. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/skills/anchor_theses.py +0 -0
  35. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/skills/edit_perspective.py +0 -0
  36. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/skills/expand_polarities.py +0 -0
  37. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/skills/find_polarities.py +0 -0
  38. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/skills/introduce_polarity.py +0 -0
  39. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/skills/sketch_tetrad.py +0 -0
  40. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/skills/surface_theses.py +0 -0
  41. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/system_prompts.py +0 -0
  42. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/tools/__init__.py +0 -0
  43. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/analyst/tools/place_statement.py +0 -0
  44. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/app_spec.py +0 -0
  45. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/apps.py +0 -0
  46. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/consultant/__init__.py +0 -0
  47. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/execution_report.py +0 -0
  48. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/__init__.py +0 -0
  49. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/explorer.py +0 -0
  50. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/skills/__init__.py +0 -0
  51. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/skills/build_wheels.py +0 -0
  52. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/skills/explore_transformations.py +0 -0
  53. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/skills/generate_synthesis.py +0 -0
  54. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/system_prompts.py +0 -0
  55. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/tools/__init__.py +0 -0
  56. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/tools/create_nexus.py +0 -0
  57. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/tools/expand_nexus.py +0 -0
  58. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/tools/generate_synthesis.py +0 -0
  59. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/explorer/tools/present_exploration.py +0 -0
  60. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/__init__.py +0 -0
  61. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/__init__.py +0 -0
  62. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/add_input.py +0 -0
  63. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/audit_feasibility.py +0 -0
  64. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/create_dx_input.py +0 -0
  65. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/digest_input.py +0 -0
  66. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/discard.py +0 -0
  67. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/get_schema.py +0 -0
  68. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/inspect_node.py +0 -0
  69. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/present_analysis.py +0 -0
  70. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/query_graph.py +0 -0
  71. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/read_digest.py +0 -0
  72. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/orchestrator/tools/read_input.py +0 -0
  73. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/reasonable_concern.py +0 -0
  74. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/stream_events.py +0 -0
  75. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/toolsets.py +0 -0
  76. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/agents/turn_timing.py +0 -0
  77. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/__init__.py +0 -0
  78. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/ac_re_taxonomy.py +0 -0
  79. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/action_extraction.py +0 -0
  80. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/add_input.py +0 -0
  81. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/ai_dto/__init__.py +0 -0
  82. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/ai_dto/statement_dto.py +0 -0
  83. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/ai_dto/statements_deck_dto.py +0 -0
  84. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/antithesis_classification.py +0 -0
  85. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/antithesis_extraction.py +0 -0
  86. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/antithetical_thesis_detection.py +0 -0
  87. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/aspect_classification.py +0 -0
  88. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/aspect_generation.py +0 -0
  89. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/build_status.py +0 -0
  90. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/__init__.py +0 -0
  91. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/causality_estimator.py +0 -0
  92. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/causality_estimator_balanced.py +0 -0
  93. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/causality_estimator_criteria.py +0 -0
  94. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/causality_estimator_desirable.py +0 -0
  95. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/causality_estimator_feasible.py +0 -0
  96. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/causality_estimator_realistic.py +0 -0
  97. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/causality_normalizer.py +0 -0
  98. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality/estimator_resolver.py +0 -0
  99. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/causality_estimation.py +0 -0
  100. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/create_dx_input.py +0 -0
  101. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/create_nexus.py +0 -0
  102. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/decision_coherence_check.py +0 -0
  103. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/decision_confirmation_check.py +0 -0
  104. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/diagonal_oppositions_check.py +0 -0
  105. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/dialectical_context.py +0 -0
  106. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/discard.py +0 -0
  107. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/display_text_edit.py +0 -0
  108. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/expand_nexus.py +0 -0
  109. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/perspective_combination.py +0 -0
  110. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/perspective_validation.py +0 -0
  111. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/positive_ac_re_apex_derivation.py +0 -0
  112. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/record_decision.py +0 -0
  113. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/scoring_scales.py +0 -0
  114. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/source_digest.py +0 -0
  115. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/statement_classification.py +0 -0
  116. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/statement_deduplication.py +0 -0
  117. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/statement_headline.py +0 -0
  118. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/statement_placement.py +0 -0
  119. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/synthesis_generation.py +0 -0
  120. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/tetrad_grounding.py +0 -0
  121. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/tetrad_sketch.py +0 -0
  122. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/thesis_extraction.py +0 -0
  123. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/transformation_audit.py +0 -0
  124. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/concerns/transformation_generation.py +0 -0
  125. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/dialectical_reasoning.py +0 -0
  126. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/enums/__init__.py +0 -0
  127. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/enums/causality_preset.py +0 -0
  128. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/enums/di.py +0 -0
  129. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/events/__init__.py +0 -0
  130. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/events/graph_event.py +0 -0
  131. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/events/graph_event_bus.py +0 -0
  132. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/events/progress_event.py +0 -0
  133. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/exceptions/__init__.py +0 -0
  134. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/exceptions/node_errors.py +0 -0
  135. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/exceptions/resolver_errors.py +0 -0
  136. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/__init__.py +0 -0
  137. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/composite_input_resolver.py +0 -0
  138. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/dialexity_input_resolver.py +0 -0
  139. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/estimation_manager.py +0 -0
  140. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/mixins/__init__.py +0 -0
  141. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/mixins/incremental_build_mixin.py +0 -0
  142. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/mixins/intent_mixin.py +0 -0
  143. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/mixins/persistable_mixin.py +0 -0
  144. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/__init__.py +0 -0
  145. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/assessable_entity.py +0 -0
  146. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/base_node.py +0 -0
  147. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/case.py +0 -0
  148. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/cycle.py +0 -0
  149. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/decision.py +0 -0
  150. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/estimation.py +0 -0
  151. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/ideas.py +0 -0
  152. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/input.py +0 -0
  153. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/nexus.py +0 -0
  154. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/note.py +0 -0
  155. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/perspective.py +0 -0
  156. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/polarity.py +0 -0
  157. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/rationale.py +0 -0
  158. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/statement.py +0 -0
  159. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/synthesis.py +0 -0
  160. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/transformation.py +0 -0
  161. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/transition.py +0 -0
  162. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/nodes/wheel.py +0 -0
  163. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationship_manager.py +0 -0
  164. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/__init__.py +0 -0
  165. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/action_reflection_relationship.py +0 -0
  166. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/belongs_to_cycle_relationship.py +0 -0
  167. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/belongs_to_nexus_relationship.py +0 -0
  168. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/changed_to_relationship.py +0 -0
  169. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/contradiction_of_relationship.py +0 -0
  170. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/critiques_relationship.py +0 -0
  171. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/distilled_to_relationship.py +0 -0
  172. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/estimates_relationship.py +0 -0
  173. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/explains_relationship.py +0 -0
  174. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/grounded_in_relationship.py +0 -0
  175. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/has_input_relationship.py +0 -0
  176. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/has_statement_relationship.py +0 -0
  177. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/has_wheel_relationship.py +0 -0
  178. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/immutable_structure.py +0 -0
  179. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/is_source_of_relationship.py +0 -0
  180. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/is_target_of_relationship.py +0 -0
  181. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/negative_side_of_relationship.py +0 -0
  182. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/opposite_direction_relationship.py +0 -0
  183. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/opposite_of_relationship.py +0 -0
  184. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/polarity_relationship.py +0 -0
  185. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/positive_side_of_relationship.py +0 -0
  186. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/provides_relationship.py +0 -0
  187. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/relationships/synthesis_of_relationship.py +0 -0
  188. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/rendering.py +0 -0
  189. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/__init__.py +0 -0
  190. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/case_repository.py +0 -0
  191. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/cycle_repository.py +0 -0
  192. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/decision_repository.py +0 -0
  193. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/input_repository.py +0 -0
  194. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/nexus_repository.py +0 -0
  195. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/node_repository.py +0 -0
  196. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/note_repository.py +0 -0
  197. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/perspective_repository.py +0 -0
  198. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/polarity_repository.py +0 -0
  199. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/schema_repository.py +0 -0
  200. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/statement_repository.py +0 -0
  201. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/transformation_repository.py +0 -0
  202. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/repositories/wheel_repository.py +0 -0
  203. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/scope_context.py +0 -0
  204. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/verbatim_input_resolver.py +0 -0
  205. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/wheel_segment.py +0 -0
  206. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/graph/wheel_segment_polar_pair.py +0 -0
  207. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/protocols/__init__.py +0 -0
  208. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/protocols/has_config.py +0 -0
  209. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/protocols/input_resolver.py +0 -0
  210. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/py.typed +0 -0
  211. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/settings.py +0 -0
  212. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/__init__.py +0 -0
  213. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/async_drain.py +0 -0
  214. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/call_census.py +0 -0
  215. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/chunking.py +0 -0
  216. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/concurrency.py +0 -0
  217. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/dc_replace.py +0 -0
  218. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/decompose_probability_uniformly.py +0 -0
  219. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/edge_context.py +0 -0
  220. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/effect_logger.py +0 -0
  221. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/input_context.py +0 -0
  222. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/order_transitions.py +0 -0
  223. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/progress.py +0 -0
  224. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/retry_accounting.py +0 -0
  225. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/sequence_generation.py +0 -0
  226. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/thinking_compat.py +0 -0
  227. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/use_brain.py +0 -0
  228. {dialectical_framework-2.0.0 → dialectical_framework-2.0.2}/src/dialectical_framework/utils/utterance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dialectical-framework
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: A dialectical framework for augmented intelligence. AI reasoning powered with dialectics supports humans in: system optimization (psychology, engineering, business, politics, etc.); dispute resolution (mediation, conflicts, negotiations, etc.); decision-making (dilemmas, challenging situations, win-win, etc.).
5
5
  License: MIT
6
6
  Keywords: dialectics,dialectical-reasoning,synthesis,thesis-antithesis,ai,artificial-intelligence,llm,reasoning-framework,philosophy,logic,argumentation,conflict-resolution,decision-making,critical-thinking,semantic-graph,mirascope,pydantic,perspectives,polarity-reasoning
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dialectical-framework"
3
- version = "2.0.0"
3
+ version = "2.0.2"
4
4
  description = "A dialectical framework for augmented intelligence. AI reasoning powered with dialectics supports humans in: system optimization (psychology, engineering, business, politics, etc.); dispute resolution (mediation, conflicts, negotiations, etc.); decision-making (dilemmas, challenging situations, win-win, etc.)."
5
5
  authors = ["Evaldas Taroza <evaldas@dialexity.com>"]
6
6
  readme = "README.md"
@@ -51,7 +51,7 @@ from dialectical_framework.agents.conversation_facilitator import (
51
51
  FROM_SETTINGS, ConversationFacilitator)
52
52
  from dialectical_framework.agents.stream_events import StreamEvent
53
53
  from dialectical_framework.agents.turn_timing import TurnTiming
54
- from dialectical_framework.graph.views import ExplorationView
54
+ from dialectical_framework.graph.views import ExplorationView, PerspectiveView
55
55
 
56
56
  #: Tool verbs → mental acts. These are REWRITES, not drops: dropping every
57
57
  #: paragraph that mentions a tool name would also delete the discrimination test
@@ -324,7 +324,12 @@ class Consultant:
324
324
  if messages:
325
325
  self._conversation.load_messages(messages)
326
326
  parts = [p for p in (preamble, method_prompt(include_decision)) if p]
327
- self._conversation.set_system_prompt("\n\n".join(parts))
327
+ # Kept because the view turn's system prompt is this text plus the
328
+ # static half of its request (`exploration_view`): appending keeps the
329
+ # head a PREFIX of it, so the chat turns' cached prefix is readable by
330
+ # the sketch turns instead of each paying for its own.
331
+ self._system_prompt = "\n\n".join(parts)
332
+ self._conversation.set_system_prompt(self._system_prompt)
328
333
 
329
334
  async def chat(self, user_message: str) -> str:
330
335
  result = await self._conversation.submit(ChatResponse, user_message)
@@ -362,7 +367,8 @@ class Consultant:
362
367
  view shows what has been ESTABLISHED, and where `focus` asks for
363
368
  structure not yet worked out ("a perspective for the thesis you
364
369
  named") the turn builds it by the method first
365
- (`concerns/view_sketch.py::view_sketch_prompt`). What it built is
370
+ (`concerns/view_sketch.py`: the static instructions ride on the system
371
+ prompt, the focus is the turn). What it built is
366
372
  kept in `messages` as the consultant's own words, so the next turn can
367
373
  be asked about a corner it just drew.
368
374
 
@@ -380,16 +386,30 @@ class Consultant:
380
386
  """
381
387
  from dialectical_framework.concerns.view_sketch import (
382
388
  ViewSketchDto, exploration_view_from_sketch, history_ask,
383
- history_text, view_sketch_prompt)
389
+ history_text, perspective_from_sketch, view_sketch_focus,
390
+ view_sketch_instructions)
384
391
 
385
392
  import asyncio
393
+ import dataclasses
386
394
 
387
395
  from dialectical_framework.concerns.tetrad_candidates import (
388
- clamp_attempts, select_sketch)
396
+ clamp_attempts, ranking, select_sketch)
389
397
 
390
398
  level = self._conversation._thinking_kwargs().get("thinking")
391
399
  history = self._conversation._messages
392
- request = view_sketch_prompt(focus, self._conversation.settings.component_length)
400
+ # The request travels in two places, not one. Its static half
401
+ # (definitions, rules, the build procedure) goes on the SYSTEM prompt,
402
+ # after this head's own, because that is where the provider's cache
403
+ # breakpoint sits: as part of the user message it followed the history
404
+ # and was re-prefilled at full rate on every draw (~1.2k tokens x N a
405
+ # card, measured 2026-10-06). Only the focus — the person's own ask —
406
+ # is the turn. Same words, and the history record is untouched
407
+ # (`history_ask`, which is why the long request was never kept anyway).
408
+ system = (
409
+ f"{self._system_prompt}\n\n"
410
+ f"{view_sketch_instructions(self._conversation.settings.component_length)}"
411
+ )
412
+ ask = view_sketch_focus(focus)
393
413
 
394
414
  async def draw() -> ViewSketchDto:
395
415
  # Each draw works on a COPY of the history: the turns run in
@@ -397,9 +417,20 @@ class Consultant:
397
417
  # read. What the view leaves behind is appended once, below.
398
418
  sketch_turn = ConversationFacilitator(format_mode="json", thinking=level)
399
419
  sketch_turn._messages = list(history)
400
- return await sketch_turn.submit(ViewSketchDto, request)
420
+ sketch_turn.set_system_prompt(system)
421
+ return await sketch_turn.submit(ViewSketchDto, ask)
401
422
 
402
423
  n = clamp_attempts(attempts)
424
+ # Fully parallel, deliberately. On a COLD cache all N draws write the
425
+ # ~12k system head instead of one writing and the rest reading (~$0.06
426
+ # a card at list prices, 2026-10-06), and the fix would be to await the
427
+ # first draw before firing the rest — but these turns do not stream, so
428
+ # nothing here can tell a cold cache from a warm one, and the stagger
429
+ # would spend a whole draw's latency (~6 s) on EVERY card to save money
430
+ # on the rare one. The 1h breakpoint TTL
431
+ # (`utils/bedrock_provider.py::_CACHE_TTL`) attacks the same cost
432
+ # without the latency, which is the trade taken. Revisit only with a
433
+ # measured cold-start rate.
403
434
  drawn = await asyncio.gather(*(draw() for _ in range(n)), return_exceptions=True)
404
435
  sketches = [d for d in drawn if not isinstance(d, BaseException)]
405
436
  if not sketches:
@@ -407,19 +438,24 @@ class Consultant:
407
438
  # With several draws the framework's own coherence check picks the one
408
439
  # whose FIRST tension holds best (`concerns/tetrad_candidates.py`); a
409
440
  # draw with no complete first tension sorts last. The card a blindspot
410
- # screen shows is that first tension.
441
+ # screen shows is that first tension. The judged draws that lost come
442
+ # back too, best first, as `runners_up` — a host's "again"/"another"
443
+ # can serve one without a new call (the module's stated intent).
444
+ runners_up: list[PerspectiveView] = []
411
445
  if len(sketches) > 1:
412
446
  firsts = [s.tensions[0] if s.tensions else None for s in sketches]
413
447
  judgeable = [i for i, t in enumerate(firsts) if t is not None and all(
414
448
  (getattr(t, k) or "").strip() for k in ("thesis", "antithesis", "t_plus", "t_minus", "a_plus", "a_minus"))]
415
449
  if len(judgeable) > 1:
416
- best_among, _verdicts = await select_sketch([firsts[i] for i in judgeable])
417
- sketch = sketches[judgeable[best_among]]
450
+ _best, verdicts = await select_sketch([firsts[i] for i in judgeable])
451
+ order = ranking(verdicts)
452
+ sketch = sketches[judgeable[order[0]]]
453
+ runners_up = [perspective_from_sketch(firsts[judgeable[i]]) for i in order[1:]]
418
454
  else:
419
455
  sketch = sketches[judgeable[0]] if judgeable else sketches[0]
420
456
  else:
421
457
  sketch = sketches[0]
422
- view = exploration_view_from_sketch(sketch)
458
+ view = dataclasses.replace(exploration_view_from_sketch(sketch), runners_up=runners_up)
423
459
  # What the turn leaves behind is NOT what it sent: the long request and
424
460
  # the DTO come off, and the person's ask plus the drawing in words go
425
461
  # on. Kept as a request→prose pair, the long request taught the next
@@ -429,6 +429,12 @@ class ConversationFacilitator(SettingsAware):
429
429
  str((p.get("text") if isinstance(p, dict) else getattr(p, "text", "")) or "")
430
430
  for p in parts
431
431
  )
432
+ elif isinstance(content, dict) and content.get("type") == "text":
433
+ # A SYSTEM message's content is ONE text part, not a list, and the
434
+ # documented recipe (`dataclasses.asdict`) renders it as a dict —
435
+ # the shape a host's saved history carries for the prompt at
436
+ # position 0 (found by the first app's backend, 2026-10-05).
437
+ text = str(content.get("text") or "")
432
438
  elif isinstance(content, str) or content is None:
433
439
  text = content or ""
434
440
  else:
@@ -32,7 +32,7 @@ from __future__ import annotations
32
32
 
33
33
  import asyncio
34
34
  from dataclasses import dataclass
35
- from typing import TYPE_CHECKING
35
+ from typing import TYPE_CHECKING, Optional, Sequence
36
36
 
37
37
  from pydantic import BaseModel, Field
38
38
 
@@ -97,6 +97,107 @@ class CoherenceEvaluationDto(BaseModel):
97
97
  )
98
98
 
99
99
 
100
+ class TetradCoherenceDto(BaseModel):
101
+ """One tetrad's two control statements, from the JOINT call.
102
+
103
+ No DV field, deliberately. DV is an annotation that only `resolve`
104
+ persists (`DialecticalValidityEstimation`) and nothing reads it when
105
+ choosing among drafts — `SketchVerdict` has no DV term — so asking for it
106
+ here would buy output tokens and nothing else. A draft that goes on to be
107
+ persisted is scored by `resolve` afterwards, DV and all.
108
+ """
109
+
110
+ tetrad: int = Field(
111
+ description=(
112
+ "The number of the tetrad this verdict scores, exactly as labelled "
113
+ "in the request."
114
+ )
115
+ )
116
+ t_plus_without_a_plus_yields_t_minus: float = Field(
117
+ ge=0.0,
118
+ le=1.0,
119
+ description="Conceptual Coherence (0.0-1.0) of that tetrad's statement (a).",
120
+ )
121
+ a_plus_without_t_plus_yields_a_minus: float = Field(
122
+ ge=0.0,
123
+ le=1.0,
124
+ description="Conceptual Coherence (0.0-1.0) of that tetrad's statement (b).",
125
+ )
126
+ reasoning: str = Field(
127
+ description=(
128
+ "One sentence on the weaker of that tetrad's two statements. A note, "
129
+ "not an essay."
130
+ )
131
+ )
132
+
133
+
134
+ class JointCoherenceEvaluationDto(BaseModel):
135
+ """Every tetrad of one request, scored in one call."""
136
+
137
+ verdicts: list[TetradCoherenceDto] = Field(
138
+ description=(
139
+ "One entry per tetrad in the request, in the request's order. Score "
140
+ "every tetrad; omit none."
141
+ )
142
+ )
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class TetradTexts:
147
+ """The four aspect texts of one tetrad — all the control statements need.
148
+
149
+ What `score_texts_many` takes, so a caller holding several drafts as text
150
+ cannot mix up the order of four positional strings.
151
+ """
152
+
153
+ t_plus: str
154
+ t_minus: str
155
+ a_plus: str
156
+ a_minus: str
157
+
158
+
159
+ def control_statements(
160
+ *, t_plus: str, t_minus: str, a_plus: str, a_minus: str
161
+ ) -> tuple[str, str]:
162
+ """The paper's two control statements, built from four texts.
163
+
164
+ One builder for all three askers (`resolve` off a committed Perspective,
165
+ `score_texts` and `score_texts_many` off bare texts) so the quoting cannot
166
+ drift: a verdict taken before persistence and one taken after must be the
167
+ same question asked of the same words.
168
+ """
169
+ return (
170
+ f'"{t_plus}" without "{a_plus}" yields "{t_minus}"',
171
+ f'"{a_plus}" without "{t_plus}" yields "{a_minus}"',
172
+ )
173
+
174
+
175
+ # --- Shared prompt text ---
176
+ #
177
+ # Written once because two prompts ask for it: one statement at a time
178
+ # (`_evaluate_control_statement`, for `resolve` and `score_texts`) and all of
179
+ # several tetrads at once (`score_texts_many`). Re-typed inline the scales
180
+ # would drift, and a verdict from one prompt is ranked against a verdict from
181
+ # the other (`concerns/tetrad_candidates.py`).
182
+
183
+ _PATTERN = """The pattern "[Positive] without [Balancing factor] yields [Negative]" tests whether
184
+ the absence of a balancing positive aspect naturally leads to the negative/shadow aspect."""
185
+
186
+ _CC_SCALE = """Conceptual Coherence (CC) — is the statement logically meaningful?
187
+ - 0.9-1.0: Highly coherent, clear logical/causal relationship
188
+ - 0.7-0.9: Coherent, reasonable logical connection
189
+ - 0.5-0.7: Somewhat coherent, plausible but weak
190
+ - 0.3-0.5: Weak coherence, tenuous connection
191
+ - 0.0-0.3: Not coherent, no clear logical relationship"""
192
+
193
+ _DV_SCALE = """Dialectical Validity (DV) — is the dialectical relationship natural, judged
194
+ independently of coherence? A statement can be perfectly coherent yet dialectically
195
+ distorted (e.g. one pole arbitrarily privileged, outcomes enforced by coercion or
196
+ ideology rather than natural system dynamics). 1.0 = natural, balanced, generative;
197
+ 0.0 = forced, artificial, distorted. Ignore factual correctness; evaluate only the
198
+ quality of the dialectical relationship as a generative principle."""
199
+
200
+
100
201
  # --- Result ---
101
202
 
102
203
 
@@ -182,8 +283,12 @@ class ControlStatementsCheck(ReasonableConcern[ControlStatementsCheckResult]):
182
283
  a_minus = perspective.get_component(POSITION_A_MINUS)
183
284
 
184
285
  # Build control statements
185
- stmt_1 = f'"{t_plus.prompt_text}" without "{a_plus.prompt_text}" yields "{t_minus.prompt_text}"'
186
- stmt_2 = f'"{a_plus.prompt_text}" without "{t_plus.prompt_text}" yields "{a_minus.prompt_text}"'
286
+ stmt_1, stmt_2 = control_statements(
287
+ t_plus=t_plus.prompt_text,
288
+ t_minus=t_minus.prompt_text,
289
+ a_plus=a_plus.prompt_text,
290
+ a_minus=a_minus.prompt_text,
291
+ )
187
292
 
188
293
  # Evaluate both statements in parallel using isolated conversations
189
294
  result_1, result_2 = await asyncio.gather(
@@ -267,18 +372,90 @@ class ControlStatementsCheck(ReasonableConcern[ControlStatementsCheckResult]):
267
372
  """The two control statements on bare texts — no Perspective, no graph.
268
373
 
269
374
  What `resolve` does after reading the four aspects off a committed
270
- node, offered to callers that hold a tetrad as text: the best-of-N
271
- selector (`concerns/tetrad_candidates.py`) and the harness. Built with
272
- the same quoting as `resolve`'s statements so a verdict here is the
273
- same question as the one asked after persistence.
375
+ node, for a caller holding ONE tetrad as text and wanting DV with it.
376
+ Built by the same statement builder as `resolve`, so a verdict here is
377
+ the same question as the one asked after persistence.
378
+
379
+ Not what best-of-N selection uses: a field of drafts goes through
380
+ `score_texts_many`, which asks once for all of them and leaves DV out
381
+ (nothing in selection reads it). Two statements, two calls, both with
382
+ reasoning, is the shape that made judging a third of a card's cost.
274
383
  """
275
- stmt_1 = f'"{t_plus}" without "{a_plus}" yields "{t_minus}"'
276
- stmt_2 = f'"{a_plus}" without "{t_plus}" yields "{a_minus}"'
384
+ stmt_1, stmt_2 = control_statements(
385
+ t_plus=t_plus, t_minus=t_minus, a_plus=a_plus, a_minus=a_minus
386
+ )
277
387
  return await asyncio.gather(
278
388
  self._evaluate_control_statement(stmt_1, text),
279
389
  self._evaluate_control_statement(stmt_2, text),
280
390
  )
281
391
 
392
+ async def score_texts_many(
393
+ self,
394
+ tetrads: Sequence[TetradTexts],
395
+ *,
396
+ text: str = "",
397
+ ) -> list[Optional[TetradCoherenceDto]]:
398
+ """Several tetrads' control statements, all in ONE call.
399
+
400
+ What `score_texts` asks for one draft at a time, asked for a whole
401
+ best-of-N field at once. Measured reason (2026-10-06, the first app's
402
+ real entry points through `utils/call_census.py`): per-statement calls
403
+ made judging 6 of the 11 provider calls behind one card and ~3.3k of
404
+ its ~5.1k output tokens — about a third of the card's cost — because
405
+ each call spent ~550 output tokens of reasoning on one sentence. One
406
+ call with a short note per tetrad replaces six.
407
+
408
+ The question is unchanged and so is its discipline: the judge sees
409
+ only each tetrad's four aspects (never the thesis or antithesis, see
410
+ `concerns/tetrad_candidates.py`), the statements are built by the same
411
+ builder `resolve` uses, and the prompt forbids comparing the drafts —
412
+ selection stays among independent draws. CC only; `TetradCoherenceDto`
413
+ says why DV is not asked for here.
414
+
415
+ Fail-soft per tetrad: a verdict the model omits, or labels with a
416
+ number that is out of range or already filled, comes back `None` for
417
+ that tetrad rather than shifting every later one onto the wrong draft.
418
+ """
419
+ if not tetrads:
420
+ return []
421
+
422
+ blocks = []
423
+ for n, tetrad in enumerate(tetrads, 1):
424
+ stmt_a, stmt_b = control_statements(
425
+ t_plus=tetrad.t_plus,
426
+ t_minus=tetrad.t_minus,
427
+ a_plus=tetrad.a_plus,
428
+ a_minus=tetrad.a_minus,
429
+ )
430
+ blocks.append(f"Tetrad {n}:\n(a) {stmt_a}\n(b) {stmt_b}")
431
+
432
+ context_section = f"<context>\n{text}\n</context>\n\n" if text else ""
433
+ joined = "\n\n".join(blocks)
434
+ prompt = f"""{context_section}Rate the control statements of {len(tetrads)} tetrads. Each tetrad has two statements, (a) and (b).
435
+
436
+ {joined}
437
+
438
+ {_PATTERN}
439
+
440
+ Rate every statement on one scale:
441
+
442
+ {_CC_SCALE}
443
+
444
+ Score each tetrad on its own, against the pattern above and nothing else. These are independent drafts: do not compare them with each other, do not rank them, do not pick a best, and do not let one tetrad's wording move another's score. The order they appear in carries no information. Return one verdict per tetrad, labelled with that tetrad's number."""
445
+
446
+ conversation = ConversationFacilitator()
447
+ result = await conversation.submit(
448
+ response_model=JointCoherenceEvaluationDto,
449
+ user_content=prompt,
450
+ )
451
+
452
+ placed: list[Optional[TetradCoherenceDto]] = [None] * len(tetrads)
453
+ for verdict in result.verdicts:
454
+ index = verdict.tetrad - 1
455
+ if 0 <= index < len(placed) and placed[index] is None:
456
+ placed[index] = verdict
457
+ return placed
458
+
282
459
  async def _evaluate_control_statement(
283
460
  self,
284
461
  statement: str,
@@ -291,22 +468,11 @@ class ControlStatementsCheck(ReasonableConcern[ControlStatementsCheckResult]):
291
468
 
292
469
  {statement}
293
470
 
294
- The pattern "[Positive] without [Balancing factor] yields [Negative]" tests whether
295
- the absence of a balancing positive aspect naturally leads to the negative/shadow aspect.
471
+ {_PATTERN}
296
472
 
297
- 1. Conceptual Coherence (CC) — is the statement logically meaningful?
298
- - 0.9-1.0: Highly coherent, clear logical/causal relationship
299
- - 0.7-0.9: Coherent, reasonable logical connection
300
- - 0.5-0.7: Somewhat coherent, plausible but weak
301
- - 0.3-0.5: Weak coherence, tenuous connection
302
- - 0.0-0.3: Not coherent, no clear logical relationship
473
+ 1. {_CC_SCALE}
303
474
 
304
- 2. Dialectical Validity (DV) — is the dialectical relationship natural, judged
305
- independently of coherence? A statement can be perfectly coherent yet dialectically
306
- distorted (e.g. one pole arbitrarily privileged, outcomes enforced by coercion or
307
- ideology rather than natural system dynamics). 1.0 = natural, balanced, generative;
308
- 0.0 = forced, artificial, distorted. Ignore factual correctness; evaluate only the
309
- quality of the dialectical relationship as a generative principle."""
475
+ 2. {_DV_SCALE}"""
310
476
 
311
477
  conversation = ConversationFacilitator()
312
478
  return await conversation.submit(
@@ -31,13 +31,12 @@ holding a text or an object with `.statement` (`TetradDto`'s `AspectDto`).
31
31
 
32
32
  from __future__ import annotations
33
33
 
34
- import asyncio
35
34
  import logging
36
35
  from dataclasses import dataclass
37
36
  from typing import Any, Optional, Sequence
38
37
 
39
- from dialectical_framework.concerns.control_statements_check import \
40
- ControlStatementsCheck
38
+ from dialectical_framework.concerns.control_statements_check import (
39
+ ControlStatementsCheck, TetradTexts)
41
40
  from dialectical_framework.graph.nodes.estimation import \
42
41
  CONCEPTUAL_COHERENCE_THRESHOLD
43
42
 
@@ -106,49 +105,96 @@ def is_complete(candidate: Any) -> bool:
106
105
  )
107
106
 
108
107
 
109
- async def judge_sketch(tension: Any, context: str = "") -> SketchVerdict:
110
- """The validator's two control statements, on the candidate's texts, graph-free.
111
-
112
- Statement texts are built exactly as `ControlStatementsCheck.resolve`
113
- builds them from committed Statements, so a verdict here and one taken
114
- after persistence are the same question asked of the same words.
108
+ async def judge_sketches(
109
+ candidates: Sequence[Any], context: str = ""
110
+ ) -> list[Optional[SketchVerdict]]:
111
+ """The validator's two control statements for EVERY candidate, in ONE call.
112
+
113
+ Statement texts are built by the same builder `ControlStatementsCheck.resolve`
114
+ uses, so a verdict here and one taken after persistence are the same
115
+ question asked of the same words. The judge still sees only the four
116
+ aspects of each draft — never the thesis or the antithesis — and the
117
+ prompt forbids comparing the drafts with each other, so selection stays
118
+ "among independent draws" (this module's docstring) and this call cannot
119
+ be turned into regeneration.
120
+
121
+ Why one call: judging used to be a call per control statement per
122
+ candidate — 6 of the 11 provider calls behind one best-of-3 card, and
123
+ ~3.3k of its ~5.1k output tokens, about a third of its cost (measured
124
+ 2026-10-06 on the first app's real entry points with
125
+ `utils/call_census.py`). One call with a short note per tetrad replaces
126
+ six long ones.
127
+
128
+ Fail-soft twice over. A verdict the model omits is `None` for that
129
+ candidate, which sorts last; a failed call leaves EVERY candidate
130
+ unjudged, which keeps the first draw — exactly what `attempts=1` would
131
+ have done. The second shape is new with the joint call and is the cost of
132
+ it: per-statement calls could lose one candidate's verdict and keep the
133
+ rest.
134
+
135
+ OPEN, because this was a cost change and not a quality one. Judge
136
+ stability was 92% on re-judging the same text when each statement had its
137
+ own call (`docs/dev-notes/antithesis-selection.md`, "Best-of-N"); the
138
+ joint call's stability, and its position bias (a candidate winning for
139
+ being listed first), are unmeasured — measure both with the same probe,
140
+ and the card quality with the app's paired eval, before reading any
141
+ quality claim into this. If stability falls below ~85%, the rung down is
142
+ one call per candidate with both statements in one DTO, which is still
143
+ half the output of the per-statement shape.
115
144
  """
116
- first, second = await ControlStatementsCheck().score_texts(
117
- t_plus=aspect_text(tension.t_plus),
118
- t_minus=aspect_text(tension.t_minus),
119
- a_plus=aspect_text(tension.a_plus),
120
- a_minus=aspect_text(tension.a_minus),
121
- text=context,
122
- )
123
- return SketchVerdict(
124
- t_plus_without_a_plus=first.coherence_score,
125
- a_plus_without_t_plus=second.coherence_score,
126
- reasoning=f"{first.reasoning} / {second.reasoning}".strip(" /"),
127
- )
145
+ try:
146
+ verdicts = await ControlStatementsCheck().score_texts_many(
147
+ [
148
+ TetradTexts(
149
+ t_plus=aspect_text(c.t_plus),
150
+ t_minus=aspect_text(c.t_minus),
151
+ a_plus=aspect_text(c.a_plus),
152
+ a_minus=aspect_text(c.a_minus),
153
+ )
154
+ for c in candidates
155
+ ],
156
+ text=context,
157
+ )
158
+ except Exception as exc: # noqa: BLE001 — degrade to "all unjudged"
159
+ logger.warning("Judging the sketches failed; the first draw is kept: %s", exc)
160
+ return [None] * len(candidates)
161
+ return [
162
+ None
163
+ if v is None
164
+ else SketchVerdict(
165
+ t_plus_without_a_plus=v.t_plus_without_a_plus_yields_t_minus,
166
+ a_plus_without_t_plus=v.a_plus_without_t_plus_yields_a_minus,
167
+ reasoning=v.reasoning,
168
+ )
169
+ for v in verdicts
170
+ ]
128
171
 
129
172
 
130
173
  async def select_sketch(
131
174
  candidates: Sequence[Any], context: str = ""
132
175
  ) -> tuple[int, list[Optional[SketchVerdict]]]:
133
- """Judge every candidate (gathered) and return the index of the best plus
134
- all verdicts (None where judging failed).
176
+ """Judge every candidate (one call) and return the index of the best plus
177
+ all verdicts (None where a verdict did not come back).
135
178
 
136
179
  Ranking: the weaker control statement first (the pass rule), then the
137
180
  mean, then the earlier draw — a deterministic tie-break, not a preference.
138
- A candidate whose judging failed sorts last; if every judging failed the
139
- first draw is kept, which is exactly what `attempts=1` would have done.
181
+ An unjudged candidate sorts last; if no verdict came back at all the first
182
+ draw is kept, which is exactly what `attempts=1` would have done.
183
+
184
+ ONE candidate is never judged: there is nothing to select among, so
185
+ `attempts=1` costs no extra call.
140
186
  """
141
187
  if len(candidates) == 1:
142
188
  return 0, [None]
143
189
 
144
- async def one(tension: Any) -> Optional[SketchVerdict]:
145
- try:
146
- return await judge_sketch(tension, context)
147
- except Exception as exc: # noqa: BLE001 — degrade to "unjudged"
148
- logger.warning("Judging a sketch failed; it sorts last: %s", exc)
149
- return None
190
+ verdicts = await judge_sketches(candidates, context)
191
+ return ranking(verdicts)[0], verdicts
192
+
150
193
 
151
- verdicts = list(await asyncio.gather(*(one(c) for c in candidates)))
194
+ def ranking(verdicts: Sequence[Optional[SketchVerdict]]) -> list[int]:
195
+ """Candidate indices best first, by `select_sketch`'s rule: the weaker
196
+ control statement, then the mean, then the earlier draw; unjudged last.
197
+ `[0]` is the winner; the rest are the runners-up in order."""
152
198
 
153
199
  def key(index: int) -> tuple[int, float, float, int]:
154
200
  verdict = verdicts[index]
@@ -157,5 +203,4 @@ async def select_sketch(
157
203
  mean = (verdict.t_plus_without_a_plus + verdict.a_plus_without_t_plus) / 2
158
204
  return (1, verdict.floor, mean, -index)
159
205
 
160
- best = max(range(len(candidates)), key=key)
161
- return best, verdicts
206
+ return sorted(range(len(verdicts)), key=key, reverse=True)
@@ -186,8 +186,22 @@ For each pair:
186
186
  3. Re-read both aspects against step 1 for two distinct failures. (a) Wrong parent: if one is really the OTHER parent developed, rewrite it from its own parent. (b) {PLUS_RESTATEMENT_CHECK}"""
187
187
 
188
188
 
189
- def view_sketch_prompt(focus: Optional[str], max_words: int) -> str:
190
- """The structured turn's request, over the consultant's own conversation.
189
+ def view_sketch_focus(focus: Optional[str]) -> str:
190
+ """The one per-call line of the request: what this draw is to show.
191
+
192
+ Split from the instructions so the Consultant can send this alone as the
193
+ turn's user message and carry the static half on the system prompt, where
194
+ the provider's cache breakpoint already sits (`view_sketch_instructions`).
195
+ """
196
+ return (
197
+ f"What to show: {focus}"
198
+ if focus and focus.strip()
199
+ else "What to show: the tensions this conversation has worked out so far."
200
+ )
201
+
202
+
203
+ def view_sketch_instructions(max_words: int) -> str:
204
+ """The STATIC half of the view turn's request — everything but the focus.
191
205
 
192
206
  Two instructions in one, because the ask is one or the other and the model
193
207
  knows which: render what is ESTABLISHED (a tension already named and taken
@@ -203,15 +217,20 @@ def view_sketch_prompt(focus: Optional[str], max_words: int) -> str:
203
217
  (docs/dev-notes/antithesis-selection.md, 2026-10-02). The thesis is asked
204
218
  for as a STANCE (`THESIS_IS_A_STANCE`): on 20 questions the turn echoed the
205
219
  question as T in 6 before the ask was here (the alsotrue app repo, docs/lab-notes-entry.md).
220
+
221
+ Static because every word of it is fixed per deployment (`max_words` comes
222
+ from settings), which is what lets the Consultant append it to its system
223
+ prompt: as a user message it sat after the history and was re-prefilled at
224
+ full rate on every draw — ~1.2k tokens x N per card, measured 2026-10-06.
225
+ On the system prompt it rides inside the cached prefix. This is a cache
226
+ placement, not a content change; the one thing it does move is WHERE the
227
+ focus line sits, from just under the opening paragraph to the end of the
228
+ turn (`tests/e2e/probe_view_turn_ask.py` is the instrument if that is ever
229
+ suspected of mattering).
206
230
  """
207
- focus_line = (
208
- f"What to show: {focus}\n\n"
209
- if focus and focus.strip()
210
- else "What to show: the tensions this conversation has worked out so far.\n\n"
211
- )
212
231
  return f"""Draw the structure of this conversation as dialectical tetrads. Answer with ONE JSON object in the requested schema and nothing else — no prose. Earlier drawings in this conversation are recorded in prose; that is their record, not the format of this answer.
213
232
 
214
- {focus_line}{ASPECT_DEFINITIONS}
233
+ {ASPECT_DEFINITIONS}
215
234
 
216
235
  Rules for what goes in the view:
217
236
  - Show what this conversation has ESTABLISHED: a tension you named and the person took up is drawn as you both have it, in their terms. Do not re-derive what is already on the table.
@@ -228,6 +247,17 @@ Rules for what goes in the view:
228
247
  Write every position in the person's own terms, {max_words} words or fewer each."""
229
248
 
230
249
 
250
+ def view_sketch_prompt(focus: Optional[str], max_words: int) -> str:
251
+ """The whole request as ONE user message: the instructions and the focus.
252
+
253
+ The shape for a caller with no system prompt of its own to append to — the
254
+ probes and the harness. The Consultant sends the two halves apart
255
+ (`view_sketch_instructions` on the system prompt, `view_sketch_focus` as
256
+ the turn), which is the same words in a different place.
257
+ """
258
+ return f"{view_sketch_instructions(max_words)}\n\n{view_sketch_focus(focus)}"
259
+
260
+
231
261
  # --- The view shaping (pure) ----------------------------------------------
232
262
 
233
263
 
@@ -339,12 +339,18 @@ class ExplorationView:
339
339
  perspectives: list[PerspectiveView] = field(default_factory=list)
340
340
  #: The Nexus this is a view of; None when it is the whole case.
341
341
  nexus_hash: Optional[str] = None
342
+ #: The best-of-N draws that lost, best first — the first tension of each
343
+ #: (`concerns/tetrad_candidates.py`: "returned, not thrown away"). A
344
+ #: host's "again"/"another" can show one without a new reasoning call.
345
+ #: Empty for a view read off the graph, or drawn once.
346
+ runners_up: list[PerspectiveView] = field(default_factory=list)
342
347
 
343
348
  def without_terminology(self) -> ExplorationView:
344
349
  return dataclasses.replace(
345
350
  self,
346
351
  perspectives=[p.without_terminology() for p in self.perspectives],
347
352
  nexus_hash=None,
353
+ runners_up=[p.without_terminology() for p in self.runners_up],
348
354
  )
349
355
 
350
356
  def to_dict(self) -> dict[str, Any]: