fde-framework 0.1.44__tar.gz → 0.1.45__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 (404) hide show
  1. {fde_framework-0.1.44 → fde_framework-0.1.45}/CHANGELOG.md +45 -1
  2. {fde_framework-0.1.44 → fde_framework-0.1.45}/PKG-INFO +17 -8
  3. {fde_framework-0.1.44 → fde_framework-0.1.45}/README.md +16 -7
  4. {fde_framework-0.1.44 → fde_framework-0.1.45}/pyproject.toml +1 -1
  5. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/cli.py +7 -0
  6. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/mcp_server.py +70 -22
  7. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_mcp.py +86 -3
  8. fde_framework-0.1.45/tests/test_mcp_stdio.py +119 -0
  9. fde_framework-0.1.45/tests/test_release_consistency.py +38 -0
  10. fde_framework-0.1.44/tests/test_release_consistency.py +0 -18
  11. {fde_framework-0.1.44 → fde_framework-0.1.45}/.gitignore +0 -0
  12. {fde_framework-0.1.44 → fde_framework-0.1.45}/LICENSE +0 -0
  13. {fde_framework-0.1.44 → fde_framework-0.1.45}/examples/invoice-extraction/README.md +0 -0
  14. {fde_framework-0.1.44 → fde_framework-0.1.45}/examples/policy-qa/README.md +0 -0
  15. {fde_framework-0.1.44 → fde_framework-0.1.45}/examples/support-triage/README.md +0 -0
  16. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/ansible-playbook.md +0 -0
  17. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/assisted-deterministic.md +0 -0
  18. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/audit-only.md +0 -0
  19. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/boundary-and-audit.md +0 -0
  20. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/cascade.md +0 -0
  21. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/classical-ml.md +0 -0
  22. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/compose.md +0 -0
  23. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/decision-log.md +0 -0
  24. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/deterministic-masking.md +0 -0
  25. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/deterministic.md +0 -0
  26. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/direct-call.md +0 -0
  27. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/episodic-store.md +0 -0
  28. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/explainability-record.md +0 -0
  29. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/field-match.md +0 -0
  30. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/finetune.md +0 -0
  31. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/fixed-sequence.md +0 -0
  32. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/gitops.md +0 -0
  33. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/governed-tools.md +0 -0
  34. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/graph-expanded-retrieval.md +0 -0
  35. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/graph-retrieval.md +0 -0
  36. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/hybrid-search.md +0 -0
  37. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/judged.md +0 -0
  38. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/keyword-search.md +0 -0
  39. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/kubernetes-manifests.md +0 -0
  40. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/labelled-decision.md +0 -0
  41. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/labelled-metrics.md +0 -0
  42. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/llm-extraction.md +0 -0
  43. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/llm-scrubbing.md +0 -0
  44. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/llm.md +0 -0
  45. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/local-embedding.md +0 -0
  46. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/managed-api.md +0 -0
  47. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/managed-embedding.md +0 -0
  48. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/manual-runbook.md +0 -0
  49. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/model-planner.md +0 -0
  50. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/ocr-pipeline.md +0 -0
  51. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/optimisation-reasoning.md +0 -0
  52. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/optimisation.md +0 -0
  53. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/passthrough.md +0 -0
  54. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/reranked-retrieval.md +0 -0
  55. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/role-scoped-authority.md +0 -0
  56. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/segmentation.md +0 -0
  57. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/self-hosted.md +0 -0
  58. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/serverless-gpu.md +0 -0
  59. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/speech-transcription.md +0 -0
  60. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/structured-logs.md +0 -0
  61. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/systemd-unit.md +0 -0
  62. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/terraform-module.md +0 -0
  63. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/text-extraction.md +0 -0
  64. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/traced.md +0 -0
  65. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/vector-search.md +0 -0
  66. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/video-ingestion.md +0 -0
  67. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/windowed-ingestion.md +0 -0
  68. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/working-state.md +0 -0
  69. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/cases/churn-scoring.md +0 -0
  70. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/cases/route-planning.md +0 -0
  71. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/cases/structured-extraction.md +0 -0
  72. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/cases/studio-style.md +0 -0
  73. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/accountability.md +0 -0
  74. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/deployment.md +0 -0
  75. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/embedding.md +0 -0
  76. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/evaluation.md +0 -0
  77. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/governance.md +0 -0
  78. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/integration.md +0 -0
  79. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/memory.md +0 -0
  80. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/observability.md +0 -0
  81. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/perception.md +0 -0
  82. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/planning.md +0 -0
  83. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/provisioning.md +0 -0
  84. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/reasoning.md +0 -0
  85. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/redaction.md +0 -0
  86. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/representation.md +0 -0
  87. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/retrieval.md +0 -0
  88. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/serving.md +0 -0
  89. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/accelerator.md +0 -0
  90. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/access_model.md +0 -0
  91. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/arrival_rate.md +0 -0
  92. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/availability_target.md +0 -0
  93. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/cheap_path_coverage.md +0 -0
  94. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/confidence_calibrated.md +0 -0
  95. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/container_competence.md +0 -0
  96. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/corpus_churn.md +0 -0
  97. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/corpus_size.md +0 -0
  98. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/data_residency.md +0 -0
  99. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/environment_lifetime.md +0 -0
  100. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/existing_cluster.md +0 -0
  101. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/existing_iac_tool.md +0 -0
  102. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/external_systems.md +0 -0
  103. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/hosting.md +0 -0
  104. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/human_waiting.md +0 -0
  105. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/input_format.md +0 -0
  106. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/interpretability_required.md +0 -0
  107. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/labelled_count.md +0 -0
  108. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/latency_budget_ms.md +0 -0
  109. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/licence_posture.md +0 -0
  110. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/operates_after_handover.md +0 -0
  111. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/output_shape.md +0 -0
  112. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/provisioning_api.md +0 -0
  113. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/query_pattern.md +0 -0
  114. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/recall_span.md +0 -0
  115. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/sensitivity_present.md +0 -0
  116. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Generator.md +0 -0
  117. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Guard.md +0 -0
  118. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Mapper.md +0 -0
  119. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/ModelServer.md +0 -0
  120. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Parser.md +0 -0
  121. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Planner.md +0 -0
  122. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Retriever.md +0 -0
  123. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Scorer.md +0 -0
  124. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Store.md +0 -0
  125. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/ToolBoundary.md +0 -0
  126. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Tracer.md +0 -0
  127. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/locales/eu-gdpr.md +0 -0
  128. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/locales/in-dpdp.md +0 -0
  129. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/ansible-playbook.md +0 -0
  130. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/assisted-deterministic.md +0 -0
  131. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/audit-only.md +0 -0
  132. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/boundary-and-audit.md +0 -0
  133. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/cascade-reasoning.md +0 -0
  134. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/cascade-representation.md +0 -0
  135. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/cascade-retrieval.md +0 -0
  136. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/classical-ml-reasoning.md +0 -0
  137. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/classical-ml.md +0 -0
  138. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/compose.md +0 -0
  139. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/decision-log.md +0 -0
  140. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/deterministic-masking.md +0 -0
  141. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/deterministic.md +0 -0
  142. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/direct-call.md +0 -0
  143. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/episodic-store.md +0 -0
  144. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/explainability-record.md +0 -0
  145. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/field-match.md +0 -0
  146. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/finetune-representation.md +0 -0
  147. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/finetune.md +0 -0
  148. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/fixed-sequence.md +0 -0
  149. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/gitops.md +0 -0
  150. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/governed-tools.md +0 -0
  151. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/graph-expanded-retrieval.md +0 -0
  152. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/graph-retrieval.md +0 -0
  153. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/hybrid-search.md +0 -0
  154. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/judged.md +0 -0
  155. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/keyword-search.md +0 -0
  156. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/kubernetes-manifests.md +0 -0
  157. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/labelled-decision.md +0 -0
  158. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/labelled-metrics.md +0 -0
  159. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/llm-representation.md +0 -0
  160. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/llm-scrubbing.md +0 -0
  161. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/llm.md +0 -0
  162. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/local-embedding.md +0 -0
  163. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/managed-api.md +0 -0
  164. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/managed-embedding.md +0 -0
  165. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/manual-runbook.md +0 -0
  166. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/model-planner.md +0 -0
  167. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/ocr-pipeline.md +0 -0
  168. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/optimisation-reasoning.md +0 -0
  169. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/optimisation.md +0 -0
  170. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/passthrough.md +0 -0
  171. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/reranked-retrieval.md +0 -0
  172. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/role-scoped-authority.md +0 -0
  173. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/segmentation.md +0 -0
  174. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/self-hosted.md +0 -0
  175. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/serverless-gpu.md +0 -0
  176. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/speech-transcription.md +0 -0
  177. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/structured-logs.md +0 -0
  178. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/systemd-unit.md +0 -0
  179. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/terraform-module.md +0 -0
  180. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/text-extraction.md +0 -0
  181. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/traced.md +0 -0
  182. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/vector-search.md +0 -0
  183. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/video-ingestion.md +0 -0
  184. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/windowed-ingestion.md +0 -0
  185. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/working-state.md +0 -0
  186. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/langgraph.md +0 -0
  187. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/llamaindex.md +0 -0
  188. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/local-judge.md +0 -0
  189. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/mcp.md +0 -0
  190. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/ollama.md +0 -0
  191. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/openai-judge.md +0 -0
  192. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/opentelemetry.md +0 -0
  193. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/ortools.md +0 -0
  194. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/peft.md +0 -0
  195. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/pgvector.md +0 -0
  196. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/plain-python.md +0 -0
  197. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/qdrant.md +0 -0
  198. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/supermemory.md +0 -0
  199. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/tesseract.md +0 -0
  200. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/vllm.md +0 -0
  201. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/whisper.md +0 -0
  202. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/xgboost.md +0 -0
  203. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/accountability/decision-log.plain.py.j2 +0 -0
  204. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/accountability/explainability-record.plain.py.j2 +0 -0
  205. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/deployment/compose.plain.py.j2 +0 -0
  206. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/deployment/kubernetes-manifests.plain.py.j2 +0 -0
  207. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/deployment/systemd-unit.plain.py.j2 +0 -0
  208. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/embedding/local-embedding.plain.py.j2 +0 -0
  209. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/embedding/managed-embedding.plain.py.j2 +0 -0
  210. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/field-match.plain.py.j2 +0 -0
  211. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/judged.local-judge.py.j2 +0 -0
  212. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/judged.openai-judge.py.j2 +0 -0
  213. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/judged.plain.py.j2 +0 -0
  214. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/labelled-metrics.plain.py.j2 +0 -0
  215. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/labelled-metrics.xgboost.py.j2 +0 -0
  216. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/governance/audit-only.plain.py.j2 +0 -0
  217. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/governance/boundary-and-audit.plain.py.j2 +0 -0
  218. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/governance/role-scoped-authority.plain.py.j2 +0 -0
  219. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/integration/direct-call.plain.py.j2 +0 -0
  220. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/integration/governed-tools.mcp.py.j2 +0 -0
  221. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/integration/governed-tools.plain.py.j2 +0 -0
  222. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/memory/episodic-store.pgvector.py.j2 +0 -0
  223. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/memory/episodic-store.plain.py.j2 +0 -0
  224. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/memory/episodic-store.supermemory.py.j2 +0 -0
  225. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/memory/working-state.plain.py.j2 +0 -0
  226. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/observability/structured-logs.plain.py.j2 +0 -0
  227. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/observability/traced.opentelemetry.py.j2 +0 -0
  228. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/observability/traced.plain.py.j2 +0 -0
  229. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/ocr-pipeline.plain.py.j2 +0 -0
  230. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/ocr-pipeline.tesseract.py.j2 +0 -0
  231. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/passthrough.plain.py.j2 +0 -0
  232. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/speech-transcription.plain.py.j2 +0 -0
  233. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/speech-transcription.whisper.py.j2 +0 -0
  234. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/text-extraction.plain.py.j2 +0 -0
  235. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/video-ingestion.plain.py.j2 +0 -0
  236. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/windowed-ingestion.plain.py.j2 +0 -0
  237. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/fixed-sequence.plain.py.j2 +0 -0
  238. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/model-planner.langgraph.py.j2 +0 -0
  239. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/model-planner.plain.py.j2 +0 -0
  240. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/optimisation.ortools.py.j2 +0 -0
  241. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/optimisation.plain.py.j2 +0 -0
  242. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/provisioning/ansible-playbook.plain.py.j2 +0 -0
  243. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/provisioning/gitops.plain.py.j2 +0 -0
  244. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/provisioning/manual-runbook.plain.py.j2 +0 -0
  245. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/provisioning/terraform-module.plain.py.j2 +0 -0
  246. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/cascade.plain.py.j2 +0 -0
  247. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/classical-ml.plain.py.j2 +0 -0
  248. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/classical-ml.xgboost.py.j2 +0 -0
  249. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/finetune.plain.py.j2 +0 -0
  250. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/labelled-decision.plain.py.j2 +0 -0
  251. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/llm.plain.py.j2 +0 -0
  252. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/optimisation.ortools.py.j2 +0 -0
  253. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/optimisation.plain.py.j2 +0 -0
  254. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/redaction/deterministic-masking.plain.py.j2 +0 -0
  255. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/redaction/llm-scrubbing.plain.py.j2 +0 -0
  256. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/assisted.plain.py.j2 +0 -0
  257. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/cascade.plain.py.j2 +0 -0
  258. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/classical-ml.plain.py.j2 +0 -0
  259. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/classical-ml.xgboost.py.j2 +0 -0
  260. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/deterministic.plain.py.j2 +0 -0
  261. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/finetune.plain.py.j2 +0 -0
  262. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/llm.plain.py.j2 +0 -0
  263. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/segmentation.plain.py.j2 +0 -0
  264. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/cascade.plain.py.j2 +0 -0
  265. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-expanded-retrieval.pgvector.py.j2 +0 -0
  266. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-expanded-retrieval.plain.py.j2 +0 -0
  267. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-expanded-retrieval.qdrant.py.j2 +0 -0
  268. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-retrieval.pgvector.py.j2 +0 -0
  269. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-retrieval.plain.py.j2 +0 -0
  270. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-retrieval.qdrant.py.j2 +0 -0
  271. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/hybrid-search.plain.py.j2 +0 -0
  272. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/keyword-search.plain.py.j2 +0 -0
  273. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/reranked-retrieval.plain.py.j2 +0 -0
  274. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/vector-search.llamaindex.py.j2 +0 -0
  275. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/vector-search.pgvector.py.j2 +0 -0
  276. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/vector-search.plain.py.j2 +0 -0
  277. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/vector-search.qdrant.py.j2 +0 -0
  278. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/managed-api.plain.py.j2 +0 -0
  279. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/self-hosted.ollama.py.j2 +0 -0
  280. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/self-hosted.plain.py.j2 +0 -0
  281. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/self-hosted.vllm.py.j2 +0 -0
  282. {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/serverless-gpu.plain.py.j2 +0 -0
  283. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/__init__.py +0 -0
  284. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/architect.py +0 -0
  285. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/bench.py +0 -0
  286. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/costing.py +0 -0
  287. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/debt.py +0 -0
  288. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/decide.py +0 -0
  289. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/decompose.py +0 -0
  290. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/deploy.py +0 -0
  291. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/drift.py +0 -0
  292. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/emit.py +0 -0
  293. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/evolution.py +0 -0
  294. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/experiment.py +0 -0
  295. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/factlog.py +0 -0
  296. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/forecast.py +0 -0
  297. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/gates.py +0 -0
  298. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/graph.py +0 -0
  299. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/history.py +0 -0
  300. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/impact.py +0 -0
  301. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/implement.py +0 -0
  302. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/importer.py +0 -0
  303. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/__init__.py +0 -0
  304. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/answers.py +0 -0
  305. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/documents.py +0 -0
  306. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/interview.py +0 -0
  307. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/llm_reader.py +0 -0
  308. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/prose.py +0 -0
  309. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/samples.py +0 -0
  310. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/lifecycle.py +0 -0
  311. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/__init__.py +0 -0
  312. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/base.py +0 -0
  313. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/fact.py +0 -0
  314. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/profile.py +0 -0
  315. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/respondent.py +0 -0
  316. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/schema.py +0 -0
  317. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/moves.py +0 -0
  318. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/ops.py +0 -0
  319. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/predicate.py +0 -0
  320. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/realization.py +0 -0
  321. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/registry.py +0 -0
  322. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/sandbox.py +0 -0
  323. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/scan.py +0 -0
  324. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/scorecard.py +0 -0
  325. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/space.py +0 -0
  326. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/stakeholders.py +0 -0
  327. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/stop.py +0 -0
  328. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/training.py +0 -0
  329. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/value.py +0 -0
  330. {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/workflow.py +0 -0
  331. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/__init__.py +0 -0
  332. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/conftest.py +0 -0
  333. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/models/__init__.py +0 -0
  334. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/models/test_profile.py +0 -0
  335. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_acceptance.py +0 -0
  336. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_access_and_sensitivity.py +0 -0
  337. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_answers.py +0 -0
  338. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_audit_fixes.py +0 -0
  339. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_bench.py +0 -0
  340. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_build_receipt.py +0 -0
  341. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cascade.py +0 -0
  342. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_engagement.py +0 -0
  343. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_evolution.py +0 -0
  344. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_gates.py +0 -0
  345. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_intake.py +0 -0
  346. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_kb.py +0 -0
  347. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_robustness.py +0 -0
  348. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_samples.py +0 -0
  349. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_costing.py +0 -0
  350. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_coverage.py +0 -0
  351. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_dead_zones.py +0 -0
  352. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_debt.py +0 -0
  353. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_decide.py +0 -0
  354. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_decompose.py +0 -0
  355. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_deploy.py +0 -0
  356. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_documents.py +0 -0
  357. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_drift.py +0 -0
  358. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_embedding.py +0 -0
  359. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_emit.py +0 -0
  360. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_evolution.py +0 -0
  361. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_example_transcript.py +0 -0
  362. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_experiment.py +0 -0
  363. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_factlog.py +0 -0
  364. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_finetune.py +0 -0
  365. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_finetune_real.py +0 -0
  366. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_forecast.py +0 -0
  367. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_gates.py +0 -0
  368. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_history.py +0 -0
  369. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_impact.py +0 -0
  370. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_implement.py +0 -0
  371. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_importer.py +0 -0
  372. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_interview.py +0 -0
  373. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_lifecycle.py +0 -0
  374. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_llm_reader.py +0 -0
  375. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_locales.py +0 -0
  376. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_memory_retrieval.py +0 -0
  377. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_more_templates.py +0 -0
  378. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_moves.py +0 -0
  379. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_multimodal.py +0 -0
  380. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_next.py +0 -0
  381. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_ops.py +0 -0
  382. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_outcome_contract.py +0 -0
  383. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_package.py +0 -0
  384. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_prose.py +0 -0
  385. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_realization.py +0 -0
  386. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_registry.py +0 -0
  387. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_remaining.py +0 -0
  388. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_retrieval_eval.py +0 -0
  389. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_reversibility.py +0 -0
  390. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_samples.py +0 -0
  391. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_sandbox.py +0 -0
  392. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_sanitisation.py +0 -0
  393. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_scan.py +0 -0
  394. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_schema.py +0 -0
  395. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_scorecard.py +0 -0
  396. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_serving.py +0 -0
  397. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_shipped_registry.py +0 -0
  398. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_space.py +0 -0
  399. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_stack_swap.py +0 -0
  400. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_stakeholders.py +0 -0
  401. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_stop.py +0 -0
  402. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_supermemory.py +0 -0
  403. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_templates.py +0 -0
  404. {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_value.py +0 -0
@@ -5,7 +5,51 @@ the project is pre-release, so everything sits under 0.1.0 until the first tag.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
- ## [0.1.44] — 2026-10-12
8
+ ## [0.1.45] — 2026-10-11
9
+
10
+ A twelfth outside reading, and a coding agent driving the server for the
11
+ first time, found three things between them: a release page that said
12
+ nothing about what it was, a date a day ahead, and an MCP server that lost
13
+ the agent's second call two different ways.
14
+
15
+ - **The agent's second call finds the first call's work.**
16
+ `start(name="acme")` puts the record at engagements/acme;
17
+ `status(engagement="acme")` a moment later looked at `<workspace>/acme`
18
+ and said there was no engagement. The in-process test had passed the
19
+ path `start` returned, so it never asked by name. A bare name is now
20
+ looked for where `start` puts it, as `fde status acme` does at the
21
+ terminal; a path is taken as given; a name that climbs out is refused
22
+ before anything is looked for.
23
+ - **Calls issued at once all come back.** Claude Code asked for `status`
24
+ and `next` in the same breath and the second came back "Connection
25
+ closed". The server runs each tool on its own thread, and the capture
26
+ that collects a command's output does so by swapping the process's
27
+ stdout; two swaps interleaved left one command printing straight onto
28
+ the JSON-RPC channel. Commands now run one at a time inside the server.
29
+ A new suite drives the server as a separate process over stdio with the
30
+ official client: six calls at once for three rounds, a call abandoned
31
+ midway, a record changed on disk between calls, and every tool's input
32
+ schema published. The hang reproduced before the fix and fails within
33
+ ninety seconds instead of hanging CI.
34
+ - **The envelope's contract is a JSON Schema.** `fde mcp --schema` prints
35
+ it; every reply is validated against it; the schema's list of tools is
36
+ the server's; a field it does not name or a status it does not name
37
+ fails. The README says when the version moves: a field changing meaning
38
+ or going away, never one being added.
39
+ - **A release page means verified.** It is written only after the wheel
40
+ PyPI serves has been installed on Python 3.11, 3.12 and 3.13, hashed
41
+ against the wheel the tests ran on, and walked through the first five
42
+ minutes. It carries the wheel and the sdist, their SHA-256, the commit,
43
+ the run and the install line.
44
+ - **One date.** 0.1.44 shipped on October 11 dated October 12 in both
45
+ CHANGELOG and CITATION. Corrected; the release workflow now holds the
46
+ CHANGELOG date, CITATION's date-released and the tagged commit's own day
47
+ in the author's time zone to one value, and a test holds the two files
48
+ to each other.
49
+ - A corrupt record is a sentence naming the file, not a traceback, through
50
+ the server as at the terminal; now pinned by a test.
51
+
52
+ ## [0.1.44] — 2026-10-11
9
53
 
10
54
  An eleventh outside reading found the README contradicting itself on its
11
55
  own test count, and asked for the published wheel to be the thing that
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fde-framework
3
- Version: 0.1.44
3
+ Version: 0.1.45
4
4
  Summary: The decision before the AI build: evidence in, an architecture with receipts out, a scored build or a stop. For Forward Deployed Engineers and anyone deciding whether an AI system should exist.
5
5
  Project-URL: Homepage, https://github.com/atulkapoor/fde-framework
6
6
  Project-URL: Documentation, https://atulkapoor.github.io/fde-framework/
@@ -159,15 +159,24 @@ It serves `triage`, `start`, `frame`, `status`, `next`, `stage`, `debt`,
159
159
  refusals included, beside a small envelope an agent can branch on without
160
160
  parsing prose: `schema_version`, `status` (ok, blocked, refused, failed),
161
161
  `mutated`, and the workspace. The envelope follows the command's exit,
162
- never a reading of its text; its contract is written in the server module
163
- and the version moves only when a field changes meaning. An agent is not a person, so every path it supplies must
164
- stay inside one workspace, the directory the server started in or
162
+ never a reading of its text. Its contract is a JSON Schema, `fde mcp
163
+ --schema`, that every reply is tested against; a client pins
164
+ `schema_version`, which moves only when a field changes meaning or goes
165
+ away, never when one is added. An engagement is named as `start` named
166
+ it (`start(name="acme")`, then `status(engagement="acme")`), or by its
167
+ path. An agent is not a person, so every path it supplies must stay
168
+ inside one workspace, the directory the server started in or
165
169
  `FDE_WORKSPACE`; a path outside it, a traversal, or a symlink that points
166
170
  out is refused in words, and engagement names are plain names. The two
167
171
  tools that write to the record, `start` and `frame`, say so and carry the
168
- protocol's hints so a client can ask first. Building, implementing and
169
- deploying are not served; they stay on the command line, where the person
170
- who runs them is accountable for them.
172
+ protocol's hints so a client can ask first. Calls run one at a time
173
+ inside the server, so an agent that asks for `status` and `next` in the
174
+ same breath gets both. Exercised against two real clients: the official
175
+ MCP Python client over stdio, in the test suite, with concurrent calls,
176
+ an abandoned call and a record changed on disk between calls; and Claude
177
+ Code 2.1, by hand, from the configuration above. Building, implementing
178
+ and deploying are not served; they stay on the command line, where the
179
+ person who runs them is accountable for them.
171
180
 
172
181
  > ### Five founding engagements
173
182
  > One real workflow, your real examples, a measured baseline, and someone
@@ -232,7 +241,7 @@ ladder, so nothing here can be over-read:
232
241
 
233
242
  | Claim | Status | The evidence, and where it stops |
234
243
  |---|---|---|
235
- | The framework is implemented | verified | 1,308 tests on three Pythons in CI; lint clean; every audit finding pinned as a check before it was fixed; every release installed from the published wheel into a fresh interpreter and walked through the first five minutes |
244
+ | The framework is implemented | verified | 1,316 tests on three Pythons in CI; lint clean; every audit finding pinned as a check before it was fixed; every release installed from the published wheel into a fresh interpreter and walked through the first five minutes |
236
245
  | A build is reproducible | verified | byte-identical rebuild pinned by test; each public demo carries a Reproduce section and its digests |
237
246
  | The decisions generalise | partial | four shapes (extraction, freeform QA, a labelled decision, seventy-seven-way routing) on real data; one industry set; `fde kb sweep` names the shapes nothing serves |
238
247
  | The deliverable is production-reliable | unverified | no production engagement; the banking operating loop ran on a laptop and its record says so |
@@ -115,15 +115,24 @@ It serves `triage`, `start`, `frame`, `status`, `next`, `stage`, `debt`,
115
115
  refusals included, beside a small envelope an agent can branch on without
116
116
  parsing prose: `schema_version`, `status` (ok, blocked, refused, failed),
117
117
  `mutated`, and the workspace. The envelope follows the command's exit,
118
- never a reading of its text; its contract is written in the server module
119
- and the version moves only when a field changes meaning. An agent is not a person, so every path it supplies must
120
- stay inside one workspace, the directory the server started in or
118
+ never a reading of its text. Its contract is a JSON Schema, `fde mcp
119
+ --schema`, that every reply is tested against; a client pins
120
+ `schema_version`, which moves only when a field changes meaning or goes
121
+ away, never when one is added. An engagement is named as `start` named
122
+ it (`start(name="acme")`, then `status(engagement="acme")`), or by its
123
+ path. An agent is not a person, so every path it supplies must stay
124
+ inside one workspace, the directory the server started in or
121
125
  `FDE_WORKSPACE`; a path outside it, a traversal, or a symlink that points
122
126
  out is refused in words, and engagement names are plain names. The two
123
127
  tools that write to the record, `start` and `frame`, say so and carry the
124
- protocol's hints so a client can ask first. Building, implementing and
125
- deploying are not served; they stay on the command line, where the person
126
- who runs them is accountable for them.
128
+ protocol's hints so a client can ask first. Calls run one at a time
129
+ inside the server, so an agent that asks for `status` and `next` in the
130
+ same breath gets both. Exercised against two real clients: the official
131
+ MCP Python client over stdio, in the test suite, with concurrent calls,
132
+ an abandoned call and a record changed on disk between calls; and Claude
133
+ Code 2.1, by hand, from the configuration above. Building, implementing
134
+ and deploying are not served; they stay on the command line, where the
135
+ person who runs them is accountable for them.
127
136
 
128
137
  > ### Five founding engagements
129
138
  > One real workflow, your real examples, a measured baseline, and someone
@@ -188,7 +197,7 @@ ladder, so nothing here can be over-read:
188
197
 
189
198
  | Claim | Status | The evidence, and where it stops |
190
199
  |---|---|---|
191
- | The framework is implemented | verified | 1,308 tests on three Pythons in CI; lint clean; every audit finding pinned as a check before it was fixed; every release installed from the published wheel into a fresh interpreter and walked through the first five minutes |
200
+ | The framework is implemented | verified | 1,316 tests on three Pythons in CI; lint clean; every audit finding pinned as a check before it was fixed; every release installed from the published wheel into a fresh interpreter and walked through the first five minutes |
192
201
  | A build is reproducible | verified | byte-identical rebuild pinned by test; each public demo carries a Reproduce section and its digests |
193
202
  | The decisions generalise | partial | four shapes (extraction, freeform QA, a labelled decision, seventy-seven-way routing) on real data; one industry set; `fde kb sweep` names the shapes nothing serves |
194
203
  | The deliverable is production-reliable | unverified | no production engagement; the banking operating loop ran on a laptop and its record says so |
@@ -2,7 +2,7 @@
2
2
  # Distribution name matches the repository and stays unique on any index;
3
3
  # the import package and the CLI command remain the short `fde`.
4
4
  name = "fde-framework"
5
- version = "0.1.44"
5
+ version = "0.1.45"
6
6
  description = "The decision before the AI build: evidence in, an architecture with receipts out, a scored build or a stop. For Forward Deployed Engineers and anyone deciding whether an AI system should exist."
7
7
  readme = "README.md"
8
8
  license = {text = "Apache-2.0"}
@@ -2407,12 +2407,19 @@ def mcp_cmd(
2407
2407
  check: Annotated[bool, typer.Option(
2408
2408
  "--check", help="Build the server and list what it serves, without serving it."
2409
2409
  )] = False,
2410
+ schema: Annotated[bool, typer.Option(
2411
+ "--schema", help="Print the JSON Schema of the envelope beside every reply."
2412
+ )] = False,
2410
2413
  ) -> None:
2411
2414
  """Serve the framework's conversational commands to a coding agent over
2412
2415
  stdio (Model Context Protocol): triage, start, frame, status, next,
2413
2416
  stage, debt, stop_when, history. Every path the agent supplies must be
2414
2417
  inside the workspace (FDE_WORKSPACE, else the current directory). Add
2415
2418
  it with, for example, `claude mcp add fde -- fde mcp`."""
2419
+ if schema:
2420
+ from fde.mcp_server import ENVELOPE_SCHEMA
2421
+ typer.echo(json.dumps(ENVELOPE_SCHEMA, indent=2))
2422
+ return
2416
2423
  try:
2417
2424
  from fde.mcp_server import describe, serve
2418
2425
  if check:
@@ -30,22 +30,41 @@ from __future__ import annotations
30
30
 
31
31
  import os
32
32
  import re
33
+ import threading
33
34
  from pathlib import Path
34
35
 
35
36
  NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
36
37
 
37
- # The envelope beside every reply. Bump when a field changes meaning or
38
- # goes away; adding a field keeps the version.
38
+ # The envelope beside every reply, as the JSON Schema a client can validate
39
+ # against (`fde mcp --schema` prints it). Bump the version when a field
40
+ # changes meaning or goes away; adding a field keeps the version.
39
41
  SCHEMA_VERSION = 1
40
42
  STATUSES = ("ok", "blocked", "refused", "failed")
41
- ENVELOPE = {
42
- "schema_version": "the integer above",
43
- "tool": "the tool that answered",
44
- "status": "ok: the command exited 0 | blocked: it refused or a condition triggered "
45
- "(exit 1) | refused: the path or name was outside what the agent may touch; "
46
- "nothing ran | failed: an exception, a bug to report",
47
- "mutated": "true only when a writing tool ran and exited 0",
48
- "workspace": "the directory every path was confined to",
43
+ TOOLS = ("triage", "start", "frame", "status", "next", "stage", "debt", "stop_when", "history")
44
+ ENVELOPE_SCHEMA = {
45
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
46
+ "$id": f"https://github.com/atulkapoor/fde-framework/schemas/mcp-envelope/v{SCHEMA_VERSION}",
47
+ "title": f"fde MCP reply envelope, version {SCHEMA_VERSION}",
48
+ "description": "The structured content beside the text of every tool reply. The text "
49
+ "is what the command printed; the envelope is what an agent branches on "
50
+ "without parsing prose. It follows the command's exit, not its wording.",
51
+ "type": "object",
52
+ "properties": {
53
+ "schema_version": {"const": SCHEMA_VERSION,
54
+ "description": "Bumped when a field changes meaning or goes away."},
55
+ "tool": {"enum": list(TOOLS), "description": "The tool that answered."},
56
+ "status": {"enum": list(STATUSES),
57
+ "description": "ok: the command exited 0. blocked: it refused or a "
58
+ "condition triggered (exit 1); the text says which. "
59
+ "refused: a path or name was outside what the agent may "
60
+ "touch; nothing ran. failed: an exception; a bug to report."},
61
+ "mutated": {"type": "boolean",
62
+ "description": "True only when a writing tool ran and exited 0."},
63
+ "workspace": {"type": "string",
64
+ "description": "The directory every path was confined to."},
65
+ },
66
+ "required": ["schema_version", "tool", "status", "mutated", "workspace"],
67
+ "additionalProperties": False,
49
68
  }
50
69
 
51
70
  THIRTY_SECONDS = """\
@@ -68,10 +87,12 @@ brief into typed facts; `status` for the gates and what is still unknown;
68
87
  for where the engagement stands on its record; `debt` for what nobody has
69
88
  settled; `stop_when` for whether the evidence says stop; `history` for the
70
89
  whole record in order. Every tool returns the command's own output,
71
- refusals included. `start` and `frame` write to the record; the rest only
72
- read it. Every path must be inside the server's workspace. Nothing here
73
- builds, implements or deploys; those are run by a person on the command
74
- line.
90
+ refusals included, with a status beside it (ok, blocked, refused, failed)
91
+ to branch on instead of the prose. `start` and `frame` write to the
92
+ record; the rest only read it. An engagement is named as `start` named
93
+ it, or by its path; every path must be inside the server's workspace.
94
+ Nothing here builds, implements or deploys; those are run by a person on
95
+ the command line.
75
96
  """
76
97
 
77
98
 
@@ -100,6 +121,21 @@ def confine(path: str | None, what: str, root: Path | None = None) -> Path | Non
100
121
  return resolved
101
122
 
102
123
 
124
+ def locate(engagement: str) -> Path:
125
+ """The engagement as the command will see it. `start(name="acme")` puts
126
+ the record at engagements/acme; `status(engagement="acme")` a moment
127
+ later must find it, as `fde status acme` does on the command line. So a
128
+ bare name is looked for at <workspace>/<name> and then where `start`
129
+ puts it; both candidates are confined, so a name that climbs out is
130
+ refused before anything is looked for. A path is taken as given."""
131
+ path = confine(engagement, "engagement")
132
+ if path is not None and not (path / "facts").is_dir() and not Path(engagement).is_absolute():
133
+ under = confine(str(Path("engagements") / engagement), "engagement")
134
+ if under is not None and (under / "facts").is_dir():
135
+ return under
136
+ return path
137
+
138
+
103
139
  def plain_name(name: str) -> str:
104
140
  if not NAME.match(name or ""):
105
141
  raise Outside(f"an engagement name is a plain name (letters, digits, '.', '_', '-'), "
@@ -113,6 +149,16 @@ def _registry() -> str:
113
149
  return os.environ.get("FDE_REGISTRY") or str(default_root())
114
150
 
115
151
 
152
+ # One command at a time. The server runs each tool on a worker thread, and
153
+ # a coding agent will issue `status` and `next` in the same breath; the
154
+ # runner that captures a command's output does so by swapping the
155
+ # process's stdout, and two swaps interleaved left one command printing
156
+ # straight onto the JSON-RPC channel. The protocol broke, the client saw
157
+ # "Connection closed", and the record was not at fault. Commands take
158
+ # milliseconds; the record has one writer at a time, as it should.
159
+ _ONE_AT_A_TIME = threading.Lock()
160
+
161
+
116
162
  def run_command(args: list[str], registry: bool = True) -> tuple[str, str]:
117
163
  """One command, in-process, as the terminal would run it: the text it
118
164
  printed, and a status an agent can branch on without parsing prose --
@@ -123,7 +169,8 @@ def run_command(args: list[str], registry: bool = True) -> tuple[str, str]:
123
169
  from fde.cli import app
124
170
 
125
171
  argv = [*args, "--registry", _registry()] if registry else list(args)
126
- result = CliRunner().invoke(app, argv)
172
+ with _ONE_AT_A_TIME:
173
+ result = CliRunner().invoke(app, argv)
127
174
  text = (result.output or "").strip()
128
175
  if result.exception and not isinstance(result.exception, SystemExit):
129
176
  return (f"{text}\n\nfde: {type(result.exception).__name__}: {result.exception}".strip(),
@@ -211,14 +258,14 @@ def build_server():
211
258
  """WRITES: reads a brief, pasted as text, into typed facts appended to
212
259
  the engagement's record, played back for correction with the
213
260
  follow-up questions. The engagement must be inside the workspace."""
214
- return run_command(["frame", str(confine(engagement, "engagement")), "--text", text])
261
+ return run_command(["frame", str(locate(engagement)), "--text", text])
215
262
 
216
263
  @server.tool(annotations=reads)
217
264
  @guarded
218
265
  def status(engagement: str):
219
266
  """The engagement's gates, what is known by scope and who said it, what
220
267
  is still missing, and which roles have not been heard."""
221
- return run_command(["status", str(confine(engagement, "engagement"))])
268
+ return run_command(["status", str(locate(engagement))])
222
269
 
223
270
  @server.tool(annotations=reads)
224
271
  @guarded
@@ -227,7 +274,7 @@ def build_server():
227
274
  and -- when it is a question -- the evidence already on record and
228
275
  the decisions that turn on each candidate answer. Reads only; the
229
276
  action it names is for a person to run."""
230
- return run_command(["next", str(confine(engagement, "engagement"))])
277
+ return run_command(["next", str(locate(engagement))])
231
278
 
232
279
  @server.tool(annotations=reads)
233
280
  @guarded
@@ -236,7 +283,7 @@ def build_server():
236
283
  validation, prototype, pilot, production, adoption, retrospective,
237
284
  or stopped -- with what would move it to the next stage. Reads only:
238
285
  a person runs `fde stage` to append the transition to the record."""
239
- args = ["stage", str(confine(engagement, "engagement")), "--no-record"]
286
+ args = ["stage", str(locate(engagement)), "--no-record"]
240
287
  target = confine(project, "project")
241
288
  return run_command(args + (["--project", str(target)] if target else []))
242
289
 
@@ -247,7 +294,7 @@ def build_server():
247
294
  settled -- failing gates, standing waivers, guessed or merely stated
248
295
  facts, disagreements, unsigned attestations, unheard roles, open
249
296
  incidents, undecided components -- each with an owner and an age."""
250
- return run_command(["debt", str(confine(engagement, "engagement"))])
297
+ return run_command(["debt", str(locate(engagement))])
251
298
 
252
299
  @server.tool(annotations=reads)
253
300
  @guarded
@@ -256,7 +303,7 @@ def build_server():
256
303
  """Judge the engagement's stop conditions against what the record has
257
304
  measured: the scorecard, the field journal, the recorded outcomes.
258
305
  Reads only; recording a condition is for a person."""
259
- args = ["stop-when", str(confine(engagement, "engagement"))]
306
+ args = ["stop-when", str(locate(engagement))]
260
307
  target = confine(project, "project")
261
308
  if target:
262
309
  args += ["--project", str(target)]
@@ -270,7 +317,7 @@ def build_server():
270
317
  def history(engagement: str):
271
318
  """Every dated entry on the engagement's record, in order, the undated
272
319
  ones above it, one line each."""
273
- return run_command(["history", str(confine(engagement, "engagement"))],
320
+ return run_command(["history", str(locate(engagement))],
274
321
  registry=False)
275
322
 
276
323
  @server.resource("fde://thirty-seconds", name="fde in thirty seconds",
@@ -293,6 +340,7 @@ def describe() -> str:
293
340
  else "reads"
294
341
  lines.append(f" {tool.name:10} {hint}")
295
342
  lines.append(" resources: fde://thirty-seconds")
343
+ lines.append(f" envelope: schema_version {SCHEMA_VERSION} (`fde mcp --schema`)")
296
344
  return "\n".join(lines)
297
345
 
298
346
 
@@ -224,10 +224,11 @@ def test_check_mode_creates_nothing(tmp_path, monkeypatch):
224
224
 
225
225
 
226
226
  def test_the_envelope_is_versioned_and_every_status_is_reachable(tmp_path, monkeypatch):
227
+ import jsonschema
228
+
227
229
  from fde import mcp_server
228
- from fde.mcp_server import ENVELOPE, SCHEMA_VERSION, STATUSES, reply, run_command
230
+ from fde.mcp_server import ENVELOPE_SCHEMA, SCHEMA_VERSION, STATUSES, reply, run_command
229
231
 
230
- assert set(ENVELOPE) == {"schema_version", "tool", "status", "mutated", "workspace"}
231
232
  monkeypatch.setenv("FDE_WORKSPACE", str(tmp_path))
232
233
  server = build_server()
233
234
  seen = {}
@@ -243,7 +244,7 @@ def test_the_envelope_is_versioned_and_every_status_is_reachable(tmp_path, monke
243
244
  for status in STATUSES:
244
245
  assert seen[status]["status"] == status
245
246
  assert seen[status]["schema_version"] == SCHEMA_VERSION
246
- assert set(seen[status]) == set(ENVELOPE)
247
+ jsonschema.validate(seen[status], ENVELOPE_SCHEMA)
247
248
 
248
249
  class Broken:
249
250
  output = "half-written"
@@ -270,3 +271,85 @@ def test_doctor_measures_the_install_and_fails_only_on_what_the_core_needs(tmp_p
270
271
  sick = runner.invoke(app, ["doctor", "--registry", str(tmp_path / "nowhere")])
271
272
  assert sick.exit_code == 1 and "NO registry" in sick.output
272
273
  assert "something the core needs is missing" in sick.output
274
+
275
+
276
+ # --- the interface as a product: the second call finds the first call's work
277
+
278
+
279
+ def test_an_engagement_is_found_by_the_name_start_gave_it(tmp_path, monkeypatch):
280
+ """`start(name="acme")` then `status(engagement="acme")` is the sequence
281
+ an agent writes first. Before this test the second call looked at
282
+ <workspace>/acme, found nothing, and said so; the record was at
283
+ engagements/acme, where `start` had put it and where `fde status acme`
284
+ finds it on the command line."""
285
+ monkeypatch.setenv("FDE_WORKSPACE", str(tmp_path))
286
+ server = build_server()
287
+ opened = asyncio.run(server.call_tool("start", {
288
+ "name": "acme", "statement": "Route each support message to one of 77 queues."}))
289
+ assert opened.structured_content["status"] == "ok"
290
+ assert (tmp_path / "engagements" / "acme" / "facts").is_dir()
291
+ for tool, expected in (("status", "blocked by"), ("next", "next: fde "),
292
+ ("stage", "acme: discovery"), ("debt", "decision debt as of"),
293
+ ("history", "acme: history"),
294
+ ("stop_when", "no stop conditions on record")):
295
+ result = asyncio.run(server.call_tool(tool, {"engagement": "acme"}))
296
+ assert expected in result.content[0].text, (tool, result.content[0].text)
297
+ assert result.structured_content["status"] == "ok", (tool, result.structured_content)
298
+ framed = asyncio.run(server.call_tool("frame", {
299
+ "engagement": "acme",
300
+ "text": "Two agents triage 9,000 emails a month by hand and misroute about 12 percent."}))
301
+ assert framed.structured_content["status"] == "ok" and framed.structured_content["mutated"]
302
+ # The path form still works, and a name that climbs is still refused
303
+ # before anything is looked for.
304
+ by_path = asyncio.run(server.call_tool("status", {"engagement": "engagements/acme"}))
305
+ assert by_path.structured_content["status"] == "ok"
306
+ climbing = asyncio.run(server.call_tool("status", {"engagement": "../acme"}))
307
+ assert climbing.structured_content["status"] == "refused"
308
+ assert "outside the workspace" in climbing.content[0].text
309
+
310
+
311
+ def test_a_corrupt_record_is_a_sentence_naming_the_file_not_a_traceback(tmp_path, monkeypatch):
312
+ monkeypatch.setenv("FDE_WORKSPACE", str(tmp_path))
313
+ server = build_server()
314
+ asyncio.run(server.call_tool("start", {
315
+ "name": "acme", "statement": "Route each support message to one of 77 queues."}))
316
+ asyncio.run(server.call_tool("frame", {
317
+ "engagement": "acme", "text": "Two agents triage 9,000 emails a month by hand."}))
318
+ record = next((tmp_path / "engagements" / "acme" / "facts").glob("*.yaml"))
319
+ with record.open("a") as fh:
320
+ fh.write('{"this is": not yaml\n')
321
+ for tool in ("status", "next", "debt"):
322
+ result = asyncio.run(server.call_tool(tool, {"engagement": "acme"}))
323
+ text = result.content[0].text
324
+ assert result.structured_content["status"] != "ok", tool
325
+ assert record.stem in text and "cannot read" in text, (tool, text)
326
+ assert "Traceback" not in text, (tool, text)
327
+
328
+
329
+ def test_the_schema_is_published_and_is_what_every_reply_follows(tmp_path, monkeypatch):
330
+ import json
331
+
332
+ import jsonschema
333
+ from typer.testing import CliRunner
334
+
335
+ from fde.cli import app
336
+ from fde.mcp_server import ENVELOPE_SCHEMA, SCHEMA_VERSION, TOOLS
337
+
338
+ result = CliRunner().invoke(app, ["mcp", "--schema"])
339
+ assert result.exit_code == 0, result.output
340
+ published = json.loads(result.output)
341
+ assert published == ENVELOPE_SCHEMA
342
+ jsonschema.Draft202012Validator.check_schema(published)
343
+ assert published["$id"].endswith(f"/v{SCHEMA_VERSION}")
344
+ # The schema's list of tools is the server's list of tools.
345
+ monkeypatch.setenv("FDE_WORKSPACE", str(tmp_path))
346
+ server = build_server()
347
+ assert {t.name for t in asyncio.run(server.list_tools())} == set(TOOLS)
348
+ # And an envelope with a field it does not name, or a status it does not
349
+ # name, fails validation: a client can trust the schema to be the contract.
350
+ good = asyncio.run(server.call_tool("triage", {"statements": ["Route."]})).structured_content
351
+ jsonschema.validate(good, published)
352
+ with pytest.raises(jsonschema.ValidationError):
353
+ jsonschema.validate({**good, "extra": 1}, published)
354
+ with pytest.raises(jsonschema.ValidationError):
355
+ jsonschema.validate({**good, "status": "done"}, published)
@@ -0,0 +1,119 @@
1
+ """The server as a client sees it: a separate process over stdio, driven by
2
+ the official MCP client, the way a coding agent drives it. The in-process
3
+ tests check each tool; these check the transport under what agents
4
+ actually do -- several calls at once, a call abandoned midway, a record
5
+ that changed on disk between calls."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import os
11
+ import sys
12
+
13
+ import pytest
14
+
15
+ pytest.importorskip("mcp", reason="the MCP extra is not installed")
16
+
17
+ from mcp import ClientSession, StdioServerParameters # noqa: E402
18
+ from mcp.client.stdio import stdio_client # noqa: E402
19
+
20
+ from fde.mcp_server import TOOLS # noqa: E402
21
+
22
+ STATEMENT = "Route each support message to one of 77 queues."
23
+ PATIENCE = 90 # seconds; a regression hangs the server, and the test must fail, not hang
24
+
25
+
26
+ def serve_in(workspace):
27
+ return StdioServerParameters(command=sys.executable, args=["-m", "fde.cli", "mcp"],
28
+ env={**os.environ, "FDE_WORKSPACE": str(workspace)})
29
+
30
+
31
+ def drive(workspace, scenario):
32
+ async def main():
33
+ async with stdio_client(serve_in(workspace)) as (read, write):
34
+ async with ClientSession(read, write) as session:
35
+ await asyncio.wait_for(session.initialize(), PATIENCE)
36
+ return await asyncio.wait_for(scenario(session), PATIENCE)
37
+ return asyncio.run(main())
38
+
39
+
40
+ def test_a_real_client_lists_the_tools_with_their_input_schemas_and_walks_by_name(tmp_path):
41
+ async def scenario(session):
42
+ listed = await session.list_tools()
43
+ schemas = {t.name: t.input_schema for t in listed.tools}
44
+ opened = await session.call_tool("start", {"name": "acme", "statement": STATEMENT})
45
+ status = await session.call_tool("status", {"engagement": "acme"})
46
+ return schemas, opened, status
47
+
48
+ schemas, opened, status = drive(tmp_path, scenario)
49
+ assert set(schemas) == set(TOOLS)
50
+ for name, schema in schemas.items():
51
+ assert schema["type"] == "object" and schema["properties"], name
52
+ if name not in ("triage", "start"):
53
+ assert "engagement" in schema["required"], name
54
+ assert opened.structured_content["status"] == "ok" and opened.structured_content["mutated"]
55
+ assert status.structured_content["status"] == "ok"
56
+ assert "blocked by" in status.content[0].text
57
+
58
+
59
+ def test_calls_issued_at_once_all_come_back(tmp_path):
60
+ """Claude Code issued `status` and `next` in the same breath and the
61
+ second came back "Connection closed": the server ran them on two
62
+ threads, and the output capture that each swaps in leaked one
63
+ command's text onto the JSON-RPC channel. Commands are now one at a
64
+ time inside the server."""
65
+ async def scenario(session):
66
+ await session.call_tool("start", {"name": "acme", "statement": STATEMENT})
67
+ rounds = []
68
+ for _ in range(3):
69
+ replies = await asyncio.gather(
70
+ *(session.call_tool(tool, {"engagement": "acme"})
71
+ for tool in ("status", "next", "debt", "stage", "history", "stop_when")),
72
+ return_exceptions=True)
73
+ rounds.append(replies)
74
+ after = await session.call_tool("history", {"engagement": "acme"})
75
+ return rounds, after
76
+
77
+ rounds, after = drive(tmp_path, scenario)
78
+ for replies in rounds:
79
+ for reply in replies:
80
+ assert not isinstance(reply, BaseException), repr(reply)
81
+ assert reply.structured_content["status"] == "ok", reply.content[0].text
82
+ assert reply.content[0].text.strip(), reply.structured_content
83
+ assert after.structured_content["status"] == "ok"
84
+
85
+
86
+ def test_a_call_abandoned_midway_leaves_the_server_answering(tmp_path):
87
+ async def scenario(session):
88
+ await session.call_tool("start", {"name": "acme", "statement": STATEMENT})
89
+ abandoned = asyncio.ensure_future(session.call_tool("next", {"engagement": "acme"}))
90
+ await asyncio.sleep(0)
91
+ abandoned.cancel()
92
+ try:
93
+ await abandoned
94
+ except asyncio.CancelledError:
95
+ pass
96
+ return await session.call_tool("status", {"engagement": "acme"})
97
+
98
+ status = drive(tmp_path, scenario)
99
+ assert status.structured_content["status"] == "ok"
100
+
101
+
102
+ def test_the_record_is_read_fresh_so_a_change_on_disk_shows_in_the_next_call(tmp_path):
103
+ from typer.testing import CliRunner
104
+
105
+ from fde.cli import app
106
+
107
+ async def scenario(session):
108
+ await session.call_tool("start", {"name": "acme", "statement": STATEMENT})
109
+ before = await session.call_tool("history", {"engagement": "acme"})
110
+ # Someone at the terminal frames a brief while the server is up.
111
+ result = CliRunner().invoke(app, ["frame", str(tmp_path / "engagements" / "acme"),
112
+ "--text", "Two agents triage 9,000 emails a month "
113
+ "by hand and misroute about 12 percent."])
114
+ assert result.exit_code == 0, result.output
115
+ after = await session.call_tool("history", {"engagement": "acme"})
116
+ return before.content[0].text, after.content[0].text
117
+
118
+ before, after = drive(tmp_path, scenario)
119
+ assert "0002-frame" not in before and "0002-frame" in after
@@ -0,0 +1,38 @@
1
+ """One version, in every place that states it. Two tags this week had to
2
+ be moved after the fact; this fails before the tag instead."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import tomllib
8
+ from pathlib import Path
9
+
10
+ REPO = Path(__file__).resolve().parent.parent
11
+
12
+
13
+ def test_pyproject_citation_and_changelog_agree_on_the_version():
14
+ version = tomllib.loads((REPO / "pyproject.toml").read_text())["project"]["version"]
15
+ citation = re.search(r"^version: (\S+)$", (REPO / "CITATION.cff").read_text(), re.M)
16
+ assert citation and citation.group(1) == version, "CITATION.cff lags pyproject.toml"
17
+ latest = re.search(r"^## \[(\d+\.\d+\.\d+)\]", (REPO / "CHANGELOG.md").read_text(), re.M)
18
+ assert latest and latest.group(1) == version, "CHANGELOG.md's newest entry lags the version"
19
+
20
+
21
+ def test_citation_and_changelog_agree_on_the_date_and_it_is_not_ahead_of_today():
22
+ """0.1.44 shipped on October 11 dated October 12, in both files, and an
23
+ outside reader noticed before I did. The two files must agree, and the
24
+ date may not be in the future; the release workflow goes further and
25
+ holds it to the day the tagged commit was authored."""
26
+ from datetime import date, timedelta
27
+
28
+ changelog = re.search(r"^## \[\d+\.\d+\.\d+\] — (\d{4}-\d{2}-\d{2})",
29
+ (REPO / "CHANGELOG.md").read_text(), re.M)
30
+ assert changelog, "the newest CHANGELOG entry carries no date"
31
+ citation = re.search(r"^date-released: '(\d{4}-\d{2}-\d{2})'$",
32
+ (REPO / "CITATION.cff").read_text(), re.M)
33
+ assert citation and citation.group(1) == changelog.group(1), \
34
+ "CITATION.cff date-released != the newest CHANGELOG date"
35
+ released = date.fromisoformat(changelog.group(1))
36
+ # A day of slack: the author's calendar and the test runner's need not
37
+ # agree at midnight.
38
+ assert released <= date.today() + timedelta(days=1), f"{released} has not happened yet"