drydock-cli 3.1.4__tar.gz → 3.1.6__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 (190) hide show
  1. {drydock_cli-3.1.4/drydock_cli.egg-info → drydock_cli-3.1.6}/PKG-INFO +17 -1
  2. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/README.md +16 -0
  3. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/fiar.py +153 -1
  4. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tools/__init__.py +63 -1
  5. {drydock_cli-3.1.4 → drydock_cli-3.1.6/drydock_cli.egg-info}/PKG-INFO +17 -1
  6. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/pyproject.toml +1 -1
  7. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_fiar.py +57 -0
  8. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/LICENSE +0 -0
  9. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/NOTICE +0 -0
  10. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/__init__.py +0 -0
  11. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/__main__.py +0 -0
  12. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/advisor.py +0 -0
  13. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/agent.py +0 -0
  14. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/bash_safety.py +0 -0
  15. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/budget.py +0 -0
  16. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/__init__.py +0 -0
  17. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/document-canvas.md +0 -0
  18. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/fiar-assess.md +0 -0
  19. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/fiar-cap.md +0 -0
  20. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/fiar-evidence.md +0 -0
  21. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/fiar-readiness.md +0 -0
  22. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/ml-data.md +0 -0
  23. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/ml-debug.md +0 -0
  24. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/ml-finetune.md +0 -0
  25. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/ml-metrics.md +0 -0
  26. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/ml-rl.md +0 -0
  27. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/ml-train.md +0 -0
  28. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
  29. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/nist-csf.md +0 -0
  30. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/rmf-categorize.md +0 -0
  31. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/rmf-control.md +0 -0
  32. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/rmf-poam.md +0 -0
  33. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/rmf-review.md +0 -0
  34. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/stig-assess.md +0 -0
  35. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/builtin_skills/stig-remediate.md +0 -0
  36. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/cci.py +0 -0
  37. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/cli.py +0 -0
  38. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/compaction.py +0 -0
  39. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/config.py +0 -0
  40. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/detect.py +0 -0
  41. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/doccanvas.py +0 -0
  42. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/events.py +0 -0
  43. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/extract.py +0 -0
  44. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/gittools.py +0 -0
  45. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/graphrag.py +0 -0
  46. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/guards.py +0 -0
  47. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/loop_detect.py +0 -0
  48. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/mcp.py +0 -0
  49. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/phases.py +0 -0
  50. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/poam.py +0 -0
  51. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/progress.py +0 -0
  52. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/providers.py +0 -0
  53. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/recipes.py +0 -0
  54. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/recovery.py +0 -0
  55. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/resume.py +0 -0
  56. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/rmf.py +0 -0
  57. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/rmf_graph.py +0 -0
  58. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/skills.py +0 -0
  59. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/stig.py +0 -0
  60. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/subagents.py +0 -0
  61. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/suggest.py +0 -0
  62. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/task_state.py +0 -0
  63. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tool_policy.py +0 -0
  64. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tool_registry.py +0 -0
  65. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tool_result.py +0 -0
  66. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tool_select.py +0 -0
  67. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tool_validate.py +0 -0
  68. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/trajectory.py +0 -0
  69. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tui/__init__.py +0 -0
  70. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tui/app.py +0 -0
  71. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tui/approval.py +0 -0
  72. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tui/messages.py +0 -0
  73. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tui/widgets.py +0 -0
  74. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/tuning.py +0 -0
  75. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/verification.py +0 -0
  76. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock/web.py +0 -0
  77. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock_cli.egg-info/SOURCES.txt +0 -0
  78. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock_cli.egg-info/dependency_links.txt +0 -0
  79. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock_cli.egg-info/entry_points.txt +0 -0
  80. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock_cli.egg-info/requires.txt +0 -0
  81. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/drydock_cli.egg-info/top_level.txt +0 -0
  82. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/setup.cfg +0 -0
  83. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_advisor.py +0 -0
  84. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_approval.py +0 -0
  85. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_back_command.py +0 -0
  86. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_background.py +0 -0
  87. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_binary_output.py +0 -0
  88. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_crossplatform.py +0 -0
  89. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_output_bounding.py +0 -0
  90. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_process_group.py +0 -0
  91. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_safety.py +0 -0
  92. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_sanitize.py +0 -0
  93. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_shell.py +0 -0
  94. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_stdin.py +0 -0
  95. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_stop_partial.py +0 -0
  96. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_timeout_network.py +0 -0
  97. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_bash_timeout_param.py +0 -0
  98. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_budget.py +0 -0
  99. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_cci.py +0 -0
  100. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_cli_agents.py +0 -0
  101. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_compact_command.py +0 -0
  102. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_compaction.py +0 -0
  103. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_config.py +0 -0
  104. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_config_migration.py +0 -0
  105. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_context_limit_config.py +0 -0
  106. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_context_limit_issue25.py +0 -0
  107. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_criteria_coverage.py +0 -0
  108. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_cycling_detection.py +0 -0
  109. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_degenerate_argument.py +0 -0
  110. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_detect.py +0 -0
  111. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_dispatch.py +0 -0
  112. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_doccanvas.py +0 -0
  113. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_e2e_connected.py +0 -0
  114. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_edit_replace_all.py +0 -0
  115. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_effort_governor.py +0 -0
  116. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_empty_response.py +0 -0
  117. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_events.py +0 -0
  118. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_extract.py +0 -0
  119. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_failure_loop.py +0 -0
  120. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_first_run_setup.py +0 -0
  121. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_gittools.py +0 -0
  122. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_glob_edit_edges.py +0 -0
  123. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_graphify_example.py +0 -0
  124. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_graphrag.py +0 -0
  125. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_graphrag_quoted_path.py +0 -0
  126. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_graphrag_sqlite.py +0 -0
  127. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_grep_and_read_robust.py +0 -0
  128. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_guards_and_tools.py +0 -0
  129. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_hallucinated_tools.py +0 -0
  130. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_leaked_tool_call.py +0 -0
  131. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_loop_detect.py +0 -0
  132. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_mcp.py +0 -0
  133. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_model_registry.py +0 -0
  134. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_oneshot_unreachable.py +0 -0
  135. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_overthink_interrupt.py +0 -0
  136. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_phases.py +0 -0
  137. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_plan_autocontinue.py +0 -0
  138. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_poam.py +0 -0
  139. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_progress.py +0 -0
  140. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_providers_unreachable.py +0 -0
  141. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_read_index.py +0 -0
  142. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_recipes.py +0 -0
  143. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_recovery.py +0 -0
  144. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_recovery_config.py +0 -0
  145. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_recovery_integration.py +0 -0
  146. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_repeated_outcome.py +0 -0
  147. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_repetition_interrupt.py +0 -0
  148. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_resume_events.py +0 -0
  149. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_resume_restore.py +0 -0
  150. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_resume_snapshot.py +0 -0
  151. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_rmf.py +0 -0
  152. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_rmf_graph.py +0 -0
  153. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_rmf_stig_graph.py +0 -0
  154. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_rolling_plan.py +0 -0
  155. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_runaway_repetition.py +0 -0
  156. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_screenshot.py +0 -0
  157. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_server_probe.py +0 -0
  158. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_skills.py +0 -0
  159. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_sqlite_events.py +0 -0
  160. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_stall_retry.py +0 -0
  161. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_stig.py +0 -0
  162. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_stop.py +0 -0
  163. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_streaming_newlines.py +0 -0
  164. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_subagent.py +0 -0
  165. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_subagents.py +0 -0
  166. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_suggest.py +0 -0
  167. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_system_prompt_help.py +0 -0
  168. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_task_state.py +0 -0
  169. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_timeline.py +0 -0
  170. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_todo.py +0 -0
  171. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tool_arg_coercion.py +0 -0
  172. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tool_arg_coercion_more.py +0 -0
  173. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tool_arg_parsing.py +0 -0
  174. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tool_canonical.py +0 -0
  175. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tool_policy.py +0 -0
  176. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tool_result.py +0 -0
  177. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tool_select.py +0 -0
  178. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tool_validate.py +0 -0
  179. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tools_undo.py +0 -0
  180. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_trajectory.py +0 -0
  181. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tui.py +0 -0
  182. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_tuning.py +0 -0
  183. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_verification_gate.py +0 -0
  184. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_viewimage.py +0 -0
  185. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_vision_input.py +0 -0
  186. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_web_tools.py +0 -0
  187. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_windows_shell.py +0 -0
  188. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_worker_subagent.py +0 -0
  189. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_write_content_coerce.py +0 -0
  190. {drydock_cli-3.1.4 → drydock_cli-3.1.6}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.1.4
3
+ Version: 3.1.6
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -267,6 +267,22 @@ exports the open findings to a deterministic **eMASS POA&M CSV** — Control (fr
267
267
  the CCI map), Vulnerability Description, `POA&M Status=Ongoing`, Milestone (the
268
268
  Fix Text), and Severity (CAT I/II/III → High/Moderate/Low) — no LLM, stdlib only.
269
269
 
270
+ ### FIAR audit-readiness (DoD financial-statement audit)
271
+
272
+ Model a Financial Improvement and Audit Readiness engagement — a seeded key-control
273
+ matrix per business cycle (FBWT, P2P, PP&E, INV, CIVPAY, REIM, FR, ITGC), the five FS
274
+ assertions, a deterministic **evidence-chain validator** (a control can't be called
275
+ effective on an incomplete population→sample→…→GL→assertion trace), findings (NFRs), and
276
+ CAPs. `python -m drydock.fiar new|controls|control|assess|reconcile|package`; skills
277
+ `/fiar-assess /fiar-evidence /fiar-readiness /fiar-cap`.
278
+
279
+ **KSD evidence packaging.** `fiar package <engagement> <out> --evidence-dir <dir>` assembles
280
+ an audit binder — `index.md` + `index.json` mapping every control → assertions → KSDs →
281
+ status → evidence-chain completeness → findings — and **collects the actual evidence files
282
+ each test cited**, zipped. Add `--redact "<term>"` (repeatable) to **red-box** names/secrets
283
+ in the evidence *and* the manifest for a releasable version (verified via the Document
284
+ Canvas); the original engagement is never touched. Also exposed as the `FiarPackage` tool.
285
+
270
286
  ### Document Canvas — editing documents far larger than the context window
271
287
 
272
288
  Edit a 300-, 800-, 1000-page document the way you edit a large codebase: **search,
@@ -243,6 +243,22 @@ exports the open findings to a deterministic **eMASS POA&M CSV** — Control (fr
243
243
  the CCI map), Vulnerability Description, `POA&M Status=Ongoing`, Milestone (the
244
244
  Fix Text), and Severity (CAT I/II/III → High/Moderate/Low) — no LLM, stdlib only.
245
245
 
246
+ ### FIAR audit-readiness (DoD financial-statement audit)
247
+
248
+ Model a Financial Improvement and Audit Readiness engagement — a seeded key-control
249
+ matrix per business cycle (FBWT, P2P, PP&E, INV, CIVPAY, REIM, FR, ITGC), the five FS
250
+ assertions, a deterministic **evidence-chain validator** (a control can't be called
251
+ effective on an incomplete population→sample→…→GL→assertion trace), findings (NFRs), and
252
+ CAPs. `python -m drydock.fiar new|controls|control|assess|reconcile|package`; skills
253
+ `/fiar-assess /fiar-evidence /fiar-readiness /fiar-cap`.
254
+
255
+ **KSD evidence packaging.** `fiar package <engagement> <out> --evidence-dir <dir>` assembles
256
+ an audit binder — `index.md` + `index.json` mapping every control → assertions → KSDs →
257
+ status → evidence-chain completeness → findings — and **collects the actual evidence files
258
+ each test cited**, zipped. Add `--redact "<term>"` (repeatable) to **red-box** names/secrets
259
+ in the evidence *and* the manifest for a releasable version (verified via the Document
260
+ Canvas); the original engagement is never touched. Also exposed as the `FiarPackage` tool.
261
+
246
262
  ### Document Canvas — editing documents far larger than the context window
247
263
 
248
264
  Edit a 300-, 800-, 1000-page document the way you edit a large codebase: **search,
@@ -20,6 +20,9 @@ not render an audit judgement.
20
20
  from __future__ import annotations
21
21
 
22
22
  import json
23
+ import re
24
+ import shutil
25
+ import zipfile
23
26
  from dataclasses import dataclass, field, asdict
24
27
  from datetime import date
25
28
  from pathlib import Path
@@ -466,6 +469,139 @@ def new_engagement(name: str, cycle: str, wave: int | None = None) -> Engagement
466
469
  })
467
470
 
468
471
 
472
+ # ── KSD evidence package (audit binder) ────────────────────────────────────
473
+ # Assemble the Key Supporting Documents behind an engagement into a reviewable
474
+ # package: a manifest (control → assertion → KSDs → status → evidence chain →
475
+ # findings) plus the actual evidence files, zipped. Optionally red-box text
476
+ # evidence for a releasable version. Deterministic, stdlib only.
477
+ _FILENAME_RE = re.compile(r"[\w][\w./-]*\.[A-Za-z0-9]{1,6}")
478
+
479
+
480
+ def _referenced_files(control: dict, evidence_dir: Path | None) -> list[str]:
481
+ """Filenames named in the control's evidence / chain text that actually exist
482
+ in evidence_dir (so the binder collects the KSDs the test cited)."""
483
+ if not evidence_dir or not evidence_dir.is_dir():
484
+ return []
485
+ avail = {p.name: p for p in evidence_dir.iterdir() if p.is_file()}
486
+ text = " ".join([str(control.get("evidence", "")),
487
+ *[str(v) for v in (control.get("evidence_chain") or {}).values()]])
488
+ found: list[str] = []
489
+ for m in _FILENAME_RE.findall(text):
490
+ name = Path(m).name
491
+ if name in avail and name not in found:
492
+ found.append(name)
493
+ return found
494
+
495
+
496
+ def build_ksd_package(engagement_path: str | Path, out_dir: str | Path, *,
497
+ evidence_dir: str | Path | None = None, cycle: str | None = None,
498
+ status: str | None = None, redact: list[str] | None = None) -> dict:
499
+ """Build a KSD evidence package from an engagement. Collects the cited evidence
500
+ files (from evidence_dir), writes index.json + index.md, optionally red-boxes
501
+ text evidence, and zips the binder. Returns a summary dict."""
502
+ eng = Engagement.load(engagement_path)
503
+ out = Path(out_dir)
504
+ (out / "evidence").mkdir(parents=True, exist_ok=True)
505
+ ev_dir = Path(evidence_dir) if evidence_dir else None
506
+ redact_terms = [t for t in (redact or []) if t.strip()]
507
+
508
+ findings_by_control: dict[str, list] = {}
509
+ for f in eng._d.get("findings", []):
510
+ findings_by_control.setdefault(f.get("control_id", ""), []).append(f)
511
+
512
+ controls = list(eng._d.get("controls", []))
513
+ if cycle:
514
+ controls = [c for c in controls if str(c.get("cycle", "")).upper() == cycle.upper()]
515
+ if status:
516
+ controls = [c for c in controls if c.get("status") == status]
517
+
518
+ entries = []
519
+ collected = 0
520
+ redactions = []
521
+ for c in controls:
522
+ cid = c["id"]
523
+ copied = []
524
+ for name in _referenced_files(c, ev_dir):
525
+ dest = out / "evidence" / cid
526
+ dest.mkdir(parents=True, exist_ok=True)
527
+ dst = dest / name
528
+ shutil.copy(ev_dir / name, dst) # type: ignore[arg-type]
529
+ if redact_terms and dst.suffix.lower() in (".txt", ".md", ".markdown"):
530
+ from drydock import doccanvas as dc
531
+ fmt = "text" if dst.suffix.lower() == ".txt" else "markdown"
532
+ doc = dc.parse(dst.read_text("utf-8", "replace"), fmt, str(dst))
533
+ n = 0
534
+ for term in redact_terms:
535
+ try:
536
+ n += dc.redact(doc, query=term)["redacted"]
537
+ except dc.PatchError:
538
+ pass
539
+ if n:
540
+ dst.write_text(dc.render(doc), "utf-8")
541
+ redactions.append({"file": f"{cid}/{name}", "redacted": n})
542
+ copied.append(name)
543
+ collected += 1
544
+ entries.append({
545
+ "id": cid, "cycle": c.get("cycle"), "objective": c.get("objective"),
546
+ "assertions": c.get("assertions"), "status": c.get("status", "not_tested"),
547
+ "ksds": c.get("ksds", []), "evidence": c.get("evidence", ""),
548
+ "evidence_chain": validate_chain(c.get("evidence_chain")),
549
+ "evidence_files": copied,
550
+ "findings": [{"id": f.get("id"), "severity": f.get("severity"),
551
+ "condition": f.get("condition")} for f in findings_by_control.get(cid, [])],
552
+ })
553
+
554
+ # For a releasable package, scrub the redaction terms from the MANIFEST too
555
+ # (not just the collected files) so a name/secret can't leak via the binder.
556
+ if redact_terms:
557
+ marker = "[REDACTED]"
558
+ for e in entries:
559
+ for term in redact_terms:
560
+ e["evidence"] = e["evidence"].replace(term, marker)
561
+ for f in e["findings"]:
562
+ f["condition"] = (f.get("condition") or "").replace(term, marker)
563
+
564
+ manifest = {
565
+ "engagement": eng.name, "assessable_unit": eng.cycle, "wave": eng.wave,
566
+ "generated": date.today().isoformat(), "source": Path(engagement_path).name,
567
+ "control_count": len(entries), "evidence_files_collected": collected,
568
+ "redactions": redactions, "controls": entries,
569
+ }
570
+ (out / "index.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
571
+ (out / "index.md").write_text(_ksd_index_md(manifest), encoding="utf-8")
572
+
573
+ zip_path = str(out) + ".zip"
574
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
575
+ for p in sorted(out.rglob("*")):
576
+ if p.is_file():
577
+ z.write(p, p.relative_to(out.parent))
578
+ return {"package": str(out), "zip": zip_path, "controls": len(entries),
579
+ "files_collected": collected, "redactions": len(redactions),
580
+ "missing_evidence": [e["id"] for e in entries if not e["evidence_files"]]}
581
+
582
+
583
+ def _ksd_index_md(m: dict) -> str:
584
+ lines = [f"# KSD Evidence Package — {m['engagement']}",
585
+ f"Assessable unit: {m['assessable_unit']} · Wave {m['wave']} · "
586
+ f"generated {m['generated']} from {m['source']}",
587
+ f"Controls: {m['control_count']} · evidence files collected: "
588
+ f"{m['evidence_files_collected']} · redactions: {len(m['redactions'])}", ""]
589
+ for e in m["controls"]:
590
+ lines.append(f"## {e['id']} — {e['objective']}")
591
+ lines.append(f"- Cycle: {e['cycle']} · Assertions: {', '.join(e['assertions'] or [])}"
592
+ f" · Status: **{e['status']}**")
593
+ lines.append(f"- KSDs required: {', '.join(e['ksds']) or '(none listed)'}")
594
+ ch = e["evidence_chain"]
595
+ lines.append(f"- Evidence chain: {'COMPLETE' if ch['complete'] else 'INCOMPLETE — missing ' + ', '.join(ch['missing'])}")
596
+ if e["evidence"]:
597
+ lines.append(f"- Evidence examined: {e['evidence']}")
598
+ lines.append(f"- Evidence files: {', '.join('evidence/'+e['id']+'/'+f for f in e['evidence_files']) or '⚠ NONE collected'}")
599
+ for f in e["findings"]:
600
+ lines.append(f"- FINDING {f['id']} ({f['severity']}): {f['condition']}")
601
+ lines.append("")
602
+ return "\n".join(lines)
603
+
604
+
469
605
  # ── CLI ───────────────────────────────────────────────────────────────────
470
606
  def _main(argv: list[str]) -> int:
471
607
  import argparse
@@ -482,6 +618,9 @@ def _main(argv: list[str]) -> int:
482
618
  q = sub.add_parser("reconcile"); q.add_argument("entity", type=float)
483
619
  q.add_argument("source", type=float); q.add_argument("--tolerance", type=float, default=0.0)
484
620
  q = sub.add_parser("cycles")
621
+ q = sub.add_parser("package"); q.add_argument("path"); q.add_argument("out")
622
+ q.add_argument("--evidence-dir"); q.add_argument("--cycle"); q.add_argument("--status")
623
+ q.add_argument("--redact", action="append", default=[])
485
624
  a = p.parse_args(argv)
486
625
 
487
626
  if a.cmd == "cycles":
@@ -491,10 +630,23 @@ def _main(argv: list[str]) -> int:
491
630
  if a.cmd == "reconcile":
492
631
  r = reconcile(a.entity, a.source, tolerance=a.tolerance)
493
632
  print(json.dumps(r, indent=2)); return 0 if r["reconciled"] else 1
633
+ if a.cmd == "package":
634
+ r = build_ksd_package(a.path, a.out, evidence_dir=a.evidence_dir,
635
+ cycle=a.cycle, status=a.status, redact=a.redact)
636
+ print(f"KSD package: {r['controls']} controls, {r['files_collected']} evidence file(s)"
637
+ + (f", {r['redactions']} file(s) redacted" if r["redactions"] else "")
638
+ + f" → {r['package']}/ (+ {Path(r['zip']).name})")
639
+ if r["missing_evidence"]:
640
+ print(f"⚠ no evidence file collected for: {', '.join(r['missing_evidence'])}")
641
+ return 0
494
642
  if a.cmd == "new":
495
643
  eng = new_engagement(a.name, a.cycle, wave=a.wave); eng.save(a.path)
496
644
  print(f"created {a.path}: {eng.name} / {eng.cycle} (Wave {eng.wave}) "
497
- f"— {len(eng.controls)} controls"); return 0
645
+ f"— {len(eng.controls)} controls")
646
+ if not eng.controls and a.cycle.strip().upper() not in ("*", "ALL"):
647
+ print(f"⚠ 0 controls seeded — '{a.cycle}' is not a known cycle. Valid cycles: "
648
+ f"{', '.join(CYCLES)}, or ALL. (See: python -m drydock.fiar cycles)")
649
+ return 0
498
650
 
499
651
  eng = Engagement.load(a.path)
500
652
  if a.cmd == "controls":
@@ -654,6 +654,31 @@ SCHEMAS = [
654
654
  "required": ["entity", "source"],
655
655
  },
656
656
  },
657
+ {
658
+ "name": "FiarPackage",
659
+ "description": (
660
+ "Assemble a KSD AUDIT EVIDENCE PACKAGE from a FIAR engagement: a binder "
661
+ "(index.md + index.json mapping each control → assertions → KSDs → status → "
662
+ "evidence-chain completeness → findings) plus the actual evidence files it "
663
+ "cited (collected from `evidence_dir`), zipped. Pass `redact` (one or a list "
664
+ "of terms) to red-box names/secrets in the text evidence AND the manifest for "
665
+ "a releasable version — the original engagement is untouched. Optionally filter "
666
+ "by `cycle` or `status`."
667
+ ),
668
+ "input_schema": {
669
+ "type": "object",
670
+ "properties": {
671
+ "path": {"type": "string", "description": "Engagement JSON path."},
672
+ "out": {"type": "string", "description": "Output package directory (a .zip is written beside it)."},
673
+ "evidence_dir": {"type": "string", "description": "Directory holding the KSD evidence files to collect."},
674
+ "cycle": {"type": "string", "description": "Only include this cycle (e.g. FBWT)."},
675
+ "status": {"type": "string", "description": "Only include controls with this status."},
676
+ "redact": {"type": "array", "items": {"type": "string"},
677
+ "description": "Terms to red-box in evidence files + manifest for release."},
678
+ },
679
+ "required": ["path", "out"],
680
+ },
681
+ },
657
682
  {
658
683
  "name": "GraphQuery",
659
684
  "description": (
@@ -973,6 +998,13 @@ def _file_index(lines: list[str], fp: str) -> str:
973
998
  f"offset/limit (e.g. {{\"file_path\": ..., \"offset\": <line-1>, "
974
999
  f"\"limit\": 120}}).\n"
975
1000
  )
1001
+ if is_doc:
1002
+ head += (
1003
+ "To SEARCH or EDIT this document, prefer the DOCUMENT CANVAS: DocOpen, then "
1004
+ "DocSearch/DocRead a region and DocPatch/DocReplace/DocRedact by block id → "
1005
+ "DocValidate → DocCommit — it edits precisely without loading the whole file "
1006
+ "(don't rewrite a large document with Write/Edit).\n"
1007
+ )
976
1008
  if not anchors:
977
1009
  return head + "(no def/class/header anchors found — read ranges with offset/limit.)"
978
1010
  body = "\n".join(anchors[:400])
@@ -2186,6 +2218,35 @@ def tool_fiarreconcile(params: dict, config: dict) -> str:
2186
2218
  f"difference {r['difference']:,.2f} (tolerance {r['tolerance']:,.2f}).")
2187
2219
 
2188
2220
 
2221
+ def tool_fiarpackage(params: dict, config: dict) -> str:
2222
+ """Assemble a KSD audit evidence package (binder + files, zipped) from an engagement."""
2223
+ from drydock import fiar
2224
+ path = _as_str_arg(params.get("path") or "")
2225
+ out = _as_str_arg(params.get("out") or params.get("out_dir") or "")
2226
+ if not path or not out:
2227
+ return "Error: FiarPackage needs `path` (engagement JSON) and `out` (package directory)."
2228
+ redact = params.get("redact")
2229
+ if isinstance(redact, str):
2230
+ redact = [redact]
2231
+ try:
2232
+ r = fiar.build_ksd_package(
2233
+ path, out,
2234
+ evidence_dir=_as_str_arg(params.get("evidence_dir") or "") or None,
2235
+ cycle=_as_str_arg(params.get("cycle") or "") or None,
2236
+ status=_as_str_arg(params.get("status") or "") or None,
2237
+ redact=[str(x) for x in redact] if isinstance(redact, list) else None)
2238
+ except FileNotFoundError:
2239
+ return f"Error: engagement not found: {path}"
2240
+ except Exception as e:
2241
+ return f"Error building KSD package: {e}"
2242
+ msg = (f"KSD package built: {r['controls']} control(s), {r['files_collected']} evidence file(s)"
2243
+ + (f", {r['redactions']} file(s) red-boxed" if r["redactions"] else "")
2244
+ + f" → {r['package']}/ (index.md + index.json + evidence/, zipped as {Path(r['zip']).name}).")
2245
+ if r["missing_evidence"]:
2246
+ msg += f" ⚠ no evidence file collected for: {', '.join(r['missing_evidence'])}."
2247
+ return msg
2248
+
2249
+
2189
2250
  def tool_graphquery(params: dict, config: dict) -> str:
2190
2251
  """Traverse the RMF typed ontology graph (read-only)."""
2191
2252
  rmf_graph, g, _ = _rmf_graph(config)
@@ -2618,6 +2679,7 @@ _TOOLS = [
2618
2679
  ("FiarAssess", tool_fiarassess, False),
2619
2680
  ("FiarFinding", tool_fiarfinding, False),
2620
2681
  ("FiarReconcile", tool_fiarreconcile, True),
2682
+ ("FiarPackage", tool_fiarpackage, False),
2621
2683
  ("WebSearch", tool_websearch, True),
2622
2684
  ("WebFetch", tool_webfetch, True),
2623
2685
  ("GitStatus", tool_gitstatus, True),
@@ -2653,7 +2715,7 @@ def register_all():
2653
2715
  "StigRules": tool_stigrules, "StigRule": tool_stigrule, "StigSet": tool_stigset,
2654
2716
  "FiarControls": tool_fiarcontrols, "FiarControl": tool_fiarcontrol,
2655
2717
  "FiarAssess": tool_fiarassess, "FiarFinding": tool_fiarfinding,
2656
- "FiarReconcile": tool_fiarreconcile,
2718
+ "FiarReconcile": tool_fiarreconcile, "FiarPackage": tool_fiarpackage,
2657
2719
  "WebSearch": tool_websearch, "WebFetch": tool_webfetch,
2658
2720
  "GitStatus": tool_gitstatus, "GitDiff": tool_gitdiff,
2659
2721
  "GitLog": tool_gitlog, "GitCommit": tool_gitcommit,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.1.4
3
+ Version: 3.1.6
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -267,6 +267,22 @@ exports the open findings to a deterministic **eMASS POA&M CSV** — Control (fr
267
267
  the CCI map), Vulnerability Description, `POA&M Status=Ongoing`, Milestone (the
268
268
  Fix Text), and Severity (CAT I/II/III → High/Moderate/Low) — no LLM, stdlib only.
269
269
 
270
+ ### FIAR audit-readiness (DoD financial-statement audit)
271
+
272
+ Model a Financial Improvement and Audit Readiness engagement — a seeded key-control
273
+ matrix per business cycle (FBWT, P2P, PP&E, INV, CIVPAY, REIM, FR, ITGC), the five FS
274
+ assertions, a deterministic **evidence-chain validator** (a control can't be called
275
+ effective on an incomplete population→sample→…→GL→assertion trace), findings (NFRs), and
276
+ CAPs. `python -m drydock.fiar new|controls|control|assess|reconcile|package`; skills
277
+ `/fiar-assess /fiar-evidence /fiar-readiness /fiar-cap`.
278
+
279
+ **KSD evidence packaging.** `fiar package <engagement> <out> --evidence-dir <dir>` assembles
280
+ an audit binder — `index.md` + `index.json` mapping every control → assertions → KSDs →
281
+ status → evidence-chain completeness → findings — and **collects the actual evidence files
282
+ each test cited**, zipped. Add `--redact "<term>"` (repeatable) to **red-box** names/secrets
283
+ in the evidence *and* the manifest for a releasable version (verified via the Document
284
+ Canvas); the original engagement is never touched. Also exposed as the `FiarPackage` tool.
285
+
270
286
  ### Document Canvas — editing documents far larger than the context window
271
287
 
272
288
  Edit a 300-, 800-, 1000-page document the way you edit a large codebase: **search,
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  # PyPI distribution name is drydock-cli (the established install name, continued
8
8
  # from the retired v2 fork); the import package + CLI command stay `drydock`.
9
9
  name = "drydock-cli"
10
- version = "3.1.4"
10
+ version = "3.1.6"
11
11
  description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -209,3 +209,60 @@ def test_effective_without_chain_advises(tmp_path):
209
209
  def test_control_detail_shows_chain_state(tmp_path):
210
210
  p = _mk(tmp_path)
211
211
  assert "not traced" in tool_fiarcontrol({"path": p, "id": "FBWT-01"}, {})
212
+
213
+
214
+ # ── KSD evidence packaging ────────────────────────────────────────────────
215
+ def _mk_assessed(tmp_path):
216
+ """Engagement with FBWT-01 effective (evidence cites recon.txt + full chain)
217
+ and FBWT-02 deficient with a finding; plus an evidence dir holding recon.txt."""
218
+ p = _mk(tmp_path)
219
+ ev = tmp_path / "evidence"
220
+ ev.mkdir()
221
+ (ev / "recon.txt").write_text("Recon: preparer J. Smith; GL ties to Treasury 0.00.\n")
222
+ e = fiar.Engagement.load(p)
223
+ e.assess("FBWT-01", status="effective",
224
+ evidence="Examined recon.txt: ties out, preparer J. Smith signed.",
225
+ chain={k: f"ev-{k}" for k in fiar.EVIDENCE_CHAIN})
226
+ e.assess("FBWT-02", status="deficient", evidence="No aging report provided.")
227
+ e.add_finding(control_id="FBWT-02", condition="UDO aging not maintained",
228
+ severity="significant_deficiency")
229
+ e.save(p)
230
+ return p, str(ev)
231
+
232
+
233
+ def test_ksd_package_builds_binder_and_zip(tmp_path):
234
+ p, ev = _mk_assessed(tmp_path)
235
+ out = tmp_path / "pkg"
236
+ r = fiar.build_ksd_package(p, out, evidence_dir=ev)
237
+ assert (out / "index.json").exists() and (out / "index.md").exists()
238
+ assert (tmp_path / "pkg.zip").exists()
239
+ assert (out / "evidence" / "FBWT-01" / "recon.txt").exists() # cited file collected
240
+ assert r["files_collected"] == 1
241
+ assert "FBWT-02" in r["missing_evidence"] # no file cited
242
+ man = json.loads((out / "index.json").read_text())
243
+ by_id = {c["id"]: c for c in man["controls"]}
244
+ assert by_id["FBWT-01"]["evidence_chain"]["complete"] is True
245
+ assert by_id["FBWT-02"]["evidence_chain"]["complete"] is False
246
+ assert by_id["FBWT-02"]["findings"][0]["severity"] == "significant_deficiency"
247
+
248
+
249
+ def test_ksd_package_redaction_no_leak(tmp_path):
250
+ p, ev = _mk_assessed(tmp_path)
251
+ out = tmp_path / "rel"
252
+ fiar.build_ksd_package(p, out, evidence_dir=ev, redact=["J. Smith"])
253
+ # the name must be gone from BOTH the collected file AND the manifest
254
+ assert "J. Smith" not in (out / "evidence" / "FBWT-01" / "recon.txt").read_text()
255
+ assert "J. Smith" not in (out / "index.json").read_text()
256
+ assert "J. Smith" not in (out / "index.md").read_text()
257
+ assert "[REDACTED]" in (out / "index.md").read_text()
258
+ # original engagement + evidence dir are untouched
259
+ assert "J. Smith" in fiar.Engagement.load(p).control("FBWT-01").evidence
260
+
261
+
262
+ def test_ksd_package_cycle_filter(tmp_path):
263
+ p = _mk(tmp_path, cycle="ALL")
264
+ out = tmp_path / "fbwt_only"
265
+ r = fiar.build_ksd_package(p, out, cycle="FBWT")
266
+ man = json.loads((out / "index.json").read_text())
267
+ assert r["controls"] >= 1
268
+ assert all(c["cycle"] == "FBWT" for c in man["controls"])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes