model-wtf 1.0.0rc1__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 (281) hide show
  1. model_wtf-1.0.0rc1/PKG-INFO +759 -0
  2. model_wtf-1.0.0rc1/README.md +726 -0
  3. model_wtf-1.0.0rc1/pyproject.toml +174 -0
  4. model_wtf-1.0.0rc1/pyproject.toml.orig +149 -0
  5. model_wtf-1.0.0rc1/src/model_wtf/__init__.py +0 -0
  6. model_wtf-1.0.0rc1/src/model_wtf/__main__.py +10 -0
  7. model_wtf-1.0.0rc1/src/model_wtf/agents/challenger.md +62 -0
  8. model_wtf-1.0.0rc1/src/model_wtf/agents/dispatcher.md +19 -0
  9. model_wtf-1.0.0rc1/src/model_wtf/agents/grouper.md +68 -0
  10. model_wtf-1.0.0rc1/src/model_wtf/agents/reviewer.md +93 -0
  11. model_wtf-1.0.0rc1/src/model_wtf/agents/threat_dispatcher.md +15 -0
  12. model_wtf-1.0.0rc1/src/model_wtf/agents/threat_reviewer.md +24 -0
  13. model_wtf-1.0.0rc1/src/model_wtf/agents/topic_dispatcher.md +21 -0
  14. model_wtf-1.0.0rc1/src/model_wtf/agents/topic_reviewer.md +29 -0
  15. model_wtf-1.0.0rc1/src/model_wtf/agents/tp_dispatcher.md +19 -0
  16. model_wtf-1.0.0rc1/src/model_wtf/agents/tp_reviewer.md +199 -0
  17. model_wtf-1.0.0rc1/src/model_wtf/cli.py +29 -0
  18. model_wtf-1.0.0rc1/src/model_wtf/compliance/__init__.py +7 -0
  19. model_wtf-1.0.0rc1/src/model_wtf/compliance/activities.py +449 -0
  20. model_wtf-1.0.0rc1/src/model_wtf/compliance/auto_review.py +1179 -0
  21. model_wtf-1.0.0rc1/src/model_wtf/compliance/check.py +505 -0
  22. model_wtf-1.0.0rc1/src/model_wtf/compliance/cli.py +486 -0
  23. model_wtf-1.0.0rc1/src/model_wtf/compliance/codeowners.py +186 -0
  24. model_wtf-1.0.0rc1/src/model_wtf/compliance/data.py +937 -0
  25. model_wtf-1.0.0rc1/src/model_wtf/compliance/data_cli.py +646 -0
  26. model_wtf-1.0.0rc1/src/model_wtf/compliance/declarations.py +208 -0
  27. model_wtf-1.0.0rc1/src/model_wtf/compliance/discovery.py +260 -0
  28. model_wtf-1.0.0rc1/src/model_wtf/compliance/exit_codes.py +29 -0
  29. model_wtf-1.0.0rc1/src/model_wtf/compliance/findings.py +135 -0
  30. model_wtf-1.0.0rc1/src/model_wtf/compliance/flows.py +475 -0
  31. model_wtf-1.0.0rc1/src/model_wtf/compliance/flows_cli.py +180 -0
  32. model_wtf-1.0.0rc1/src/model_wtf/compliance/gate.py +522 -0
  33. model_wtf-1.0.0rc1/src/model_wtf/compliance/init_cmd.py +497 -0
  34. model_wtf-1.0.0rc1/src/model_wtf/compliance/knowledge.py +503 -0
  35. model_wtf-1.0.0rc1/src/model_wtf/compliance/mcp_server.py +2327 -0
  36. model_wtf-1.0.0rc1/src/model_wtf/compliance/ops.py +376 -0
  37. model_wtf-1.0.0rc1/src/model_wtf/compliance/options.py +36 -0
  38. model_wtf-1.0.0rc1/src/model_wtf/compliance/render.py +467 -0
  39. model_wtf-1.0.0rc1/src/model_wtf/compliance/report.py +402 -0
  40. model_wtf-1.0.0rc1/src/model_wtf/compliance/review.py +335 -0
  41. model_wtf-1.0.0rc1/src/model_wtf/compliance/rights.py +1009 -0
  42. model_wtf-1.0.0rc1/src/model_wtf/compliance/schemas.py +131 -0
  43. model_wtf-1.0.0rc1/src/model_wtf/compliance/severity.py +369 -0
  44. model_wtf-1.0.0rc1/src/model_wtf/compliance/stamps.py +284 -0
  45. model_wtf-1.0.0rc1/src/model_wtf/compliance/stores.py +306 -0
  46. model_wtf-1.0.0rc1/src/model_wtf/compliance/stores_cli.py +190 -0
  47. model_wtf-1.0.0rc1/src/model_wtf/compliance/threats.py +1204 -0
  48. model_wtf-1.0.0rc1/src/model_wtf/compliance/threats_cli.py +833 -0
  49. model_wtf-1.0.0rc1/src/model_wtf/compliance/threats_gen.py +158 -0
  50. model_wtf-1.0.0rc1/src/model_wtf/compliance/touchpoints.py +1179 -0
  51. model_wtf-1.0.0rc1/src/model_wtf/compliance/touchpoints_cli.py +1067 -0
  52. model_wtf-1.0.0rc1/src/model_wtf/compliance/workspace.py +192 -0
  53. model_wtf-1.0.0rc1/src/model_wtf/compliance/yaml_io.py +224 -0
  54. model_wtf-1.0.0rc1/src/model_wtf/introspect/__init__.py +0 -0
  55. model_wtf-1.0.0rc1/src/model_wtf/introspect/cache.py +151 -0
  56. model_wtf-1.0.0rc1/src/model_wtf/introspect/django_models.py +357 -0
  57. model_wtf-1.0.0rc1/src/model_wtf/introspect/django_touchpoints.py +814 -0
  58. model_wtf-1.0.0rc1/src/model_wtf/introspect/runner.py +385 -0
  59. model_wtf-1.0.0rc1/src/model_wtf/introspect/sveltekit_touchpoints.mjs +282 -0
  60. model_wtf-1.0.0rc1/src/model_wtf/knowledge/adequacy.yaml +50 -0
  61. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/behavioural.yaml +5 -0
  62. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/biometric.yaml +5 -0
  63. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/connection.yaml +5 -0
  64. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/contact.yaml +5 -0
  65. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/content.yaml +5 -0
  66. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/credentials.yaml +5 -0
  67. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/criminal.yaml +5 -0
  68. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/financial.yaml +5 -0
  69. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/health.yaml +5 -0
  70. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/identity.yaml +5 -0
  71. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/location.yaml +5 -0
  72. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/professional.yaml +5 -0
  73. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/special_other.yaml +5 -0
  74. model_wtf-1.0.0rc1/src/model_wtf/knowledge/categories/technical.yaml +5 -0
  75. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/address.yaml +7 -0
  76. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/behavioural.yaml +7 -0
  77. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/birth_date.yaml +7 -0
  78. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/connection.yaml +8 -0
  79. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/credential.yaml +7 -0
  80. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/email.yaml +8 -0
  81. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/fallback.yaml +7 -0
  82. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/file.yaml +7 -0
  83. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/file_path.yaml +8 -0
  84. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/financial.yaml +7 -0
  85. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/health.yaml +7 -0
  86. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/identifier.yaml +7 -0
  87. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/json.yaml +8 -0
  88. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/location.yaml +8 -0
  89. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/name.yaml +7 -0
  90. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/phone.yaml +8 -0
  91. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/primary_key.yaml +8 -0
  92. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/relation.yaml +8 -0
  93. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/special.yaml +7 -0
  94. model_wtf-1.0.0rc1/src/model_wtf/knowledge/data_rules/technical.yaml +8 -0
  95. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/admin.LogEntry.yaml +21 -0
  96. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.Group.yaml +2 -0
  97. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.Group_permissions.yaml +2 -0
  98. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.Permission.yaml +2 -0
  99. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.User.yaml +14 -0
  100. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.User_groups.yaml +2 -0
  101. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/auth.User_user_permissions.yaml +2 -0
  102. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/contenttypes.ContentType.yaml +2 -0
  103. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/procrastinate.ProcrastinateEvent.yaml +2 -0
  104. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/procrastinate.ProcrastinateJob.yaml +12 -0
  105. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/procrastinate.ProcrastinatePeriodicDefer.yaml +2 -0
  106. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/procrastinate.ProcrastinateWorker.yaml +2 -0
  107. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/sessions.Session.yaml +19 -0
  108. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/taggit.Tag.yaml +2 -0
  109. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/taggit.TaggedItem.yaml +2 -0
  110. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailadmin.Admin.yaml +2 -0
  111. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailadmin.EditingSession.yaml +11 -0
  112. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailadmin.FormState.yaml +18 -0
  113. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Collection.yaml +2 -0
  114. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.CollectionViewRestriction.yaml +4 -0
  115. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.CollectionViewRestriction_groups.yaml +2 -0
  116. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Comment.yaml +15 -0
  117. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.CommentReply.yaml +15 -0
  118. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupApprovalTask.yaml +2 -0
  119. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupApprovalTask_groups.yaml +2 -0
  120. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupCollectionPermission.yaml +2 -0
  121. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupPagePermission.yaml +2 -0
  122. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.GroupSitePermission.yaml +2 -0
  123. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Locale.yaml +2 -0
  124. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.ModelLogEntry.yaml +19 -0
  125. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Page.yaml +27 -0
  126. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.PageLogEntry.yaml +19 -0
  127. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.PageSubscription.yaml +11 -0
  128. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.PageViewRestriction.yaml +4 -0
  129. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.PageViewRestriction_groups.yaml +2 -0
  130. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.ReferenceIndex.yaml +2 -0
  131. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Revision.yaml +22 -0
  132. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Site.yaml +2 -0
  133. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Task.yaml +2 -0
  134. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.TaskState.yaml +12 -0
  135. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.UploadedFile.yaml +24 -0
  136. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.Workflow.yaml +2 -0
  137. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.WorkflowContentType.yaml +2 -0
  138. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.WorkflowPage.yaml +2 -0
  139. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.WorkflowState.yaml +12 -0
  140. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailcore.WorkflowTask.yaml +2 -0
  141. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtaildocs.Document.yaml +21 -0
  142. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailembeds.Embed.yaml +2 -0
  143. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailforms.FormSubmission.yaml +13 -0
  144. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailimages.Image.yaml +21 -0
  145. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailimages.Rendition.yaml +6 -0
  146. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailredirects.Redirect.yaml +2 -0
  147. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailsearch.IndexEntry.yaml +17 -0
  148. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailsearch.Query.yaml +4 -0
  149. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wagtailusers.UserProfile.yaml +20 -0
  150. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wailer.Email.yaml +17 -0
  151. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/wailer.Sms.yaml +17 -0
  152. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.CheckIn.yaml +2 -0
  153. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.Control.yaml +2 -0
  154. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.Control_depends_on.yaml +2 -0
  155. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.Cron.yaml +2 -0
  156. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.Incident.yaml +6 -0
  157. model_wtf-1.0.0rc1/src/model_wtf/knowledge/library/whealth.RunRecord.yaml +8 -0
  158. model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/confidential.yaml +5 -0
  159. model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/internal.yaml +5 -0
  160. model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/personal.yaml +5 -0
  161. model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/public.yaml +5 -0
  162. model_wtf-1.0.0rc1/src/model_wtf/knowledge/sensitivity/special.yaml +5 -0
  163. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AA01.yaml +33 -0
  164. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AA02.yaml +33 -0
  165. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AA03.yaml +61 -0
  166. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AA04.yaml +53 -0
  167. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC01.yaml +39 -0
  168. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC02.yaml +30 -0
  169. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC03.yaml +48 -0
  170. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC04.yaml +58 -0
  171. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC05.yaml +38 -0
  172. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC06.yaml +33 -0
  173. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC07.yaml +38 -0
  174. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC08.yaml +35 -0
  175. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC09.yaml +30 -0
  176. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC10.yaml +29 -0
  177. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC11.yaml +40 -0
  178. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC12.yaml +25 -0
  179. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC13.yaml +32 -0
  180. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC14.yaml +37 -0
  181. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC15.yaml +38 -0
  182. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC16.yaml +31 -0
  183. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC17.yaml +27 -0
  184. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC18.yaml +28 -0
  185. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC19.yaml +31 -0
  186. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC20.yaml +32 -0
  187. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC21.yaml +49 -0
  188. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC22.yaml +23 -0
  189. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC23.yaml +22 -0
  190. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/AC24.yaml +24 -0
  191. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/API01.yaml +30 -0
  192. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/API02.yaml +34 -0
  193. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR01.yaml +47 -0
  194. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR02.yaml +58 -0
  195. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR03.yaml +38 -0
  196. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR04.yaml +39 -0
  197. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR05.yaml +31 -0
  198. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR06.yaml +29 -0
  199. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR07.yaml +58 -0
  200. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/CR08.yaml +33 -0
  201. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DE01.yaml +30 -0
  202. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DE02.yaml +47 -0
  203. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DE03.yaml +28 -0
  204. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DE04.yaml +34 -0
  205. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO01.yaml +33 -0
  206. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO02.yaml +40 -0
  207. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO03.yaml +33 -0
  208. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO04.yaml +42 -0
  209. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DO05.yaml +33 -0
  210. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DR01.yaml +29 -0
  211. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS01.yaml +40 -0
  212. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS02.yaml +27 -0
  213. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS03.yaml +62 -0
  214. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS04.yaml +34 -0
  215. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS05.yaml +30 -0
  216. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/DS06.yaml +28 -0
  217. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/HA01.yaml +48 -0
  218. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/HA02.yaml +24 -0
  219. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/HA03.yaml +38 -0
  220. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/HA04.yaml +32 -0
  221. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP01.yaml +34 -0
  222. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP02.yaml +37 -0
  223. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP03.yaml +36 -0
  224. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP04.yaml +38 -0
  225. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP05.yaml +34 -0
  226. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP06.yaml +42 -0
  227. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP07.yaml +34 -0
  228. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP08.yaml +35 -0
  229. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP09.yaml +34 -0
  230. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP10.yaml +34 -0
  231. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP11.yaml +39 -0
  232. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP12.yaml +36 -0
  233. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP13.yaml +47 -0
  234. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP14.yaml +35 -0
  235. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP15.yaml +44 -0
  236. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP16.yaml +34 -0
  237. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP17.yaml +38 -0
  238. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP18.yaml +46 -0
  239. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP19.yaml +26 -0
  240. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP20.yaml +43 -0
  241. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP21.yaml +26 -0
  242. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP22.yaml +28 -0
  243. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP23.yaml +47 -0
  244. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP24.yaml +46 -0
  245. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP25.yaml +25 -0
  246. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP26.yaml +26 -0
  247. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP27.yaml +28 -0
  248. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP28.yaml +44 -0
  249. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP29.yaml +34 -0
  250. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP30.yaml +29 -0
  251. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP31.yaml +35 -0
  252. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP32.yaml +33 -0
  253. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP33.yaml +25 -0
  254. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP34.yaml +29 -0
  255. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP35.yaml +32 -0
  256. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP36.yaml +37 -0
  257. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP37.yaml +43 -0
  258. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP38.yaml +49 -0
  259. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP39.yaml +53 -0
  260. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP40.yaml +45 -0
  261. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/INP41.yaml +36 -0
  262. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LB01.yaml +36 -0
  263. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM01.yaml +24 -0
  264. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM02.yaml +25 -0
  265. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM03.yaml +26 -0
  266. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM04.yaml +24 -0
  267. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM05.yaml +26 -0
  268. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM06.yaml +25 -0
  269. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM07.yaml +23 -0
  270. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM08.yaml +25 -0
  271. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/LLM09.yaml +25 -0
  272. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC01.yaml +68 -0
  273. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC02.yaml +35 -0
  274. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC03.yaml +68 -0
  275. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC04.yaml +54 -0
  276. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/SC05.yaml +46 -0
  277. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/_actors.yaml +30 -0
  278. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/_mapping.yaml +176 -0
  279. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/_rules.yaml +218 -0
  280. model_wtf-1.0.0rc1/src/model_wtf/knowledge/threats/_topics.yaml +99 -0
  281. model_wtf-1.0.0rc1/src/model_wtf/opencode.py +605 -0
@@ -0,0 +1,759 @@
1
+ Metadata-Version: 2.3
2
+ Name: model-wtf
3
+ Version: 1.0.0rc1
4
+ Summary: Compliance as code for Model W projects: data inventory, processing register, threat model, and a gate on every pull request
5
+ Keywords: gdpr,compliance,threat-model,django,sveltekit,security
6
+ Author: Rémy Sanchez
7
+ Author-email: Rémy Sanchez <remy.sanchez@hyperthese.net>
8
+ License: MIT
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Legal Industry
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Requires-Dist: rich
20
+ Requires-Dist: rich-click
21
+ Requires-Dist: click
22
+ Requires-Dist: httpx
23
+ Requires-Dist: pyyaml
24
+ Requires-Dist: pydantic>=2.13.5
25
+ Requires-Dist: ruamel-yaml>=0.19.1
26
+ Requires-Dist: mcp>=1.2
27
+ Requires-Python: >=3.12
28
+ Project-URL: Documentation, https://modelw.github.io/wtf/
29
+ Project-URL: Source, https://github.com/ModelW/wtf
30
+ Project-URL: Changelog, https://github.com/ModelW/wtf/blob/develop/CHANGELOG.md
31
+ Project-URL: Issues, https://github.com/ModelW/wtf/issues
32
+ Description-Content-Type: text/markdown
33
+
34
+ # `model-wtf`
35
+
36
+ The Model W Transformation Facilitator is a CLI tool that facilitates Model W
37
+ compliance of a given Git repo.
38
+
39
+ It maintains a compliance-oriented model of the application — its data,
40
+ components and flows — declared in YAML under `compliance/` folders next to
41
+ the code. That model feeds static analysis and code review, and derived
42
+ documents such as the GDPR Art. 30 registry or the threat model.
43
+
44
+ **Documentation: <https://modelw.github.io/wtf/>** — the model, task-shaped
45
+ guides (adding it to a project, a PR that failed the gate, answering a
46
+ finding, reading the register) and the generated reference (CLI, file
47
+ schemas, threat catalogue). What follows is the same material in one file.
48
+
49
+ ## Compliance
50
+
51
+ ```
52
+ uv run model-wtf compliance init [--name X] [--controller-name X --controller-country CC]
53
+ [--processor-name X --processor-country CC | --no-processor]
54
+ [--codeowners | --no-codeowners] [--owner-dpo @org/team] [--owner-ciso @org/team]
55
+ uv run model-wtf compliance check [--strict] [--allow-todo] [--todo] [-v] [--format text|json|github] [--root PATH]
56
+ ```
57
+
58
+ `init` scaffolds the repo-root `compliance/` folder (`app.yaml`, one
59
+ `parties/<id>.yaml` per organisation, a README), adds a `compliance:` block to
60
+ every image of `snow.yml` (guessing the discovery backend from the code) and
61
+ creates the per-unit folders. It never
62
+ overwrites anything; re-run it to add what is missing. Values left for a human
63
+ are written as the YAML tag `!todo`. The processor defaults to
64
+ `default_processor: {name, country, address, email}` from
65
+ `~/.config/model-wtf/config.yml`.
66
+
67
+ When `origin` is on GitHub, `init` also writes a managed block into
68
+ `.github/CODEOWNERS` (between `# model-wtf compliance (managed)` markers,
69
+ hand-written rules untouched, re-runs rewrite it in place): the register
70
+ (`activities/`, `parties/`, `data/`, `data.lock.yaml`) to the DPO team, the
71
+ posture (`stores/`, `findings.lock.yaml`, `snow.yml`) to the CISO team,
72
+ `app.yaml` and `touchpoints/` to both. Teams default to `@<org>/dpo` and
73
+ `@<org>/ciso`; `--owner-dpo`/`--owner-ciso` override and are recorded as
74
+ `owners:` in `app.yaml`, which decides on later runs. `--codeowners` makes
75
+ a missing owner an error; `--no-codeowners` skips. `check` prints an info
76
+ line when a compliance folder (a unit added later) has no owner and a
77
+ `CODEOWNERS` exists.
78
+
79
+ `check` is the to-do list. It discovers the units, validates every
80
+ declaration file against its schema (pydantic; unknown keys are errors) and
81
+ groups what it finds by the kind of work it asks for:
82
+
83
+ ```
84
+ Errors — fix the files exit 3
85
+ Missing — non-compliant code or process, to build exit 1, never ignorable
86
+ Todo — questions only a human can answer exit 1 (--allow-todo → 0)
87
+ Review — run the agents, or decide exit 1
88
+ Info (-v for the details)
89
+ ```
90
+
91
+ One line per thing to do, with the command that resolves it after an arrow;
92
+ marker findings are folded per file (`parties/fah.yaml: address, email`).
93
+ `check --todo` prints only the open questions, one per line with the
94
+ question the field asks, so the list can be handed to whoever holds the
95
+ answers. `--format json` groups under `sections` with a stable `subject`
96
+ per entry (`app.yaml#description`, `api:data`) that a gate can diff between
97
+ runs; `--format github` maps Errors/Missing to `error`, Todo/Review to
98
+ `warning`, Info to `notice`.
99
+
100
+ Two YAML tags mark a value deliberately left open, both with an optional
101
+ note: `!todo` (the analysis has not been conducted) and `!missing "no purge
102
+ task, see FAH-210"` (it has, and the code or process is not there — an
103
+ established non-compliance, which always fails the gate).
104
+
105
+ ### Files
106
+
107
+ - `compliance/app.yaml` — `name`, `description`, `controller` (party id, the
108
+ client), optional `processor` (party id, the agency), `large_scale`
109
+ (Art. 35(3)(b); absent means no: a DPIA is then only required for
110
+ special-category data; `!todo` asks the question once) and `owners`
111
+ (`dpo`/`ciso` GitHub teams for `CODEOWNERS`).
112
+ - `compliance/parties/<id>.yaml` — `name`, `country` (ISO alpha-2),
113
+ `address`, `email`; optional `phone`, `website`, `hosts` (API hostnames the
114
+ code calls when they differ from the website, e.g. `api.hubapi.com`: a
115
+ call to one is a transfer to this party), `registration`, `dpa`
116
+ (where the processing agreement lives), `safeguard`/`dpf_certified`,
117
+ `dpo` and `representative` contact blocks. A party is role-less:
118
+ controller, processor or recipient is decided per processing activity. A
119
+ party nothing refers to (no transfer, no role, no `recipients`) is a Todo.
120
+
121
+ ### Unit discovery
122
+
123
+ Units are read from `snow.yml` at the repo root: every `images[]` entry that
124
+ carries a `compliance:` block is a unit. `discover` names the discovery backend
125
+ for that codebase (`django`, `sveltekit`, `none`); the compliance folder is
126
+ `compliance/` next to the image's Dockerfile unless `dir` (relative to the
127
+ build context) says otherwise. An image without `compliance` produces a
128
+ warning (an error under `--strict`).
129
+
130
+ ```yaml
131
+ images:
132
+ - id: api
133
+ context: api # Dockerfile at api/Dockerfile
134
+ compliance:
135
+ discover: django # -> api/compliance
136
+ - id: front
137
+ context: .
138
+ dockerfile: front/Dockerfile
139
+ compliance:
140
+ discover: sveltekit # -> front/compliance
141
+ - id: docs
142
+ context: .
143
+ compliance:
144
+ discover: none
145
+ dir: docs/compliance # -> docs/compliance
146
+ ```
147
+
148
+ Repos not deployed through Snow can use `.model-wtf.yml` instead
149
+ (`units: [{id, context, dockerfile, compliance}]`). When both files exist
150
+ `snow.yml` wins.
151
+
152
+ The repo-root `compliance/` folder is always loaded as the _shared_ scope
153
+ (controller, actors, assumptions, recipients).
154
+
155
+ ### Data inventory
156
+
157
+ ```
158
+ uv run model-wtf compliance data list [--unit ID] [--format table|json]
159
+ uv run model-wtf compliance data rules
160
+ uv run model-wtf compliance data override <unit>:<app.Model.field> [--pii|--no-pii] [--sensitivity L] [--category C] [--reason TEXT]
161
+ ```
162
+
163
+ Every Django model field of every `discover: django` unit is inventoried
164
+ live — model-wtf detects the unit's interpreter (uv, Poetry, `.venv`,
165
+ `MODEL_WTF_PYTHON`) and its `DJANGO_SETTINGS_MODULE` (env, `manage.py`,
166
+ `[tool.model-wtf] django_settings`), then pipes its own stdlib-only
167
+ introspection script into it. Nothing generated is written to disk.
168
+
169
+ Each field gets three classifications from the built-in rules
170
+ (`model_wtf/knowledge/data_rules/`, first match by ascending priority):
171
+
172
+ - `pii` — personal data or not;
173
+ - `sensitivity` — ordinal: `public < internal < personal < confidential < special`,
174
+ each level carrying a DPIA hint (`never`, `large_scale`, `always`);
175
+ - `category` — nominal, what the Art. 30 register will print (`identity`,
176
+ `contact`, `financial`, `connection`, `location`, `behavioural`, `content`,
177
+ `credentials`, `health`, `biometric`, `special_other`, `criminal`,
178
+ `professional`, `technical`).
179
+
180
+ `JSONField`s are presumed to hold personal data (`confidential`, `content`)
181
+ until a review says otherwise; unrecognised plain fields default to
182
+ `technical` and rely on the review to be promoted.
183
+
184
+ ### JSON-like columns hold *contents*
185
+
186
+ A `JSONField` / `ArrayField` / `HStoreField` (not Wagtail's `StreamField`,
187
+ which is CMS content) is a container: one triple cannot describe a blob
188
+ holding a name, an address and an IBAN. Its file declares what it holds,
189
+ one entry per **kind** of information (identifiers, not JSON paths):
190
+
191
+ ```yaml
192
+ contents:
193
+ customer_name: {pii: true, sensitivity: personal, category: identity}
194
+ iban: {pii: true, sensitivity: confidential, category: financial}
195
+ utm_campaign: {pii: false, sensitivity: internal, category: technical}
196
+ unknown_contents: none # none | possible | likely — is the list exhaustive?
197
+ reason: written in orders/services.py:88-104 and checkout/serializers.py:41
198
+ ```
199
+
200
+ Each entry becomes a row `<app.Model.field>@json.<name>` reviewed and
201
+ overridable on its own; the column's verdict is **derived** (`pii` = any,
202
+ `sensitivity` = max, `category` = the set as `financial+identity+technical`,
203
+ DPIA = max). `unknown_contents: none` replaces the rule's presumption,
204
+ `possible` keeps a `json-unknown-contents` warning, `likely` folds the
205
+ presumption back in (so `contents: {}` + `likely` = "opaque, treat as
206
+ personal"). `data contents <unit:id> name=yes,personal,contact ... [--unknown none]
207
+ [--reason ...]` writes the file; the auto-review never closes a JSON-like
208
+ field with a bare `ok`: it follows the write sites `data_model` lists and
209
+ declares the contents itself.
210
+
211
+ Humans correct the rules with `<unit>/compliance/data/<app.Model.field>.yaml`
212
+ (any subset of `pii` / `sensitivity` / `category` / `store`, plus a `reason`),
213
+ and add data the ORM does not know with a complete manual item
214
+ (`description`, `pii`, `sensitivity`, `category`, optional `store`) under any
215
+ other id.
216
+
217
+ `init --custom-sensitivity` / `--custom-categories` copy the built-in scale
218
+ or category list into `compliance/sensitivity/` / `compliance/categories/`
219
+ for editing; a renamed entry declares `replaces: [<built-in id>]` so the
220
+ rules still resolve, and `check` verifies every built-in id is covered once.
221
+
222
+ ### Review
223
+
224
+ The inventory is virtual, so what has been looked at is tracked in
225
+ `<unit>/compliance/data.lock.yaml` (fingerprint of the field facts *and* of
226
+ the verdict, who, when, note). `data list` shows a `Review` column
227
+ (`pending:new`, `pending:changed`, `reviewed`, `override`, `known`) and
228
+ `--pending` filters on it; `check` fails with exit 1 while
229
+ anything is pending.
230
+
231
+ - `data reviewed <unit>:<id>... [--note TEXT]` — a human confirms the current
232
+ classification.
233
+ - `data override …` also marks the item reviewed.
234
+ - Third-party models are reviewed like the project's own: what a task queue
235
+ or a user table holds is this project's data. What model-wtf already knows
236
+ about them lives in `knowledge/library/<app.Model>.yaml`: per field a
237
+ default verdict with `fixed: true` when the framework fixes the meaning
238
+ (`auth.User.password` — source/status `known`, nothing to review) or
239
+ `fixed: false` when it depends on the project (`ProcrastinateJob.args`,
240
+ `Session.session_data`, `FormSubmission.form_data` — source `library`,
241
+ status `pending:assumed`). Assumed models carry an `assumption` and a
242
+ `check`: what we take for granted and what to look at in *this* project;
243
+ `data list` prints the assumption under the row (`--assumed` filters on
244
+ them), `data_model` shows it to the agent (which must do the check before
245
+ confirming); `check` only counts them in the pending breakdown
246
+ (`37 data item(s) pending (12 new, 9 assumed, 16 contents)`).
247
+ `fields_default` covers unlisted columns of tables that are technical
248
+ through and through.
249
+ - Every file field also yields `<field>@files.content`: the bytes in the
250
+ storage behind the column, classified on their own.
251
+
252
+ ### Stores
253
+
254
+ ```
255
+ uv run model-wtf compliance stores list [--unit ID] [--all] [--format table|json]
256
+ uv run model-wtf compliance stores explain <unit>:<slug>
257
+ ```
258
+
259
+ Where the data lives is an item with a slug, and every data row references one
260
+ in its `Store` column. A store is a slug, a `type` and a conceptual `backend`
261
+ (`postgresql`, `redis`, `s3`, `filesystem`, ...) — nothing environmental:
262
+ hosts, bucket names and credentials belong to a deployment, not to the model
263
+ of the application. Stores are read from the Django settings, so there is
264
+ nothing to write for the common case: `DATABASES[alias]` → `db-<alias>` (rows
265
+ follow the router), `CACHES` → `cache-<alias>`, `STORAGES` → `files-<alias>`
266
+ (`bucket` when the backend is S3/GCS/Azure, else `filesystem`; `staticfiles`
267
+ skipped), a field's own `storage=` → `files-<app.Model.field>`,
268
+ `CELERY_BROKER_URL` → `queue-celery`, `WAGTAILSEARCH_BACKENDS` →
269
+ `search-<alias>`.
270
+
271
+ Optional `<unit>/compliance/stores/<slug>.yaml` files can override facts of an
272
+ introspected store (`backend`, `name`, `provider`, `location` as a region or
273
+ country, `retention`, `description`), declare a store the settings do not show
274
+ (`type: external`, `browser`, ... — `type` is then mandatory), or hide one
275
+ with `ignore: true`.
276
+ `check` reports `store-unknown` / `store-ignored-referenced` for data rows
277
+ naming a slug that does not exist or is hidden, and `store-orphan` for a
278
+ manual store file without `type`. `data override … --store <slug>` moves a
279
+ row to another store.
280
+
281
+ ```
282
+ uv run model-wtf compliance data auto-review [--unit ID] [--base REF] [--batch 8] [--workers 16] [--max-rounds 20] [--max-tokens N] [--model provider/model] [--dry-run]
283
+ ```
284
+
285
+ Runs an OpenCode agent on OpenRouter (`openrouter/openrouter/auto` by default;
286
+ `OPENROUTER_API_KEY` required) until nothing is pending. The instance is
287
+ sandboxed (`model_wtf/opencode.py`): throwaway `HOME`/XDG tree, generated
288
+ config via `OPENCODE_CONFIG`, `--pure`, whitelisted environment, deny-all
289
+ permissions except read/glob/grep inside the repository and its interpreters'
290
+ import roots, our MCP server as the only write path; repository-level
291
+ `opencode.json` / `.opencode/` / `AGENTS.md` have no effect. Work is
292
+ dispatched one **model** at a time: `data_pending` → `data_model` (field
293
+ table + class source + JSON write sites) → `data_review_model` (all decisions
294
+ in one call), which keeps each subagent session small enough for flash-class
295
+ models. `--base REF` also re-dispatches models whose file changed since `REF`.
296
+
297
+ ### Touchpoints
298
+
299
+ ```
300
+ uv run model-wtf compliance touchpoints list [--unit ID] [--pending] [--all] [--format json]
301
+ uv run model-wtf compliance touchpoints show <unit>:<id>
302
+ uv run model-wtf compliance touchpoints set-data <unit>:<id> [<unit>:<item>[=read|write]...] [--add|--remove] [--ignore] [--note ...]
303
+ ```
304
+
305
+ Vocabulary, kept clear of pytm's: a **unit** is a running component (pytm
306
+ *Process*); a **touchpoint** is one of its entry points through which data
307
+ flows (pytm *Dataflows*); an **activity** is a GDPR processing activity. The
308
+ word "process" is not used.
309
+
310
+ Touchpoints are introspected, never written: Django URL patterns (id = route
311
+ name, or `METHOD /path`; Ninja endpoints by operation id with their
312
+ request/response schemas flattened from the API's own OpenAPI document, DRF
313
+ serializers, `FormView` fields, auth classes), Procrastinate/Celery tasks
314
+ (`task:<name>`, signature, periodic flag, tasks they defer) and admin screens
315
+ (`admin:<app.Model>`, the fields staff see). SvelteKit units run `svelte-kit
316
+ sync` and read the generated `$types.d.ts` with the project's own TypeScript:
317
+ id = route ID, `RouteParams`, `PageData`/`ActionData` shapes, form field
318
+ names, and the generated-API-client operations the route calls — which link
319
+ to the Django touchpoints by operation id (`calls`). Plumbing (health checks,
320
+ OpenAPI documents, the admin's own URL patterns) is ignored by default.
321
+
322
+ The optional manifest `<unit>/compliance/touchpoints/<slug>.yaml` declares
323
+ what the touchpoint **does** to data, with a closed vocabulary of **facts**
324
+ (`src/model_wtf/compliance/ops.py`): each `data:` entry is a ref (`@json`/
325
+ `@files` rows allowed, `unit:app.Model.*` for a whole model) and its ops —
326
+ a bare `- unit:app.Model.field` is a `read`, `- ref: create`, `- ref: [create,
327
+ read]`, `- ref: {delete: {mode: anonymise}}`, `- ref: {retention_purge:
328
+ {after: settings.ANONYMOUS_ADDRESS_MAX_AGE, since: last use, when: anonymous
329
+ only}}`. Verbs: `create[{consent_for}]`, `read`, `update`, `delete[{mode}]`,
330
+ `retention_purge{after (duration or setting name), since, when?}`,
331
+ `portability{format}`, `consent_withdraw{for}`; each verb takes only its own
332
+ metadata. No legal verb: a person changing their own address is an `update`,
333
+ deleting it a `delete` — what that means for their rights follows from the
334
+ touchpoint's **scope** (`scope: subject | staff | public | system`, inferred
335
+ from auth classes, `request.user` in the body and admin namespaces, or
336
+ declared in the manifest). `write`, `rectify`, `access`, `erase`, `object`,
337
+ `restrict` still load, folded onto the fact they imply with an
338
+ `op-ambiguous` warning. `transfers:`
339
+ lists what leaves to another organisation's API — `- {party: mapbox, data:
340
+ [...], purpose: ...}`, the party being a `compliance/parties/` id, which is
341
+ where the register's recipients come from (`exporting:` still loads, with a
342
+ deprecation warning); plus `ignore`, `note`. The project's own database,
343
+ file storage, cache and queue are *stores*, not transfers, whoever hosts
344
+ them: hosting is a separate layer, taken as adequate here. Every inventory item the code
345
+ touches is listed, personal or not: the register filters on `pii`
346
+ downstream, the data-flow model needs all of it. A touchpoint is
347
+ **pending** until it has a `data` key — an explicit `[]` means "touches no
348
+ inventory item, checked". `check` reports `touchpoint-pending` and
349
+ `touchpoint-orphan` (handles personal data, belongs to no activity), both
350
+ exit 1, and `data-unreferenced` as information.
351
+
352
+ Introspection pre-fills **likely ops** (`touchpoints show` → "likely ops"):
353
+ HTTP method (`POST` → create, `PUT/PATCH` → update|rectify, `DELETE` →
354
+ delete|erase), Django admin permissions (`has_*_permission` overrides,
355
+ `readonly_fields`, `list_display`), task names (`purge|clean|expire`,
356
+ `anonymi[sz]e|erase|gdpr`, `export`) and task bodies (`.delete()`,
357
+ `.update()`, `timedelta(days=30)` as an `after` hint), SvelteKit handlers and
358
+ action names. The reviewer confirms them against the code. `data why`
359
+ prints each item's lifecycle from the ops: *created by api:signup, read by
360
+ 6, rectified by admin:people.User (by staff), never erased, purged by
361
+ api:task:cart.purge (after days 30, ...), sent to mapbox*.
362
+
363
+ ```
364
+ uv run model-wtf compliance touchpoints auto-review [--unit ID] [--batch 8] [--workers 16] [--max-rounds 20] [--group/--no-group] [--group-only] [--model ...] [--max-tokens N]
365
+ ```
366
+
367
+ Same sandboxed OpenCode loop as `data auto-review` (`--workers` sessions run in
368
+ parallel each round, each on its own shard of the pending list), two passes. **Pass 1**,
369
+ one touchpoint per subagent session: `touchpoint_show` gives the code
370
+ location, the schemas and what the API operations it calls already declare;
371
+ the reviewer reads the view/task/route, resolves items with `data_search`
372
+ (never typing an id it did not see), creates a **manual item** with
373
+ `data_add_manual` for personal data the ORM has no row for — transient
374
+ (a card number forwarded to the PSP, a position sent to a geocoder, a search
375
+ query) or kept outside the ORM; processing counts even without storage,
376
+ while non-personal transient values are not tracked — and closes
377
+ with one `touchpoint_set_data` call citing file:line (`[]` = touches nothing
378
+ personal). **Pass 2**, once nothing is pending (or right away with
379
+ `--group-only`): a single session reads `activities_graph` — every
380
+ PII-touching touchpoint with its categories, `calls`/`defers` edges and
381
+ current activity — and follows the chains front → api → task to
382
+ `activity_create` / `activity_add_touchpoints`; `legal_basis` only when
383
+ evident, `retention` and the rest stay `!todo` for a human, existing
384
+ activities are never emptied.
385
+
386
+ ### Flows
387
+
388
+ Data, touchpoints and activities say what exists, who touches it and why.
389
+ **Flows** say where it goes: one per edge along which items move, named
390
+ `source->sink`, with a **kind** decided from its ends and a **status**:
391
+
392
+ | kind | ends | status |
393
+ |------------|---------------------------------------|---------------------------|
394
+ | `request` | an actor and a touchpoint | derived (shapes) |
395
+ | `store` | a touchpoint and a project store | declared (ops) |
396
+ | `transfer` | a touchpoint and a party | declared (`transfers`) |
397
+ | `call` | a front route and an API operation | derived (introspection) |
398
+ | `defer` | a touchpoint and a background task | derived |
399
+ | any | found by a reviewer, absent from the model | **undeclared** — a gap |
400
+
401
+ ```
402
+ uv run model-wtf compliance flows list [--unit api] [--element api:listRestaurants] [--kind transfer] [--status undeclared] [--format json]
403
+ uv run model-wtf compliance flows show api:listRestaurants->party:mapbox # items, and the threat cells on it
404
+ ```
405
+
406
+ The threat reviewers get the same inventory in words through the `flows`
407
+ tool (and inside `threat_topic`): *sends geo.Address.position to
408
+ party:mapbox (US) — declared transfer, safeguarded: sending it there is the
409
+ intended use*; *create/read Cart rows on api:db-default*; *exchanges 38
410
+ items with anonymous callers*. A declared, safeguarded transfer is not a
411
+ leak, and the reviewer no longer reconstructs the flows from the code and
412
+ calls one. What the code sends somewhere **not on the list** is reported
413
+ with `flow_report(element, sink, data, note)`: the manifest gets an
414
+ `undeclared:` entry, `check` shows a `flow-undeclared` finding (Missing) and
415
+ the touchpoint is pending again — the declaration is incomplete. Declaring
416
+ the transfer (party first) closes it; the transfer's own threats then
417
+ follow the `declared_transfer` rule.
418
+
419
+ Flow-only threats (DS06, DR01, AC22…) are stamped per flow: `DS06@actor:public`
420
+ for the response, `DS06@api:db-default` for the store side. A bare `DS06` on
421
+ the touchpoint is refused while several flows carry the open cell, with the
422
+ keys to use; it is accepted when only one does.
423
+
424
+ ### Activities
425
+
426
+ ```
427
+ uv run model-wtf compliance activities list [--format json]
428
+ uv run model-wtf compliance activities explain <slug>
429
+ uv run model-wtf compliance activities create <slug> [--name] [--purpose] [--legal-basis] [--touchpoint unit:id]... [--subject]... [--recipient]... [--retention]
430
+ uv run model-wtf compliance activities add <slug> <unit:id>...
431
+ uv run model-wtf compliance data why <unit:id>... [--model unit:app.Model] [--manifests] [--format json]
432
+ ```
433
+
434
+ `compliance/activities/<slug>.yaml` (repository root, activities span units)
435
+ is the Art. 30 row: `name`, `purpose`, `legal_basis` (`consent | contract |
436
+ legal_obligation | vital_interests | public_task | legitimate_interests`, or
437
+ `no_pii` — a claim that the activity handles no personal item, verified at
438
+ every check: `no-pii-violated` otherwise), `data_subjects`, `touchpoints`,
439
+ `recipients` (party ids), `controller`/`processor` (default: `app.yaml`'s);
440
+ `consent: {record: <ref>, granularity: separate|bundled}` for consent-based
441
+ ones (the stored proof, created with `create: {consent_for: <slug>}`),
442
+ `interest` for legitimate interests (the balancing test), `basis_note` when
443
+ two bases compete, `dpia_reference` when the derived trigger fires. Any of
444
+ them may be `!todo` or `!missing "why"`. Everything else is **derived** from
445
+ the touchpoints: the data items, hence categories, stores, maximum
446
+ sensitivity, DPIA trigger, units, recipients and the ops per item. Retention
447
+ is not a field: the policy is the `retention_purge` op in the code.
448
+
449
+ ### Rights coverage
450
+
451
+ Nothing about rights is written on activities. Touchpoints state what the
452
+ code does (ops), data items state what is true of the data regardless of
453
+ code, activities carry purpose and basis; `check` derives, **per personal
454
+ item in every activity that handles it**, whether each right is served
455
+ (`src/model_wtf/compliance/rights.py`):
456
+
457
+ | right | satisfied when | code |
458
+ | -- | -- | -- |
459
+ | access (Art. 15) | a `subject`-scoped touchpoint `read`s it | `access-missing` |
460
+ | rectification (Art. 16) | only for values the person provided: a `subject` `update`, or delete + create (re-creation) | `rectification-missing` |
461
+ | erasure (Art. 17) | a `subject` `delete`; `mode: anonymise` needs a ground to keep the row; `legal_obligation` activities exempt by construction | `erasure-missing` |
462
+ | storage limitation (Art. 5(1)(e)) | a `retention_purge` covering all rows, or purge cases + a delete path for the rest; a staff/system `delete` also ends the row's life | `retention-missing` |
463
+ | portability (Art. 20) | consent/contract, values the person provided, access served: a `portability` op or a JSON API the person calls on their own data | `portability-missing` |
464
+ | objection (Art. 21) | legitimate-interests activities: a `subject` update/delete on one of its items (an opt-out) | `objection-missing` |
465
+ | consent (Art. 7) | consent activities: `consent.record` created with `consent_for`, and a `consent_withdraw: {for: slug}` op | `consent-proof-missing`, `consent-withdrawal-missing` |
466
+ | transfers (Ch. V) | party outside the EEA / adequacy list (`knowledge/adequacy.yaml`) carries `safeguard: sccs|bcr|dpf|derogation` (`dpf` with `dpf_certified: true`); an unknown country is a Todo | `transfer-safeguard-missing` |
467
+ | DPIA (Art. 35) | special-category data (`always`) → `dpia_reference` on the activity; confidential data (`large_scale`) only when `app.yaml` says `large_scale: true` | `dpia-missing` |
468
+
469
+ When a staff screen performs the op but no self-service does, the finding
470
+ says so (*no self-service; staff can via admin:people.User — exempt
471
+ staff_only if a request process exists*). When every activity holding an
472
+ item is about `staff`/`employees`, the back-office is the person's own
473
+ interface and staff ops count as the subject's. Transient manual items
474
+ (`transient: true`) have no storage-side rights, only transfers. Library
475
+ models ship their own rights story (`knowledge/library/*.yaml` `rights:`
476
+ block: an audit trail is kept for accountability, a session is purged by the
477
+ framework) which applies to inherited columns too (a page type's `owner`).
478
+
479
+ Exemptions live on the **data item** (`<unit>/compliance/data/<id>.yaml`, or
480
+ `<app.Model>.*.yaml` for every personal field of a model; the item's own
481
+ file wins right by right):
482
+
483
+ ```yaml
484
+ rights:
485
+ erase: {exempt: legal_obligation, note: "accounting records, 10 years"}
486
+ portability: {exempt: derived}
487
+ rectify: {exempt: staff_only} # verified: an admin op by staff must exist
488
+ access: {exempt: manual, note: "..."} # always listed under Review
489
+ retention: !missing "no purge task, see FAH-210"
490
+ ```
491
+
492
+ Grounds: `legal_obligation`, `contract_active` (still needs an event-driven
493
+ `erase`), `not_provided_by_subject` (portability), `derived`
494
+ (rectify/portability), `staff_only`, `manual`, `public_interest`, `research`,
495
+ `legal_claims`. Precedence for a right: item exemption → derived from ops →
496
+ missing. Every unmet right lands in the **Missing** section tagged with its
497
+ origin — `[derived]` (the tool), `[claimed]` (an agent that read the code,
498
+ via `data_flag` or a `{"missing": ...}` verdict in `activity_create`; the
499
+ note is prefixed `[agent]`), `[declared]` (a human's `!missing`) — and
500
+ `data why` prints each right's status next to the item's lifecycle.
501
+
502
+ ### Threats
503
+
504
+ The threat model is a projection of the folder, not a new declaration.
505
+ Every touchpoint is a *process* (one node each), every store a *store*,
506
+ the actors (the person, staff, anyone, the system) and the parties with
507
+ transfers are *parties*, and *flows* join them: actor → touchpoint,
508
+ touchpoint → store (its ops), touchpoint → party (transfers), front route →
509
+ api operation (`calls`), touchpoint → task (`defers`). A flow carries the
510
+ declared items and their sensitivity.
511
+
512
+ The catalogue is pytm's threat library (`knowledge/threats/<SID>.yaml`,
513
+ generated by `threats gen` — a developer command that refuses a pytm
514
+ threat `_mapping.yaml` does not classify). The mapping says how each
515
+ threat is treated: `never` (impossible in our stacks — memory-safe
516
+ runtimes, no PHP/LDAP/SOAP — or infra we do not model — TLS, HTTP
517
+ smuggling, hosting), or a list of **dismissal rules** (`_rules.yaml`) any
518
+ of which closes the cell for an element. Rules are SIMPLE facts: the
519
+ touchpoint has no request (a task, a bare GET), returns JSON not HTML, has
520
+ no `mark_safe`/`{@html}` in its files, no `.raw(`, no `subprocess`, no XML
521
+ parser, no file input, is not cookie-authenticated (CSRF), is public by
522
+ design (ownership does not apply), the flow carries no personal item, no
523
+ credentials. No AST, no reasoning: when a rule cannot decide, the cell is
524
+ **open** and belongs to an agent under the threat's topic (access, auth,
525
+ input, disclosure, dos, files, xss, csrf, credentials, store).
526
+
527
+ ```
528
+ uv run model-wtf compliance threats matrix # counts per kind and topic
529
+ uv run model-wtf compliance threats matrix --open # every open cell
530
+ uv run model-wtf compliance threats why api:getOrder # each threat: in/out and the rule
531
+ ```
532
+
533
+ `check` folds the open cells into one Review line per unit (items
534
+ `unit:id#SID` ride on it for the gate). Undeclared touchpoints do not
535
+ count yet: their flows are unknown until they are reviewed.
536
+
537
+ #### Stamps
538
+
539
+ An open cell is closed by a **stamp** in the element's own YAML
540
+ (touchpoint manifest, `stores/<slug>.yaml`, `parties/<id>.yaml`):
541
+
542
+ ```yaml
543
+ threats:
544
+ AC01: {status: mitigated, note: "get_object_or_404(user=request.user) api.py:245"}
545
+ DO02: {status: accepted, note: "list capped at 50 by CursorPagination"}
546
+ HA01: {status: n/a, note: "photo id is a UUID looked up in the DB; no path built"}
547
+ DS06: !missing "returns payment_method to anonymous callers (auth=None)"
548
+ DS06@party:mapbox: {status: mitigated, note: "only the position is sent"}
549
+ ```
550
+
551
+ `mitigated` (the code handles it; cite where), `accepted` (the risk owner
552
+ accepts; say why), `n/a` (the rule could not tell but the threat does not
553
+ apply here). A key is a SID — the element and every flow it is part of —
554
+ or `SID@<other end>` for one flow. `!missing` is a finding: **Missing**
555
+ (`threat-missing`, origin claimed/declared like rights). Stamps written by
556
+ the tool carry the element's fingerprint; when the code moves the stamp is
557
+ **stale** and the cell reopens. Re-declaring a touchpoint keeps its
558
+ stamps; the challenger's `reviews` tool lists them as assertions to
559
+ re-check.
560
+
561
+ ```
562
+ uv run model-wtf compliance threats stamp api:getOrder AC01 --status mitigated --note "orders/api.py:283 scoped to request.user"
563
+ uv run model-wtf compliance threats stamp api:getOrder DS06 --missing "payment_method returned to anonymous callers"
564
+ uv run model-wtf compliance threats stamp api:checkout->api:db-default DS06 --status n/a --note "the app's own database"
565
+ ```
566
+
567
+ #### Severity
568
+
569
+ A `!missing` stamp is weighed by the tool, not the agent: **impact ×
570
+ likelihood**. Impact is the *effect* — `disclosure`, `tampering`,
571
+ `destruction`, `denial`, `escalation`, `repudiation` (`_mapping.yaml`
572
+ carries one per threat; `ops` resolves from what the touchpoint does) — at
573
+ a *degree* (`existence` 0.25 < `attribute` 0.5 < `record` 1 < `bulk` 1.5,
574
+ inferred: lists, exports, admin screens, tasks and integer ids are `bulk`)
575
+ on the most sensitive item reached (`public` 0 … `special` 4); escalation
576
+ counts 4, denial is capped at 2. Likelihood is the most feared *actor* the
577
+ touchpoint's scope lets in — `anonymous`, `subject`, `staff`, `system`,
578
+ each with a `malice` and a `reach` in `knowledge/threats/_actors.yaml`,
579
+ overridable in `compliance/actors.yaml` — minus the actors already
580
+ **entitled** to that data through another declared touchpoint (staff
581
+ counting pictures they see in the back-office is not a finding). Buckets:
582
+ `critical` / `high` / `medium` / `low` / `info`. The reviewer may only
583
+ narrow (`--degree existence`, `--effect denial`, `--actor subject`) with a
584
+ reason; `check` tags and sorts findings by risk.
585
+
586
+ Every finding gets a stable id, `F-0042`, allocated in
587
+ `compliance/findings.lock.yaml` on first sighting and never reused (a fixed
588
+ finding is closed with a date, not deleted, so a ticket citing it still
589
+ resolves). `threats findings` lists them most severe first — several
590
+ threats with the same evidence on one element fold into one row — and
591
+ `threats why F-0042` explains one: weight, who, data, evidence, the
592
+ threat's description and mitigations. A declared, safeguarded transfer to
593
+ a party is the intended use, not a leak: disclosure threats on that flow
594
+ are dismissed by rule.
595
+
596
+ #### The swarm
597
+
598
+ ```
599
+ uv run model-wtf compliance threats auto-review --unit api # one reviewer per topic
600
+ uv run model-wtf compliance threats auto-review --by touchpoint # one reviewer per touchpoint
601
+ uv run model-wtf compliance threats auto-review --elements api:getOrder,api:checkout
602
+ ```
603
+
604
+ `auto-review` sends agents to stamp the open cells. By default one
605
+ reviewer per **topic** (access, auth, input, disclosure, dos, files, xss,
606
+ csrf, credentials, store, llm; `knowledge/threats/_topics.yaml` carries
607
+ each checklist) over a batch of touchpoints (`--topic-batch`, 12): the
608
+ same question asked of each touchpoint, answered from its code with
609
+ `threat_stamp`. `--by touchpoint` sends one reviewer per touchpoint with
610
+ all its open SIDs instead. Measured on Food@Home (14 subject-facing
611
+ endpoints, same commit): per topic closed every cell for 49k tokens and
612
+ found the unscoped `Cart`/`Order` lookups; per touchpoint spent 109k
613
+ tokens, stalled on 12 of 14 and missed them — small models do better with
614
+ one narrow question than with fifteen. The reviewer never reclassifies
615
+ data or edits ops; a stamp it cannot justify with a file:line stays open.
616
+
617
+ Introspection payloads are cached under `.git/model-wtf/introspect/`,
618
+ keyed by the source tree (paths, sizes, mtimes): a swarm of MCP servers
619
+ boots Django once, not once per tool call. `MODEL_WTF_NO_CACHE=1` bypasses.
620
+
621
+ ### Use it in CI: the gate
622
+
623
+ Nobody expects a repository to be clean on day one; the gate expects it to
624
+ **not get worse**. `model-wtf compliance ghate` runs the whole `check`
625
+ twice — on the base ref, checked out into a temporary `git worktree` with
626
+ its own `compliance/` state, and on the head (the working tree by default,
627
+ so uncommitted work is gated too) — and fails only on findings the change
628
+ **introduces**.
629
+
630
+ ```yaml
631
+ # .github/workflows/compliance.yml (written by `compliance init`)
632
+ name: compliance
633
+ on: [pull_request]
634
+ jobs:
635
+ gate:
636
+ runs-on: ubuntu-latest
637
+ steps:
638
+ - uses: actions/checkout@v4
639
+ with:
640
+ fetch-depth: 0
641
+ ref: ${{ github.event.pull_request.head.ref }}
642
+ - uses: ModelW/wtf@v1
643
+ with:
644
+ openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }} # optional
645
+ ```
646
+
647
+ The action (`action.yml` at the root of this repository, `v1` tag) installs
648
+ uv and model-wtf, runs `uv sync --frozen` / `pnpm install` in every folder
649
+ holding a lockfile so introspection works, then runs the gate. Inputs:
650
+ `merge-into` (default: the PR base from the event), `fail-on-existing`,
651
+ `python-version`, `install-python-deps`, `install-node-deps`. Outputs:
652
+ `introduced`, `fixed`, `pre-existing`. Under Actions it emits one
653
+ `::error`/`::warning` annotation per introduced finding on the head's
654
+ files, a `::notice` verdict, and a Markdown table (introduced / fixed /
655
+ pre-existing per check) in the step summary.
656
+
657
+ Locally:
658
+
659
+ ```
660
+ uv run model-wtf compliance ghate --merge-into develop # gate the working tree
661
+ uv run model-wtf compliance ghate --merge-into develop --head feature/x
662
+ uv run model-wtf compliance ghate --merge-into develop --format json
663
+ ```
664
+
665
+ Findings are compared by **identity** — `(scope, code, subject)`, where the
666
+ subject is a data id, a touchpoint id, an activity slug or `file#field`,
667
+ never a line number or a message. Folded lines (`12 data item(s) pending`)
668
+ are compared item by item, so a PR that adds an unreviewed personal field
669
+ fails with exactly that item, while a PR touching an unrelated file when
670
+ 300 items were already pending passes. Fixed findings are reported too.
671
+
672
+ The base worktree gets the head's `.venv` / `node_modules` linked in when
673
+ the unit's lockfile is byte identical on both sides; otherwise the base
674
+ run is approximate and the gate says so (a dependency change is a
675
+ legitimate reason for new findings). Exit codes: 0 nothing introduced
676
+ (pre-existing findings are listed, not failed), 1 findings introduced, 3
677
+ declaration errors in the head (always the PR's fault), 4 tool error;
678
+ `--fail-on-existing` also fails on pre-existing findings for repositories
679
+ that are already clean.
680
+
681
+ #### The challenger
682
+
683
+ Fingerprints catch shape changes on data items (a field's type or
684
+ nullability); they cannot catch a view that starts mailing an address to a new
685
+ provider, a purge task that gets disabled, or a column re-purposed with the
686
+ same type. That is a reading job, so with an API key the gate first runs
687
+ the **challenger**: an agent with the full git checkout, `git diff` and
688
+ `grep`, the `reviews` tool (what reviewers asserted about the changed
689
+ files: classifications with their reasons, declared ops, transfers,
690
+ exemption notes — every one citing code) and one write tool, `challenge`.
691
+ It does not reclassify; it re-opens, with grounds citing the hunk.
692
+
693
+ `reviews` lists three kinds of assertion: data classifications, touchpoint
694
+ declarations and **threat stamps** (`threat api:getOrder#AC01: mitigated —
695
+ orders/api.py:283 scoped to request.user`). `challenge` takes any of the
696
+ three refs. A hunk that changes what a touchpoint reads, writes, returns or
697
+ sends re-opens the declaration; a hunk that removes the control a stamp
698
+ cites (a queryset scope, an auth class, a throttle, a validator, a cookie
699
+ setting) re-opens that one stamp — `element#SID` — and the cell is stale
700
+ (`threats why` shows the grounds) until re-stamped; both when both. A
701
+ `!missing` is a finding, not a claim: it cannot be challenged. Stores are
702
+ listed when a settings file is in the diff, since their stamps cite
703
+ settings.
704
+
705
+ A challenge is recorded in `data.lock.yaml` (`challenge: {commit,
706
+ grounds}`), in the touchpoint manifest, or on the stamp itself, and makes
707
+ the item `pending:challenged` (the cell `stale`), which the gate counts as
708
+ introduced. The record is
709
+ what keeps the non-determinism out of the gate: an item is challenged at
710
+ most once per change, a re-review (confirming is fine) moves the challenge
711
+ to `answered:` and the same grounds are refused afterwards — a false
712
+ positive costs one review, then silence. In CI the challenger's commit is
713
+ pushed to the PR branch (`commit-challenges`, default on); the developer
714
+ sees exactly which reviews to redo and why. The agent's shell never sees
715
+ the CI environment: only the OpenCode whitelist, and our own MCP server
716
+ drops `GITHUB_TOKEN` and friends.
717
+
718
+ ```
719
+ uv run model-wtf compliance challenge --merge-into develop [--commit]
720
+ ```
721
+
722
+ ### Exit codes
723
+
724
+ | Code | Meaning |
725
+ | ---- | -------------------------------------------------------------------- |
726
+ | 0 | Clean (or only `!todo` questions, with `--allow-todo`) |
727
+ | 1 | Missing (`!missing`, never ignorable), Todo (`!todo`), Review (pending data / touchpoints, orphans) |
728
+ | 2 | Stale attestation |
729
+ | 3 | Declaration errors (schema, missing files, dangling party ids) |
730
+ | 4 | Tool error |
731
+
732
+ ## Development
733
+
734
+ ```
735
+ uv sync
736
+ make clean # format + lint + typecheck
737
+ make test
738
+ make docs # generate the reference pages and build the site into site/
739
+ ```
740
+
741
+ ### Releasing
742
+
743
+ The version is the git tag; `pyproject.toml` carries a `0.0.0` placeholder
744
+ that the release workflow replaces before building. To cut a release:
745
+
746
+ ```
747
+ git tag v1.2.3 && git push origin v1.2.3 # a release
748
+ git tag v1.2.3rc1 && git push origin v1.2.3rc1 # a release candidate
749
+ ```
750
+
751
+ `release.yml` builds, publishes to PyPI through trusted publishing (the
752
+ PyPI project trusts this repository's `release.yml` in the `pypi`
753
+ environment; no token is stored), moves the `v1` branch and tag so
754
+ `uses: ModelW/wtf@v1` follows the latest 1.x, and creates the GitHub
755
+ release with the matching `CHANGELOG.md` section. Write that section
756
+ before tagging. A candidate (`rc`, `a`, `b`) is published and released as a
757
+ pre-release under the final version's changelog section but does **not**
758
+ move `v1`: try it with `uses: ModelW/wtf@v1.2.3rc1` or `pip install
759
+ model-wtf==1.2.3rc1`.