drydock-cli 3.0.171__tar.gz → 3.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. {drydock_cli-3.0.171/drydock_cli.egg-info → drydock_cli-3.1.2}/PKG-INFO +1 -1
  2. drydock_cli-3.1.2/drydock/builtin_skills/fiar-assess.md +33 -0
  3. drydock_cli-3.1.2/drydock/builtin_skills/fiar-cap.md +26 -0
  4. drydock_cli-3.1.2/drydock/builtin_skills/fiar-evidence.md +33 -0
  5. drydock_cli-3.1.2/drydock/builtin_skills/fiar-readiness.md +28 -0
  6. drydock_cli-3.1.2/drydock/fiar.py +528 -0
  7. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/loop_detect.py +91 -3
  8. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/providers.py +101 -1
  9. drydock_cli-3.1.2/drydock/suggest.py +125 -0
  10. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tools/__init__.py +282 -7
  11. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/app.py +41 -10
  12. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/widgets.py +7 -0
  13. {drydock_cli-3.0.171 → drydock_cli-3.1.2/drydock_cli.egg-info}/PKG-INFO +1 -1
  14. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/SOURCES.txt +6 -0
  15. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/pyproject.toml +1 -1
  16. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_degenerate_argument.py +20 -0
  17. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_dispatch.py +26 -0
  18. drydock_cli-3.1.2/tests/test_fiar.py +211 -0
  19. drydock_cli-3.1.2/tests/test_loop_detect.py +194 -0
  20. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_mcp.py +5 -2
  21. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_skills.py +5 -1
  22. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_stig.py +9 -0
  23. drydock_cli-3.1.2/tests/test_suggest.py +73 -0
  24. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_arg_coercion.py +21 -1
  25. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_arg_coercion_more.py +11 -0
  26. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_arg_parsing.py +35 -0
  27. drydock_cli-3.0.171/drydock/suggest.py +0 -30
  28. drydock_cli-3.0.171/tests/test_loop_detect.py +0 -101
  29. drydock_cli-3.0.171/tests/test_suggest.py +0 -30
  30. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/LICENSE +0 -0
  31. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/NOTICE +0 -0
  32. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/README.md +0 -0
  33. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/__init__.py +0 -0
  34. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/__main__.py +0 -0
  35. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/advisor.py +0 -0
  36. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/agent.py +0 -0
  37. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/bash_safety.py +0 -0
  38. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/budget.py +0 -0
  39. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/__init__.py +0 -0
  40. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-data.md +0 -0
  41. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-debug.md +0 -0
  42. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-finetune.md +0 -0
  43. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-metrics.md +0 -0
  44. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-rl.md +0 -0
  45. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-train.md +0 -0
  46. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
  47. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/nist-csf.md +0 -0
  48. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/rmf-categorize.md +0 -0
  49. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/rmf-control.md +0 -0
  50. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/rmf-poam.md +0 -0
  51. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/rmf-review.md +0 -0
  52. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/stig-assess.md +0 -0
  53. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/stig-remediate.md +0 -0
  54. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/cci.py +0 -0
  55. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/cli.py +0 -0
  56. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/compaction.py +0 -0
  57. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/config.py +0 -0
  58. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/detect.py +0 -0
  59. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/events.py +0 -0
  60. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/extract.py +0 -0
  61. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/gittools.py +0 -0
  62. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/graphrag.py +0 -0
  63. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/guards.py +0 -0
  64. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/mcp.py +0 -0
  65. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/phases.py +0 -0
  66. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/poam.py +0 -0
  67. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/progress.py +0 -0
  68. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/recipes.py +0 -0
  69. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/recovery.py +0 -0
  70. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/resume.py +0 -0
  71. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/rmf.py +0 -0
  72. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/rmf_graph.py +0 -0
  73. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/skills.py +0 -0
  74. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/stig.py +0 -0
  75. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/subagents.py +0 -0
  76. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/task_state.py +0 -0
  77. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_policy.py +0 -0
  78. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_registry.py +0 -0
  79. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_result.py +0 -0
  80. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_select.py +0 -0
  81. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_validate.py +0 -0
  82. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/trajectory.py +0 -0
  83. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/__init__.py +0 -0
  84. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/approval.py +0 -0
  85. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/messages.py +0 -0
  86. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tuning.py +0 -0
  87. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/verification.py +0 -0
  88. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/web.py +0 -0
  89. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/dependency_links.txt +0 -0
  90. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/entry_points.txt +0 -0
  91. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/requires.txt +0 -0
  92. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/top_level.txt +0 -0
  93. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/setup.cfg +0 -0
  94. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_advisor.py +0 -0
  95. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_approval.py +0 -0
  96. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_back_command.py +0 -0
  97. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_background.py +0 -0
  98. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_binary_output.py +0 -0
  99. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_crossplatform.py +0 -0
  100. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_output_bounding.py +0 -0
  101. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_process_group.py +0 -0
  102. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_safety.py +0 -0
  103. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_sanitize.py +0 -0
  104. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_shell.py +0 -0
  105. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_stdin.py +0 -0
  106. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_stop_partial.py +0 -0
  107. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_timeout_network.py +0 -0
  108. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_timeout_param.py +0 -0
  109. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_budget.py +0 -0
  110. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_cci.py +0 -0
  111. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_cli_agents.py +0 -0
  112. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_compact_command.py +0 -0
  113. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_compaction.py +0 -0
  114. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_config.py +0 -0
  115. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_config_migration.py +0 -0
  116. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_context_limit_config.py +0 -0
  117. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_context_limit_issue25.py +0 -0
  118. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_criteria_coverage.py +0 -0
  119. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_cycling_detection.py +0 -0
  120. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_detect.py +0 -0
  121. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_e2e_connected.py +0 -0
  122. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_edit_replace_all.py +0 -0
  123. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_effort_governor.py +0 -0
  124. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_empty_response.py +0 -0
  125. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_events.py +0 -0
  126. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_extract.py +0 -0
  127. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_failure_loop.py +0 -0
  128. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_first_run_setup.py +0 -0
  129. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_gittools.py +0 -0
  130. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_glob_edit_edges.py +0 -0
  131. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_graphify_example.py +0 -0
  132. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_graphrag.py +0 -0
  133. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_graphrag_quoted_path.py +0 -0
  134. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_graphrag_sqlite.py +0 -0
  135. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_grep_and_read_robust.py +0 -0
  136. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_guards_and_tools.py +0 -0
  137. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_hallucinated_tools.py +0 -0
  138. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_leaked_tool_call.py +0 -0
  139. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_model_registry.py +0 -0
  140. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_oneshot_unreachable.py +0 -0
  141. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_overthink_interrupt.py +0 -0
  142. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_phases.py +0 -0
  143. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_plan_autocontinue.py +0 -0
  144. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_poam.py +0 -0
  145. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_progress.py +0 -0
  146. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_providers_unreachable.py +0 -0
  147. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_read_index.py +0 -0
  148. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_recipes.py +0 -0
  149. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_recovery.py +0 -0
  150. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_recovery_config.py +0 -0
  151. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_recovery_integration.py +0 -0
  152. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_repeated_outcome.py +0 -0
  153. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_repetition_interrupt.py +0 -0
  154. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_resume_events.py +0 -0
  155. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_resume_restore.py +0 -0
  156. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_resume_snapshot.py +0 -0
  157. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_rmf.py +0 -0
  158. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_rmf_graph.py +0 -0
  159. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_rmf_stig_graph.py +0 -0
  160. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_rolling_plan.py +0 -0
  161. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_runaway_repetition.py +0 -0
  162. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_screenshot.py +0 -0
  163. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_server_probe.py +0 -0
  164. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_sqlite_events.py +0 -0
  165. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_stall_retry.py +0 -0
  166. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_stop.py +0 -0
  167. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_streaming_newlines.py +0 -0
  168. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_subagent.py +0 -0
  169. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_subagents.py +0 -0
  170. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_system_prompt_help.py +0 -0
  171. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_task_state.py +0 -0
  172. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_timeline.py +0 -0
  173. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_todo.py +0 -0
  174. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_canonical.py +0 -0
  175. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_policy.py +0 -0
  176. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_result.py +0 -0
  177. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_select.py +0 -0
  178. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_validate.py +0 -0
  179. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tools_undo.py +0 -0
  180. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_trajectory.py +0 -0
  181. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tui.py +0 -0
  182. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tuning.py +0 -0
  183. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_verification_gate.py +0 -0
  184. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_viewimage.py +0 -0
  185. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_vision_input.py +0 -0
  186. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_web_tools.py +0 -0
  187. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_windows_shell.py +0 -0
  188. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_worker_subagent.py +0 -0
  189. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_write_content_coerce.py +0 -0
  190. {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.171
3
+ Version: 3.1.2
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
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: fiar-assess
3
+ description: Test the next un-tested FIAR control objective against real evidence
4
+ ---
5
+ Test FIAR control objectives in the engagement at: $ARGS
6
+
7
+ Do exactly ONE control per run (loop it — /loop N /fiar-assess <path> — for the
8
+ whole matrix):
9
+
10
+ 1. Call FiarControls with that path and status=not_tested. If none remain, say the
11
+ control matrix is fully tested and STOP without calling other tools.
12
+ 2. Take the FIRST not_tested control. Call FiarControl (path + id) to read its
13
+ control objective, assertions, required Key Supporting Documents (KSDs), and
14
+ test procedure.
15
+ 3. You MUST examine real evidence before deciding — do NOT guess. Follow the test
16
+ procedure: Read the actual KSD the control names (obligating document, receiving
17
+ report, invoice, reconciliation workpaper, SF-50, access-recert listing, …) or
18
+ run its check command (Bash). If the procedure says to sample, state your sample
19
+ size and how many exceptions you found. Reason step by step about whether the
20
+ evidence satisfies the control objective for its assertion(s).
21
+ 4. Call FiarAssess (path + id) with:
22
+ - status = effective ONLY if the evidence clearly shows the control operated;
23
+ deficient if the evidence shows it did not (any un-remediated
24
+ exception makes the control deficient);
25
+ not_applicable if the control does not apply to this entity;
26
+ - evidence = the exact KSDs you examined and what they showed (quote the line);
27
+ - sample_size and exceptions when you tested a sample.
28
+ 5. If you set deficient, immediately call FiarFinding to record the NFR: condition
29
+ (what you observed), criteria (the policy/standard/GAAP requirement), cause
30
+ (root cause), effect (impact on the assertion/balance), recommendation, and a
31
+ severity (deficiency / significant_deficiency / material_weakness).
32
+
33
+ Give a one-line summary: control id, the KSD evidence, and the status you set.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: fiar-cap
3
+ description: Draft a Corrective Action Plan for the open FIAR findings (NFRs)
4
+ ---
5
+ Draft Corrective Action Plans for the open findings in the FIAR engagement at: $ARGS
6
+
7
+ 1. Call FiarControls (path only) to see the engagement, then read the findings by
8
+ listing deficient controls (FiarControls status=deficient) and, for each, its
9
+ recorded NFR. If there are no findings, say so and STOP.
10
+ 2. Take ONE finding at a time (worst severity first: material_weakness, then
11
+ significant_deficiency, then deficiency). For it, draft a CAP that is specific
12
+ and testable:
13
+ - root_cause: the underlying reason the control failed (people / process /
14
+ system / policy) — go past the symptom in the NFR condition;
15
+ - action: the concrete corrective action that will make the control operate;
16
+ - responsible: the organization / POC who owns it;
17
+ - target_date: a realistic milestone date;
18
+ - milestones: 2–4 dated, verifiable steps (e.g. "update SOP", "train preparers",
19
+ "re-test 25 samples with 0 exceptions").
20
+ 3. The CAP must close the specific assertion gap the NFR identified — tie the final
21
+ milestone to a re-test of the same control with zero exceptions. Note whether the
22
+ fix is a process change, a system change, or an ITGC (access / SoD / change
23
+ management) remediation, since those route to different owners.
24
+
25
+ Present each CAP in the standard structure (finding → root cause → action →
26
+ responsible → milestones), and end with which finding to remediate first and why.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: fiar-evidence
3
+ description: Trace the full FIAR evidence chain for a control before passing it
4
+ ---
5
+ Substantively test ONE FIAR control by tracing the complete evidence chain, for the
6
+ engagement + control id in: $ARGS
7
+
8
+ A control cannot support a financial-statement assertion on a broken audit trail.
9
+ You MUST trace all eight links before FiarAssess will let you mark it effective:
10
+
11
+ population → sample → source_transaction → authorization → supporting_document
12
+ → system_posting → gl_effect → assertion
13
+
14
+ 1. Call FiarControl (path + id) to read the objective, assertions and KSDs.
15
+ 2. Establish the POPULATION: the recorded universe (a GL detail, a transaction
16
+ register, an APSR listing). State how you know it is complete.
17
+ 3. SELECT a sample from that population and record exactly which item(s).
18
+ 4. For each sampled item, gather and Read the real evidence for each link:
19
+ - source_transaction: the transaction the item represents;
20
+ - authorization: the warranted/authorized approver (warrant, SF-50, delegation);
21
+ - supporting_document: the KSD (contract, receiving report, invoice, recon wp);
22
+ - system_posting: where it posted in the feeder/accounting system;
23
+ - gl_effect: the resulting general-ledger entry;
24
+ - assertion: the FS assertion it ultimately supports.
25
+ 5. Call FiarAssess with status=effective and a `chain` object whose keys are the
26
+ eight links above, each set to the specific evidence you found. If any link is
27
+ missing, FiarAssess will REFUSE and name the gap — go get that evidence, do not
28
+ force the status. If a link genuinely cannot be traced (e.g. no authorization on
29
+ file), the control is DEFICIENT: set status=deficient and record an NFR with
30
+ FiarFinding.
31
+
32
+ Report the item you traced and, link by link, the evidence that supported it (or the
33
+ link that broke the chain).
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: fiar-readiness
3
+ description: Assess audit-readiness posture of a FIAR engagement and report gaps
4
+ ---
5
+ Produce a Discover-phase audit-readiness assessment for the FIAR engagement at: $ARGS
6
+
7
+ 1. Call FiarControls (path only) to see the whole matrix: cycle, Wave, phase, and
8
+ the status counts. If the engagement JSON does not exist yet, tell the operator
9
+ to create one first: `python -m drydock.fiar new <path> <name> <cycle>` (cycles:
10
+ FBWT, P2P, PPE, INV, CIVPAY, REIM, FR, ITGC, or * for all).
11
+ 2. Call FiarControls with status=deficient and status=not_tested to enumerate the
12
+ gaps. For each DEFICIENT control, note the assertion(s) at risk and whether an
13
+ NFR already exists for it.
14
+ 3. Read the current readiness roll-up (the counts show tested vs effective). State,
15
+ in plain terms:
16
+ - percent of applicable controls tested, and of those, percent effective;
17
+ - the assertions (Existence, Completeness, Valuation, Presentation & Disclosure,
18
+ Rights & Obligations) that are NOT yet supported because a control covering
19
+ them is deficient or untested;
20
+ - open findings by severity (material weakness / significant deficiency /
21
+ deficiency) and whether each has a Corrective Action Plan.
22
+ 4. Recommend the highest-value next actions to move toward assertion, prioritizing
23
+ material weaknesses and untested key controls (FBWT reconciliation, three-way
24
+ match, access/SoD ITGCs). Do NOT claim the entity is audit-ready — readiness is
25
+ asserted by management and validated by an independent examination; your job is
26
+ to report the evidenced posture and the gaps.
27
+
28
+ Report a short readiness summary (3–4 sentences) first, then the detail beneath it.
@@ -0,0 +1,528 @@
1
+ """FIAR — Financial Improvement and Audit Readiness (DoD/War Dept audit readiness).
2
+
3
+ A stdlib-only engine for driving an audit-readiness engagement the way the FIAR
4
+ Guidance describes it: assess control objectives within a business-process cycle
5
+ against Key Supporting Documents (KSDs), record Notices of Findings and
6
+ Recommendations (NFRs), and track Corrective Action Plans (CAPs) through the
7
+ five FIAR methodology phases (Discover -> Correct -> Assert -> Validate ->
8
+ Sustain).
9
+
10
+ An "engagement" is one JSON file (analogous to a STIG .ckl): an assessable unit,
11
+ a control matrix seeded from the FIAR key-control-objective library, its findings,
12
+ and its CAPs. The agent reads and mutates it through the Fiar* tools; a human (or
13
+ eMASS/audit workpaper) can consume the JSON directly.
14
+
15
+ Reference: DoD FIAR Guidance (comptroller.war.gov). All code original to Drydock;
16
+ the control library encodes standard, publicly-documented FIAR control objectives.
17
+ Not a substitute for an IPA opinion — it structures the readiness work, it does
18
+ not render an audit judgement.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from dataclasses import dataclass, field, asdict
24
+ from datetime import date
25
+ from pathlib import Path
26
+
27
+ # ── Controlled vocabularies ───────────────────────────────────────────────
28
+ # The five commonly-accepted financial statement assertions the FIAR Guidance
29
+ # (April 2017, §1.B / §3) tests against — kept distinct, as the guidance lists
30
+ # them (existence and completeness are frequently TESTED together but are
31
+ # separate assertions).
32
+ ASSERTIONS = {
33
+ "E": "Existence",
34
+ "C": "Completeness",
35
+ "V": "Valuation & Accuracy",
36
+ "PD": "Presentation & Disclosure",
37
+ "RO": "Rights & Obligations",
38
+ }
39
+ # The FIAR Strategy prioritizes work into Four Waves toward a full-statement
40
+ # audit (Guidance Figure 1-1 / §2.C, §5).
41
+ WAVES = {
42
+ 1: "Appropriations Received / short-term readiness",
43
+ 2: "Statement of Budgetary Resources (SBR)",
44
+ 3: "Mission-Critical Asset Existence & Completeness",
45
+ 4: "Proprietary (full financial statement) audit",
46
+ }
47
+ # The FIAR Methodology organizes work as numbered KEY TASKS (e.g. 1.3.3 internal
48
+ # control testing, 1.4.5 KSD testing) that culminate in a management ASSERTION,
49
+ # an independent EXAMINATION/audit (IPA or DoD OIG), then SUSTAINMENT under OMB
50
+ # A-123 App. A. These five readiness stages summarize that flow for tracking.
51
+ PHASES = ("discover", "correct", "assert", "validate", "sustain")
52
+ # Assessable-unit business-process cycles the seed library covers.
53
+ CYCLES = {
54
+ "FBWT": "Fund Balance with Treasury",
55
+ "P2P": "Procure-to-Pay",
56
+ "PPE": "Property, Plant & Equipment",
57
+ "INV": "Inventory & Operating Materials",
58
+ "CIVPAY": "Civilian Pay",
59
+ "REIM": "Reimbursables / Order-to-Cash",
60
+ "FR": "Financial Reporting (Budget-to-Report)",
61
+ "ITGC": "IT General Controls",
62
+ }
63
+ # Control assessment outcomes.
64
+ CONTROL_STATUSES = ("not_tested", "effective", "deficient", "not_applicable")
65
+ # NFR severity, worst-first (GAO Green Book / A-123 deficiency taxonomy).
66
+ SEVERITIES = ("material_weakness", "significant_deficiency", "deficiency")
67
+ # CAP lifecycle.
68
+ CAP_STATUSES = ("open", "in_progress", "validated", "closed")
69
+
70
+
71
+ _STATUS_ALIASES = {
72
+ "pass": "effective", "passed": "effective", "compliant": "effective",
73
+ "effective": "effective",
74
+ "fail": "deficient", "failed": "deficient", "noncompliant": "deficient",
75
+ "deficient": "deficient",
76
+ "na": "not_applicable", "n_a": "not_applicable", "not_applicable": "not_applicable",
77
+ "open": "not_tested", "untested": "not_tested", "not_tested": "not_tested",
78
+ }
79
+
80
+
81
+ def _norm_status(s: str | None) -> str:
82
+ return str(s or "").strip().lower().replace(" ", "_").replace("-", "_").replace("/", "_")
83
+
84
+
85
+ def canonical_status(s: str | None) -> str:
86
+ """Lenient: map any input to a status, defaulting to not_tested. Use
87
+ recognized_status() to reject genuinely unknown input at a tool boundary."""
88
+ if not s:
89
+ return "not_tested"
90
+ k = _norm_status(s)
91
+ return _STATUS_ALIASES.get(k, k) if _STATUS_ALIASES.get(k, k) in CONTROL_STATUSES else "not_tested"
92
+
93
+
94
+ def recognized_status(s: str | None) -> bool:
95
+ """True only if s is a known status or alias (not the lenient fallback)."""
96
+ k = _norm_status(s)
97
+ return k in _STATUS_ALIASES or k in CONTROL_STATUSES
98
+
99
+
100
+ def canonical_assertion(a: str) -> str:
101
+ a0 = str(a).strip().upper()
102
+ if a0 in ASSERTIONS:
103
+ return a0
104
+ aliases = {"EXISTENCE": "E", "COMPLETENESS": "C", "ACCURACY": "V",
105
+ "VALUATION": "V", "VALUATION & ACCURACY": "V",
106
+ "PRESENTATION": "PD", "PRESENTATION & DISCLOSURE": "PD",
107
+ "DISCLOSURE": "PD", "RIGHTS": "RO", "RIGHTS & OBLIGATIONS": "RO",
108
+ "OBLIGATIONS": "RO"}
109
+ if a0 in aliases:
110
+ return aliases[a0]
111
+ rev = {v.upper(): k for k, v in ASSERTIONS.items()}
112
+ return rev.get(a0, a0)
113
+
114
+
115
+ # ── Data model ────────────────────────────────────────────────────────────
116
+ @dataclass
117
+ class Control:
118
+ """One control objective within a cycle, mapped to assertions + KSDs."""
119
+ id: str
120
+ cycle: str # CYCLES key
121
+ objective: str # the control objective / risk it addresses
122
+ activity: str = "" # the control activity that should exist
123
+ assertions: list[str] = field(default_factory=list) # ASSERTIONS keys
124
+ fro: str = "" # Financial Reporting Objective it supports
125
+ ksds: list[str] = field(default_factory=list) # Key Supporting Documents
126
+ test_procedure: str = "" # how to test it
127
+ key_task: str = "" # FIAR Methodology key task (e.g. "1.3.3", "1.4.5")
128
+ itgc: bool = False # is this an IT General Control?
129
+ status: str = "not_tested"
130
+ evidence: str = "" # what was observed / KSDs examined
131
+ sample_size: int = 0
132
+ exceptions: int = 0 # test exceptions found in the sample
133
+ evidence_chain: dict = field(default_factory=dict) # EVIDENCE_CHAIN link -> evidence
134
+
135
+ def summary(self) -> str:
136
+ mark = {"effective": "✓", "deficient": "✗",
137
+ "not_applicable": "–", "not_tested": "·"}[self.status]
138
+ asrt = "/".join(self.assertions) or "-"
139
+ return f"{mark} {self.id} [{self.cycle}/{asrt}] {self.objective[:70]}"
140
+
141
+
142
+ @dataclass
143
+ class Finding:
144
+ """A Notice of Findings and Recommendations (NFR) — the audit finding form."""
145
+ id: str
146
+ control_id: str = ""
147
+ condition: str = "" # what is (the deficiency observed)
148
+ criteria: str = "" # what should be (policy/standard/GAAP)
149
+ cause: str = "" # why the gap exists (root cause)
150
+ effect: str = "" # impact on the assertion / balance
151
+ recommendation: str = ""
152
+ severity: str = "deficiency"
153
+ assertions: list[str] = field(default_factory=list)
154
+ status: str = "open"
155
+
156
+ def summary(self) -> str:
157
+ return f"[{self.severity}] {self.id} ({self.control_id or 'entity'}): {self.condition[:70]}"
158
+
159
+
160
+ @dataclass
161
+ class Cap:
162
+ """A Corrective Action Plan addressing one NFR, with milestones."""
163
+ id: str
164
+ finding_id: str
165
+ root_cause: str = ""
166
+ action: str = ""
167
+ responsible: str = "" # responsible organization / POC
168
+ target_date: str = "" # ISO date
169
+ status: str = "open"
170
+ milestones: list[dict] = field(default_factory=list) # [{desc, due, done}]
171
+
172
+ def summary(self) -> str:
173
+ done = sum(1 for m in self.milestones if m.get("done"))
174
+ return (f"[{self.status}] {self.id} -> {self.finding_id}: {self.action[:60]} "
175
+ f"({done}/{len(self.milestones)} milestones, due {self.target_date or 'TBD'})")
176
+
177
+
178
+ class Engagement:
179
+ """One audit-readiness engagement, persisted as a single JSON file."""
180
+
181
+ def __init__(self, data: dict) -> None:
182
+ self._d = data
183
+ self._d.setdefault("controls", [])
184
+ self._d.setdefault("findings", [])
185
+ self._d.setdefault("caps", [])
186
+
187
+ # -- persistence --
188
+ @classmethod
189
+ def load(cls, path: str | Path) -> "Engagement":
190
+ return cls(json.loads(Path(path).read_text("utf-8")))
191
+
192
+ def save(self, path: str | Path) -> None:
193
+ Path(path).write_text(json.dumps(self._d, indent=2), "utf-8")
194
+
195
+ # -- header --
196
+ @property
197
+ def name(self) -> str: return self._d.get("engagement", "")
198
+ @property
199
+ def cycle(self) -> str: return self._d.get("assessable_unit", "")
200
+ @property
201
+ def phase(self) -> str: return self._d.get("phase", "discover")
202
+ @property
203
+ def wave(self) -> int: return int(self._d.get("wave", 4))
204
+
205
+ def set_phase(self, phase: str) -> bool:
206
+ if phase not in PHASES:
207
+ return False
208
+ self._d["phase"] = phase
209
+ return True
210
+
211
+ # -- controls --
212
+ @property
213
+ def controls(self) -> list[Control]:
214
+ return [Control(**c) for c in self._d["controls"]]
215
+
216
+ def control(self, cid: str) -> Control | None:
217
+ for c in self._d["controls"]:
218
+ if c.get("id", "").lower() == cid.lower():
219
+ return Control(**c)
220
+ return None
221
+
222
+ def _raw_control(self, cid: str) -> dict | None:
223
+ for c in self._d["controls"]:
224
+ if c.get("id", "").lower() == cid.lower():
225
+ return c
226
+ return None
227
+
228
+ def assess(self, cid: str, *, status: str | None = None, evidence: str | None = None,
229
+ sample_size: int | None = None, exceptions: int | None = None,
230
+ chain: dict | None = None) -> bool:
231
+ c = self._raw_control(cid)
232
+ if c is None:
233
+ return False
234
+ if status is not None:
235
+ c["status"] = canonical_status(status)
236
+ if evidence is not None:
237
+ c["evidence"] = evidence
238
+ if sample_size is not None:
239
+ c["sample_size"] = int(sample_size)
240
+ if exceptions is not None:
241
+ c["exceptions"] = int(exceptions)
242
+ if chain is not None:
243
+ merged = dict(c.get("evidence_chain") or {})
244
+ merged.update({k: v for k, v in chain.items() if k in EVIDENCE_CHAIN})
245
+ c["evidence_chain"] = merged
246
+ return True
247
+
248
+ def counts(self) -> dict:
249
+ c = {s: 0 for s in CONTROL_STATUSES}
250
+ for ctrl in self._d["controls"]:
251
+ c[canonical_status(ctrl.get("status"))] = c.get(canonical_status(ctrl.get("status")), 0) + 1
252
+ return c
253
+
254
+ # -- findings (NFRs) --
255
+ @property
256
+ def findings(self) -> list[Finding]:
257
+ return [Finding(**f) for f in self._d["findings"]]
258
+
259
+ def add_finding(self, **kw) -> Finding:
260
+ fid = kw.get("id") or f"NFR-{len(self._d['findings']) + 1:03d}"
261
+ kw["id"] = fid
262
+ kw["assertions"] = [canonical_assertion(a) for a in (kw.get("assertions") or [])]
263
+ if kw.get("severity") not in SEVERITIES:
264
+ kw["severity"] = "deficiency"
265
+ f = Finding(**{k: v for k, v in kw.items() if k in Finding.__dataclass_fields__})
266
+ self._d["findings"].append(asdict(f))
267
+ return f
268
+
269
+ # -- CAPs --
270
+ @property
271
+ def caps(self) -> list[Cap]:
272
+ return [Cap(**c) for c in self._d["caps"]]
273
+
274
+ def add_cap(self, **kw) -> Cap:
275
+ cid = kw.get("id") or f"CAP-{len(self._d['caps']) + 1:03d}"
276
+ kw["id"] = cid
277
+ c = Cap(**{k: v for k, v in kw.items() if k in Cap.__dataclass_fields__})
278
+ self._d["caps"].append(asdict(c))
279
+ return c
280
+
281
+ # -- readiness roll-up --
282
+ def readiness(self) -> dict:
283
+ cts = self.counts()
284
+ tested = cts["effective"] + cts["deficient"]
285
+ total_applicable = tested + cts["not_tested"]
286
+ pct_tested = round(100 * tested / total_applicable, 1) if total_applicable else 0.0
287
+ pct_effective = round(100 * cts["effective"] / tested, 1) if tested else 0.0
288
+ open_mw = sum(1 for f in self._d["findings"]
289
+ if f.get("severity") == "material_weakness" and f.get("status") != "closed")
290
+ open_caps = sum(1 for c in self._d["caps"] if c.get("status") != "closed")
291
+ return {
292
+ "phase": self.phase, "counts": cts,
293
+ "pct_tested": pct_tested, "pct_effective": pct_effective,
294
+ "open_findings": sum(1 for f in self._d["findings"] if f.get("status") != "closed"),
295
+ "open_material_weaknesses": open_mw, "open_caps": open_caps,
296
+ }
297
+
298
+
299
+ # ── Evidence chain (the FIAR audit-trail every effective test must trace) ─
300
+ # A control test cannot support an assertion unless the evidence traces the full
301
+ # audit chain from the recorded population all the way to the FS assertion. This
302
+ # is the deterministic backbone that separates a real FIAR review from a checklist.
303
+ EVIDENCE_CHAIN = (
304
+ "population", # the recorded universe the sample was drawn from
305
+ "sample", # the specific item(s) selected
306
+ "source_transaction", # the transaction the item represents
307
+ "authorization", # who was authorized to commit/approve it
308
+ "supporting_document", # the KSD evidencing it
309
+ "system_posting", # where it posted in the feeder/accounting system
310
+ "gl_effect", # its effect in the general ledger
311
+ "assertion", # the FS assertion it ultimately supports
312
+ )
313
+
314
+
315
+ def validate_chain(chain: dict | None) -> dict:
316
+ """Given a chain dict {link: evidence}, report which links are present and
317
+ whether the trace is complete. A link counts as present only if it has a
318
+ non-empty value. Returns {complete, present, missing}."""
319
+ chain = chain or {}
320
+ present = [k for k in EVIDENCE_CHAIN if str(chain.get(k, "")).strip()]
321
+ missing = [k for k in EVIDENCE_CHAIN if k not in present]
322
+ return {"complete": not missing, "present": present, "missing": missing}
323
+
324
+
325
+ # ── Reconciliation (a core FIAR substantive procedure, e.g. FBWT tie-out) ─
326
+ def reconcile(entity: float, source: float, *, tolerance: float = 0.0) -> dict:
327
+ """Tie an entity balance to an authoritative source (e.g. Fund Balance with
328
+ Treasury vs Treasury's balance). Returns the difference and whether it clears
329
+ the tolerance — the shape of every FIAR reconciliation control."""
330
+ diff = round(float(entity) - float(source), 2)
331
+ return {
332
+ "entity": round(float(entity), 2), "source": round(float(source), 2),
333
+ "difference": diff, "abs_difference": abs(diff),
334
+ "tolerance": float(tolerance), "reconciled": abs(diff) <= float(tolerance),
335
+ }
336
+
337
+
338
+ # ── Seed control library — standard FIAR key control objectives ───────────
339
+ # A representative, extensible set of key control objectives per cycle, each
340
+ # mapped to the assertion(s) it supports and the KSDs that evidence it.
341
+ def _expand_assertions(codes: list[str]) -> list[str]:
342
+ """Normalize assertion codes to the five keys, expanding the E&C pairing."""
343
+ out: list[str] = []
344
+ for x in codes:
345
+ xu = str(x).strip().upper()
346
+ if xu == "EC":
347
+ out += ["E", "C"]
348
+ elif xu == "VA":
349
+ out.append("V")
350
+ else:
351
+ out.append(canonical_assertion(xu))
352
+ seen: set[str] = set()
353
+ return [a for a in out if not (a in seen or seen.add(a))]
354
+
355
+
356
+ # Most seed controls are internal-control tests (key task 1.3.3); KSD-existence
357
+ # and reconciliation tests are called out individually.
358
+ def _c(cid, cycle, objective, assertions, activity, ksds, test,
359
+ itgc=False, fro="", key_task="1.3.3") -> dict:
360
+ return asdict(Control(id=cid, cycle=cycle, objective=objective, activity=activity,
361
+ assertions=_expand_assertions(assertions), fro=fro, ksds=ksds,
362
+ test_procedure=test, key_task=key_task, itgc=itgc))
363
+
364
+ SEED_CONTROLS: dict[str, list[dict]] = {
365
+ "FBWT": [
366
+ _c("FBWT-01", "FBWT", "Monthly reconciliation of Fund Balance with Treasury to the Treasury balance is performed and reviewed.",
367
+ ["EC", "VA"], "Preparer reconciles GL FBWT to TAS/BETC in GTAS/CARS; independent reviewer signs off.",
368
+ ["Reconciliation workpaper", "GTAS/CARS statement", "GL trial balance"],
369
+ "Select a month; recompute the tie-out; confirm differences are researched and cleared and the review signature is present and timely."),
370
+ _c("FBWT-02", "FBWT", "Undistributed and in-transit disbursements/collections are researched and resolved timely.",
371
+ ["EC", "VA"], "Aging of unmatched transactions; resolution within policy threshold.",
372
+ ["UDO/in-transit aging report", "Resolution documentation"],
373
+ "Sample aged items; verify each was researched and cleared within the policy window."),
374
+ ],
375
+ "P2P": [
376
+ _c("P2P-01", "P2P", "Obligations are recorded only for valid, authorized commitments supported by an obligating document.",
377
+ ["EC", "RO"], "3-way check: obligating document exists, is signed by a warranted official, and matches the recorded obligation.",
378
+ ["Obligating document (contract/PO/MIPR)", "Warrant/appointment", "GL obligation entry"],
379
+ "Sample recorded obligations; trace each to a signed obligating document by a warranted official for the recorded amount and period of availability."),
380
+ _c("P2P-02", "P2P", "Disbursements are made only for goods/services received and correctly matched (three-way match).",
381
+ ["EC", "VA"], "Invoice matched to receiving report and obligating document before payment.",
382
+ ["Invoice", "Receiving report / acceptance", "Obligating document", "Payment voucher"],
383
+ "Sample disbursements; verify a valid three-way match and that the paid amount agrees to the matched documents."),
384
+ _c("P2P-03", "P2P", "Purchases obligate against an available, correct appropriation (period, purpose, amount).",
385
+ ["RO", "PD"], "Appropriation/line of accounting validated at commitment; ADA edits enforced.",
386
+ ["Line of accounting", "Funds availability check"],
387
+ "Sample obligations; confirm the appropriation charged is correct as to purpose, time, and amount (Antideficiency edits present)."),
388
+ ],
389
+ "PPE": [
390
+ _c("PPE-01", "PPE", "General PP&E recorded in the property system exists and is in the entity's possession/control.",
391
+ ["EC", "RO"], "Periodic physical inventory reconciled to the accountable property system of record (APSR).",
392
+ ["APSR listing", "Physical inventory count sheets", "Acquisition document"],
393
+ "Select recorded assets; physically observe existence; select floor assets; trace to the APSR (two-directional existence & completeness)."),
394
+ _c("PPE-02", "PPE", "PP&E is valued at capitalized acquisition cost and depreciated per policy.",
395
+ ["VA"], "Capitalization threshold applied; depreciation method/life per policy; recomputation.",
396
+ ["Acquisition/CLIN cost documentation", "Depreciation schedule"],
397
+ "Sample assets; recompute capitalized cost from KSDs and recompute depreciation; compare to the GL."),
398
+ ],
399
+ "INV": [
400
+ _c("INV-01", "INV", "Inventory and operating materials & supplies recorded in the GL exist and are complete.",
401
+ ["EC"], "Cyclic counts reconciled to the inventory system and GL.",
402
+ ["Count sheets", "Inventory system extract", "GL balance"],
403
+ "Two-directional test: recorded-to-floor and floor-to-recorded; confirm count differences are adjusted."),
404
+ ],
405
+ "CIVPAY": [
406
+ _c("CIVPAY-01", "CIVPAY", "Civilian pay is disbursed only to bona fide employees at authorized rates.",
407
+ ["EC", "VA"], "Payroll reconciled to time & attendance and to authorized personnel actions (SF-50).",
408
+ ["SF-50 personnel action", "Certified T&A", "Payroll register"],
409
+ "Sample pay transactions; trace to a valid SF-50 and certified T&A; recompute gross pay at the authorized rate."),
410
+ ],
411
+ "REIM": [
412
+ _c("REIM-01", "REIM", "Reimbursable orders are supported by a valid agreement and revenue is recognized as earned.",
413
+ ["EC", "RO", "VA"], "Order acceptance documented; earnings recognized on performance.",
414
+ ["Reimbursable agreement (e.g. 7600A/B)", "Performance/delivery evidence"],
415
+ "Sample reimbursable revenue; trace to an accepted order and to evidence of performance for the recognized amount."),
416
+ ],
417
+ "FR": [
418
+ _c("FR-01", "FR", "Journal vouchers (especially manual/top-side) are supported, reviewed, and approved.",
419
+ ["VA", "PD"], "JV supported by documentation; prepared/reviewed by different people (SoD).",
420
+ ["Journal voucher", "Supporting analysis", "Approval evidence"],
421
+ "Sample JVs (bias to manual/top-side); verify support, an independent review, and that preparer≠approver."),
422
+ _c("FR-02", "FR", "The trial balance ties to the financial statements and to GTAS with no unsupported abnormal balances.",
423
+ ["EC", "VA", "PD"], "TB-to-FS crosswalk; GTAS edit/validation checks pass.",
424
+ ["Crosswalk workpaper", "GTAS edit report"],
425
+ "Recompute the TB-to-FS tie-out; confirm GTAS validation/edit checks are clean or explained."),
426
+ ],
427
+ "ITGC": [
428
+ _c("ITGC-01", "ITGC", "Access to financial systems is authorized, least-privilege, and periodically recertified.",
429
+ ["EC", "VA"], "Provisioning approved; quarterly access recertification; timely deprovisioning.",
430
+ ["Access request/approval", "Recertification listing", "Separation records"],
431
+ "Sample users; verify approved provisioning, recertification, and that separated users were disabled timely.", itgc=True),
432
+ _c("ITGC-02", "ITGC", "Segregation of duties prevents one user from both initiating and approving transactions.",
433
+ ["EC", "RO"], "SoD ruleset enforced in the system; conflicts identified and mitigated.",
434
+ ["SoD matrix", "Role assignments", "Conflict/mitigation log"],
435
+ "Obtain the role-to-user mapping; test for incompatible-duty combinations against the SoD matrix.", itgc=True),
436
+ _c("ITGC-03", "ITGC", "Changes to financial systems are authorized, tested, and approved before production.",
437
+ ["VA"], "Change tickets approved; test evidence retained; migration by a separate role.",
438
+ ["Change ticket", "Test evidence", "Approval/migration record"],
439
+ "Sample production changes; verify authorization, testing, approval, and migration SoD.", itgc=True),
440
+ ],
441
+ }
442
+
443
+
444
+ # Which FIAR Wave each cycle most naturally falls under.
445
+ CYCLE_WAVE = {"FR": 2, "REIM": 2, "P2P": 2, "CIVPAY": 2, # budgetary / SBR
446
+ "PPE": 3, "INV": 3, # asset existence & completeness
447
+ "FBWT": 4, "ITGC": 4} # proprietary / cross-cutting
448
+
449
+
450
+ def new_engagement(name: str, cycle: str, wave: int | None = None) -> Engagement:
451
+ """Create an engagement seeded with the key control objectives for a cycle
452
+ (or ALL cycles when cycle is '*' / 'all')."""
453
+ cyc = cycle.strip().upper()
454
+ if cyc in ("*", "ALL"):
455
+ controls = [dict(c) for lst in SEED_CONTROLS.values() for c in lst]
456
+ au = "All cycles"
457
+ w = wave or 4
458
+ else:
459
+ controls = [dict(c) for c in SEED_CONTROLS.get(cyc, [])]
460
+ au = CYCLES.get(cyc, cycle)
461
+ w = wave or CYCLE_WAVE.get(cyc, 4)
462
+ return Engagement({
463
+ "engagement": name, "assessable_unit": au, "cycle": cyc, "wave": w,
464
+ "phase": "discover", "created": date.today().isoformat(),
465
+ "controls": controls, "findings": [], "caps": [],
466
+ })
467
+
468
+
469
+ # ── CLI ───────────────────────────────────────────────────────────────────
470
+ def _main(argv: list[str]) -> int:
471
+ import argparse
472
+ p = argparse.ArgumentParser(prog="drydock.fiar", description="FIAR audit-readiness engine")
473
+ sub = p.add_subparsers(dest="cmd", required=True)
474
+ q = sub.add_parser("new"); q.add_argument("path"); q.add_argument("name"); q.add_argument("cycle")
475
+ q.add_argument("--wave", type=int, choices=[1, 2, 3, 4])
476
+ q = sub.add_parser("controls"); q.add_argument("path"); q.add_argument("--status")
477
+ q = sub.add_parser("control"); q.add_argument("path"); q.add_argument("id")
478
+ q = sub.add_parser("assess"); q.add_argument("path"); q.add_argument("id")
479
+ q.add_argument("--status"); q.add_argument("--evidence", default="")
480
+ q.add_argument("--sample", type=int); q.add_argument("--exceptions", type=int)
481
+ q = sub.add_parser("summary"); q.add_argument("path")
482
+ q = sub.add_parser("reconcile"); q.add_argument("entity", type=float)
483
+ q.add_argument("source", type=float); q.add_argument("--tolerance", type=float, default=0.0)
484
+ q = sub.add_parser("cycles")
485
+ a = p.parse_args(argv)
486
+
487
+ if a.cmd == "cycles":
488
+ for k, v in CYCLES.items():
489
+ print(f"{k:8} {v} ({len(SEED_CONTROLS.get(k, []))} controls)")
490
+ return 0
491
+ if a.cmd == "reconcile":
492
+ r = reconcile(a.entity, a.source, tolerance=a.tolerance)
493
+ print(json.dumps(r, indent=2)); return 0 if r["reconciled"] else 1
494
+ if a.cmd == "new":
495
+ eng = new_engagement(a.name, a.cycle, wave=a.wave); eng.save(a.path)
496
+ print(f"created {a.path}: {eng.name} / {eng.cycle} (Wave {eng.wave}) "
497
+ f"— {len(eng.controls)} controls"); return 0
498
+
499
+ eng = Engagement.load(a.path)
500
+ if a.cmd == "controls":
501
+ sf = canonical_status(a.status) if a.status else None
502
+ rows = [c for c in eng.controls if sf is None or c.status == sf]
503
+ print(f"{eng.name} [{eng.cycle}] Wave {eng.wave} phase={eng.phase} — " +
504
+ ", ".join(f"{k}={v}" for k, v in eng.counts().items()))
505
+ for c in rows:
506
+ print(" " + c.summary())
507
+ elif a.cmd == "control":
508
+ c = eng.control(a.id)
509
+ if not c:
510
+ print(f"no control {a.id}"); return 1
511
+ print(f"{c.id} [{eng.cycle}] — {c.objective}\nAssertions: {', '.join(c.assertions)}\n"
512
+ f"Activity: {c.activity}\nKSDs: {', '.join(c.ksds)}\nTest: {c.test_procedure}\n"
513
+ f"Status: {c.status} sample={c.sample_size} exceptions={c.exceptions}\n"
514
+ f"Evidence: {c.evidence or '(none)'}")
515
+ elif a.cmd == "assess":
516
+ ok = eng.assess(a.id, status=a.status, evidence=a.evidence or None,
517
+ sample_size=a.sample, exceptions=a.exceptions)
518
+ if not ok:
519
+ print(f"no control {a.id}"); return 1
520
+ eng.save(a.path); print(f"✓ {a.id} -> {canonical_status(a.status)}")
521
+ elif a.cmd == "summary":
522
+ print(json.dumps(eng.readiness(), indent=2))
523
+ return 0
524
+
525
+
526
+ if __name__ == "__main__":
527
+ import sys
528
+ raise SystemExit(_main(sys.argv[1:]))