@terpjs/spec 0.14.0

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 (392) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +417 -0
  3. package/VERSION +1 -0
  4. package/app-check-report.schema.json +122 -0
  5. package/assurance-profile.schema.json +70 -0
  6. package/catalog/backend/alembic_downgrades_not_empty.json +22 -0
  7. package/catalog/backend/base_query_not_overridden.json +27 -0
  8. package/catalog/backend/canonical_module_shape.json +22 -0
  9. package/catalog/backend/datetime_columns_are_timezone_aware.json +23 -0
  10. package/catalog/backend/escape_hatch_budget.json +21 -0
  11. package/catalog/backend/events_reference_catalog.json +27 -0
  12. package/catalog/backend/input_schemas_exclude_managed_columns.json +27 -0
  13. package/catalog/backend/input_str_fields_have_max_length.json +22 -0
  14. package/catalog/backend/jobs_reference_catalog.json +27 -0
  15. package/catalog/backend/list_routes_paginate.json +32 -0
  16. package/catalog/backend/modules_declare_policy.json +26 -0
  17. package/catalog/backend/mutations_emit_audit.json +26 -0
  18. package/catalog/backend/mutations_require_write_role.json +32 -0
  19. package/catalog/backend/no_adhoc_background_runtime.json +22 -0
  20. package/catalog/backend/no_adhoc_config_decrypt.json +27 -0
  21. package/catalog/backend/no_adhoc_logging_config.json +22 -0
  22. package/catalog/backend/no_adhoc_middleware.json +27 -0
  23. package/catalog/backend/no_adhoc_permission_literals.json +27 -0
  24. package/catalog/backend/no_app_instantiation.json +22 -0
  25. package/catalog/backend/no_blocking_sleep.json +22 -0
  26. package/catalog/backend/no_cross_module_imports.json +21 -0
  27. package/catalog/backend/no_dependency_overrides.json +27 -0
  28. package/catalog/backend/no_destructive_migrations.json +22 -0
  29. package/catalog/backend/no_dynamic_sql.json +22 -0
  30. package/catalog/backend/no_empty_tests.json +22 -0
  31. package/catalog/backend/no_eval_or_exec.json +22 -0
  32. package/catalog/backend/no_hardcoded_credentials.json +21 -0
  33. package/catalog/backend/no_internal_imports.json +22 -0
  34. package/catalog/backend/no_manual_actor_stamping.json +27 -0
  35. package/catalog/backend/no_manual_ownership_checks.json +32 -0
  36. package/catalog/backend/no_manual_scope_filtering.json +27 -0
  37. package/catalog/backend/no_manual_table_schema.json +22 -0
  38. package/catalog/backend/no_manual_version_assignment.json +22 -0
  39. package/catalog/backend/no_mutable_default_args.json +22 -0
  40. package/catalog/backend/no_naive_datetime.json +22 -0
  41. package/catalog/backend/no_oversized_python_files.json +22 -0
  42. package/catalog/backend/no_print.json +22 -0
  43. package/catalog/backend/no_raw_app_routes.json +26 -0
  44. package/catalog/backend/no_raw_connection_access.json +27 -0
  45. package/catalog/backend/no_raw_file_references.json +27 -0
  46. package/catalog/backend/no_raw_outbound_http.json +22 -0
  47. package/catalog/backend/no_raw_session_construction.json +22 -0
  48. package/catalog/backend/no_star_imports.json +22 -0
  49. package/catalog/backend/no_todo_fixme.json +22 -0
  50. package/catalog/backend/no_unique_columns_on_soft_delete_models.json +22 -0
  51. package/catalog/backend/offset_queries_declare_ordering.json +22 -0
  52. package/catalog/backend/path_id_params_are_uuid.json +22 -0
  53. package/catalog/backend/policy_refs_resolve.json +27 -0
  54. package/catalog/backend/public_modules_are_read_only.json +26 -0
  55. package/catalog/backend/reads_use_base_query.json +27 -0
  56. package/catalog/backend/response_model_not_table_model.json +27 -0
  57. package/catalog/backend/routes_declare_response_model.json +27 -0
  58. package/catalog/backend/safe_methods_are_read_only.json +31 -0
  59. package/catalog/backend/schemas_exclude_sensitive_fields.json +32 -0
  60. package/catalog/backend/session_imported_from_sqlmodel.json +22 -0
  61. package/catalog/backend/table_models_use_base_table.json +22 -0
  62. package/catalog/backend/tables_have_migrations.json +27 -0
  63. package/catalog/backend/tenant_scoped_models_use_scoped_service.json +22 -0
  64. package/catalog/backend/ungoverned_escape_hatch.json +21 -0
  65. package/catalog/backend/update_schemas_inherit_base_update_schema.json +32 -0
  66. package/catalog/frontend/escape-hatch.json +21 -0
  67. package/catalog/frontend/generated-client-only.json +26 -0
  68. package/catalog/frontend/layout-contract.json +26 -0
  69. package/catalog/frontend/no-cross-module-imports.json +21 -0
  70. package/catalog/frontend/no-deep-imports.json +25 -0
  71. package/catalog/frontend/no-dom-html-injection.json +22 -0
  72. package/catalog/frontend/no-eval.json +21 -0
  73. package/catalog/frontend/no-inline-styling.json +25 -0
  74. package/catalog/frontend/no-style-imports.json +24 -0
  75. package/catalog/frontend/no-unsafe-href.json +21 -0
  76. package/catalog/frontend/no-unsafe-target-blank.json +21 -0
  77. package/catalog/frontend/router-links.json +22 -0
  78. package/catalog/frontend/token-styled-elements.json +25 -0
  79. package/catalog/schema.json +95 -0
  80. package/corpus/PENDING.json +4 -0
  81. package/corpus/RESIDUALS.json +21 -0
  82. package/corpus/backend/alembic_downgrades_not_empty/compliant-01/modules/notes/migrations/versions/0001_change.py +6 -0
  83. package/corpus/backend/alembic_downgrades_not_empty/violation-01/modules/notes/migrations/versions/0001_change.py +6 -0
  84. package/corpus/backend/base_query_not_overridden/compliant-01/modules/notes/service.py +8 -0
  85. package/corpus/backend/base_query_not_overridden/compliant-02/modules/notes/service.py +8 -0
  86. package/corpus/backend/base_query_not_overridden/violation-01/modules/notes/service.py +8 -0
  87. package/corpus/backend/base_query_not_overridden/violation-02/expected-findings.json +7 -0
  88. package/corpus/backend/base_query_not_overridden/violation-02/modules/notes/service.py +8 -0
  89. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/models.py +6 -0
  90. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/module.py +7 -0
  91. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/router.py +3 -0
  92. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/schemas.py +10 -0
  93. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/service.py +5 -0
  94. package/corpus/backend/canonical_module_shape/violation-01/modules/notes/router.py +3 -0
  95. package/corpus/backend/datetime_columns_are_timezone_aware/compliant-01/modules/notes/models.py +12 -0
  96. package/corpus/backend/datetime_columns_are_timezone_aware/violation-01/modules/notes/models.py +12 -0
  97. package/corpus/backend/datetime_columns_are_timezone_aware/violation-02/modules/notes/models.py +13 -0
  98. package/corpus/backend/datetime_columns_are_timezone_aware/violation-03/modules/notes/models.py +15 -0
  99. package/corpus/backend/escape_hatch_budget/compliant-01/escape-hatch-budget.json +3 -0
  100. package/corpus/backend/escape_hatch_budget/compliant-01/modules/notes/service.py +2 -0
  101. package/corpus/backend/escape_hatch_budget/compliant-02/escape-hatch-budget.json +1 -0
  102. package/corpus/backend/escape_hatch_budget/compliant-02/modules/notes/service.py +4 -0
  103. package/corpus/backend/escape_hatch_budget/violation-01/escape-hatch-budget.json +1 -0
  104. package/corpus/backend/escape_hatch_budget/violation-01/modules/notes/service.py +2 -0
  105. package/corpus/backend/escape_hatch_budget/violation-02/escape-hatch-budget.json +3 -0
  106. package/corpus/backend/escape_hatch_budget/violation-02/modules/notes/service.py +4 -0
  107. package/corpus/backend/events_reference_catalog/compliant-01/modules/notes/module.py +9 -0
  108. package/corpus/backend/events_reference_catalog/violation-01/modules/notes/module.py +8 -0
  109. package/corpus/backend/input_schemas_exclude_managed_columns/compliant-01/modules/notes/schemas.py +6 -0
  110. package/corpus/backend/input_schemas_exclude_managed_columns/violation-01/expected-findings.json +7 -0
  111. package/corpus/backend/input_schemas_exclude_managed_columns/violation-01/modules/notes/schemas.py +9 -0
  112. package/corpus/backend/input_schemas_exclude_managed_columns/violation-02/expected-findings.json +12 -0
  113. package/corpus/backend/input_schemas_exclude_managed_columns/violation-02/modules/notes/router.py +14 -0
  114. package/corpus/backend/input_str_fields_have_max_length/compliant-01/modules/notes/schemas.py +4 -0
  115. package/corpus/backend/input_str_fields_have_max_length/violation-01/modules/notes/schemas.py +2 -0
  116. package/corpus/backend/jobs_reference_catalog/compliant-01/modules/notes/service.py +5 -0
  117. package/corpus/backend/jobs_reference_catalog/violation-01/modules/notes/service.py +2 -0
  118. package/corpus/backend/list_routes_paginate/compliant-01/modules/notes/router.py +6 -0
  119. package/corpus/backend/list_routes_paginate/violation-01/modules/notes/router.py +3 -0
  120. package/corpus/backend/modules_declare_policy/compliant-01/modules/notes/module.py +7 -0
  121. package/corpus/backend/modules_declare_policy/violation-01/modules/notes/module.py +6 -0
  122. package/corpus/backend/mutations_emit_audit/compliant-01/modules/notes/service.py +9 -0
  123. package/corpus/backend/mutations_emit_audit/violation-01/modules/notes/service.py +4 -0
  124. package/corpus/backend/mutations_require_write_role/compliant-01/modules/notes/module.py +7 -0
  125. package/corpus/backend/mutations_require_write_role/compliant-01/modules/notes/router.py +3 -0
  126. package/corpus/backend/mutations_require_write_role/compliant-02/modules/notes/module.py +7 -0
  127. package/corpus/backend/mutations_require_write_role/compliant-02/modules/notes/router.py +3 -0
  128. package/corpus/backend/mutations_require_write_role/violation-01/expected-findings.json +7 -0
  129. package/corpus/backend/mutations_require_write_role/violation-01/modules/notes/module.py +7 -0
  130. package/corpus/backend/mutations_require_write_role/violation-01/modules/notes/router.py +3 -0
  131. package/corpus/backend/mutations_require_write_role/violation-02/expected-findings.json +7 -0
  132. package/corpus/backend/mutations_require_write_role/violation-02/modules/notes/module.py +7 -0
  133. package/corpus/backend/mutations_require_write_role/violation-02/modules/notes/router.py +5 -0
  134. package/corpus/backend/mutations_require_write_role/violation-03/expected-findings.json +7 -0
  135. package/corpus/backend/mutations_require_write_role/violation-03/modules/notes/module.py +10 -0
  136. package/corpus/backend/mutations_require_write_role/violation-03/modules/notes/router.py +3 -0
  137. package/corpus/backend/no_adhoc_background_runtime/compliant-01/modules/notes/service.py +9 -0
  138. package/corpus/backend/no_adhoc_background_runtime/violation-01/modules/notes/service.py +5 -0
  139. package/corpus/backend/no_adhoc_background_runtime/violation-02/modules/notes/service.py +3 -0
  140. package/corpus/backend/no_adhoc_config_decrypt/compliant-01/modules/billing/service.py +2 -0
  141. package/corpus/backend/no_adhoc_config_decrypt/violation-01/modules/billing/service.py +2 -0
  142. package/corpus/backend/no_adhoc_logging_config/compliant-01/modules/notes/service.py +3 -0
  143. package/corpus/backend/no_adhoc_logging_config/violation-01/modules/notes/service.py +3 -0
  144. package/corpus/backend/no_adhoc_middleware/compliant-01/main.py +3 -0
  145. package/corpus/backend/no_adhoc_middleware/violation-01/main.py +4 -0
  146. package/corpus/backend/no_adhoc_middleware/violation-02/main.py +5 -0
  147. package/corpus/backend/no_adhoc_permission_literals/compliant-01/modules/notes/module.py +3 -0
  148. package/corpus/backend/no_adhoc_permission_literals/compliant-02/modules/notes/router.py +5 -0
  149. package/corpus/backend/no_adhoc_permission_literals/violation-01/expected-findings.json +7 -0
  150. package/corpus/backend/no_adhoc_permission_literals/violation-01/modules/notes/module.py +1 -0
  151. package/corpus/backend/no_adhoc_permission_literals/violation-02/expected-findings.json +7 -0
  152. package/corpus/backend/no_adhoc_permission_literals/violation-02/modules/notes/router.py +2 -0
  153. package/corpus/backend/no_adhoc_permission_literals/violation-03/expected-findings.json +12 -0
  154. package/corpus/backend/no_adhoc_permission_literals/violation-03/modules/notes/module.py +10 -0
  155. package/corpus/backend/no_app_instantiation/compliant-01/main.py +3 -0
  156. package/corpus/backend/no_app_instantiation/violation-01/main.py +3 -0
  157. package/corpus/backend/no_blocking_sleep/compliant-01/modules/notes/service.py +2 -0
  158. package/corpus/backend/no_blocking_sleep/violation-01/modules/notes/service.py +5 -0
  159. package/corpus/backend/no_cross_module_imports/compliant-01/modules/a/service.py +1 -0
  160. package/corpus/backend/no_cross_module_imports/violation-01/modules/a/service.py +1 -0
  161. package/corpus/backend/no_cross_module_imports/violation-02/modules/a/service.py +1 -0
  162. package/corpus/backend/no_dependency_overrides/compliant-01/main.py +12 -0
  163. package/corpus/backend/no_dependency_overrides/violation-01/main.py +11 -0
  164. package/corpus/backend/no_destructive_migrations/compliant-01/modules/notes/migrations/versions/0001_change.py +3 -0
  165. package/corpus/backend/no_destructive_migrations/compliant-02/modules/notes/migrations/versions/0001_change.py +8 -0
  166. package/corpus/backend/no_destructive_migrations/violation-01/expected-findings.json +7 -0
  167. package/corpus/backend/no_destructive_migrations/violation-01/modules/notes/migrations/versions/0001_change.py +2 -0
  168. package/corpus/backend/no_destructive_migrations/violation-02/expected-findings.json +7 -0
  169. package/corpus/backend/no_destructive_migrations/violation-02/modules/notes/migrations/versions/0001_change.py +2 -0
  170. package/corpus/backend/no_destructive_migrations/violation-03/expected-findings.json +7 -0
  171. package/corpus/backend/no_destructive_migrations/violation-03/modules/notes/migrations/versions/0001_change.py +3 -0
  172. package/corpus/backend/no_destructive_migrations/violation-04/expected-findings.json +12 -0
  173. package/corpus/backend/no_destructive_migrations/violation-04/modules/notes/migrations/versions/0001_change.py +6 -0
  174. package/corpus/backend/no_destructive_migrations/violation-05/expected-findings.json +12 -0
  175. package/corpus/backend/no_destructive_migrations/violation-05/modules/notes/migrations/versions/0001_change.py +3 -0
  176. package/corpus/backend/no_dynamic_sql/compliant-01/modules/notes/service.py +1 -0
  177. package/corpus/backend/no_dynamic_sql/compliant-02/modules/notes/service.py +20 -0
  178. package/corpus/backend/no_dynamic_sql/violation-01/modules/notes/service.py +2 -0
  179. package/corpus/backend/no_dynamic_sql/violation-02/modules/notes/service.py +2 -0
  180. package/corpus/backend/no_dynamic_sql/violation-03/modules/notes/service.py +7 -0
  181. package/corpus/backend/no_dynamic_sql/violation-04/modules/notes/service.py +16 -0
  182. package/corpus/backend/no_empty_tests/compliant-01/tests/test_notes.py +2 -0
  183. package/corpus/backend/no_empty_tests/violation-01/tests/test_notes.py +2 -0
  184. package/corpus/backend/no_eval_or_exec/compliant-01/modules/notes/service.py +2 -0
  185. package/corpus/backend/no_eval_or_exec/violation-01/modules/notes/service.py +2 -0
  186. package/corpus/backend/no_hardcoded_credentials/compliant-01/modules/billing/service.py +3 -0
  187. package/corpus/backend/no_hardcoded_credentials/compliant-02/modules/billing/service.py +18 -0
  188. package/corpus/backend/no_hardcoded_credentials/violation-01/expected-findings.json +7 -0
  189. package/corpus/backend/no_hardcoded_credentials/violation-01/modules/billing/service.py +2 -0
  190. package/corpus/backend/no_hardcoded_credentials/violation-02/expected-findings.json +7 -0
  191. package/corpus/backend/no_hardcoded_credentials/violation-02/modules/billing/service.py +1 -0
  192. package/corpus/backend/no_hardcoded_credentials/violation-03/expected-findings.json +22 -0
  193. package/corpus/backend/no_hardcoded_credentials/violation-03/modules/billing/service.py +11 -0
  194. package/corpus/backend/no_hardcoded_credentials/violation-04/expected-findings.json +12 -0
  195. package/corpus/backend/no_hardcoded_credentials/violation-04/modules/billing/service.py +8 -0
  196. package/corpus/backend/no_hardcoded_credentials/violation-05/expected-findings.json +12 -0
  197. package/corpus/backend/no_hardcoded_credentials/violation-05/modules/billing/service.py +4 -0
  198. package/corpus/backend/no_internal_imports/compliant-01/modules/notes/service.py +1 -0
  199. package/corpus/backend/no_internal_imports/violation-01/modules/notes/service.py +1 -0
  200. package/corpus/backend/no_manual_actor_stamping/compliant-01/modules/notes/service.py +5 -0
  201. package/corpus/backend/no_manual_actor_stamping/compliant-02/modules/notes/schemas.py +12 -0
  202. package/corpus/backend/no_manual_actor_stamping/violation-01/modules/notes/service.py +3 -0
  203. package/corpus/backend/no_manual_actor_stamping/violation-02/expected-findings.json +7 -0
  204. package/corpus/backend/no_manual_actor_stamping/violation-02/modules/notes/service.py +4 -0
  205. package/corpus/backend/no_manual_ownership_checks/compliant-01/modules/journals/service.py +3 -0
  206. package/corpus/backend/no_manual_ownership_checks/compliant-02/modules/journals/schemas.py +11 -0
  207. package/corpus/backend/no_manual_ownership_checks/violation-01/modules/journals/service.py +3 -0
  208. package/corpus/backend/no_manual_ownership_checks/violation-02/expected-findings.json +7 -0
  209. package/corpus/backend/no_manual_ownership_checks/violation-02/modules/journals/service.py +9 -0
  210. package/corpus/backend/no_manual_ownership_checks/violation-03/expected-findings.json +7 -0
  211. package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/jobs.py +1 -0
  212. package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/models.py +7 -0
  213. package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/module.py +11 -0
  214. package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/service.py +7 -0
  215. package/corpus/backend/no_manual_scope_filtering/compliant-01/modules/notes/service.py +8 -0
  216. package/corpus/backend/no_manual_scope_filtering/compliant-02/modules/notes/schemas.py +12 -0
  217. package/corpus/backend/no_manual_scope_filtering/violation-01/modules/notes/service.py +2 -0
  218. package/corpus/backend/no_manual_scope_filtering/violation-02/modules/notes/service.py +2 -0
  219. package/corpus/backend/no_manual_scope_filtering/violation-03/expected-findings.json +7 -0
  220. package/corpus/backend/no_manual_scope_filtering/violation-03/modules/notes/service.py +7 -0
  221. package/corpus/backend/no_manual_table_schema/compliant-01/modules/notes/models.py +6 -0
  222. package/corpus/backend/no_manual_table_schema/violation-01/modules/notes/models.py +8 -0
  223. package/corpus/backend/no_manual_version_assignment/compliant-01/modules/notes/service.py +5 -0
  224. package/corpus/backend/no_manual_version_assignment/violation-01/modules/notes/service.py +3 -0
  225. package/corpus/backend/no_manual_version_assignment/violation-02/modules/notes/service.py +3 -0
  226. package/corpus/backend/no_manual_version_assignment/violation-03/modules/notes/service.py +3 -0
  227. package/corpus/backend/no_mutable_default_args/compliant-01/modules/notes/service.py +3 -0
  228. package/corpus/backend/no_mutable_default_args/violation-01/modules/notes/service.py +2 -0
  229. package/corpus/backend/no_naive_datetime/compliant-01/modules/notes/service.py +2 -0
  230. package/corpus/backend/no_naive_datetime/violation-01/modules/notes/service.py +2 -0
  231. package/corpus/backend/no_naive_datetime/violation-02/modules/notes/service.py +2 -0
  232. package/corpus/backend/no_oversized_python_files/compliant-01/modules/notes/service.py +9 -0
  233. package/corpus/backend/no_oversized_python_files/violation-01/modules/notes/service.py +530 -0
  234. package/corpus/backend/no_print/compliant-01/modules/notes/service.py +7 -0
  235. package/corpus/backend/no_print/violation-01/modules/notes/service.py +2 -0
  236. package/corpus/backend/no_raw_app_routes/compliant-01/main.py +12 -0
  237. package/corpus/backend/no_raw_app_routes/violation-01/main.py +10 -0
  238. package/corpus/backend/no_raw_app_routes/violation-02/main.py +16 -0
  239. package/corpus/backend/no_raw_app_routes/violation-03/main.py +12 -0
  240. package/corpus/backend/no_raw_app_routes/violation-04/main.py +10 -0
  241. package/corpus/backend/no_raw_app_routes/violation-05/main.py +12 -0
  242. package/corpus/backend/no_raw_app_routes/violation-06/main.py +10 -0
  243. package/corpus/backend/no_raw_connection_access/compliant-01/modules/notes/service.py +5 -0
  244. package/corpus/backend/no_raw_connection_access/violation-01/modules/notes/service.py +2 -0
  245. package/corpus/backend/no_raw_connection_access/violation-02/modules/notes/service.py +2 -0
  246. package/corpus/backend/no_raw_file_references/compliant-01/modules/notes/models.py +10 -0
  247. package/corpus/backend/no_raw_file_references/violation-01/modules/notes/models.py +9 -0
  248. package/corpus/backend/no_raw_outbound_http/compliant-01/modules/notes/service.py +2 -0
  249. package/corpus/backend/no_raw_outbound_http/compliant-03/modules/notes/service.py +12 -0
  250. package/corpus/backend/no_raw_outbound_http/violation-01/expected-findings.json +7 -0
  251. package/corpus/backend/no_raw_outbound_http/violation-01/modules/notes/service.py +1 -0
  252. package/corpus/backend/no_raw_outbound_http/violation-02/expected-findings.json +7 -0
  253. package/corpus/backend/no_raw_outbound_http/violation-02/modules/notes/service.py +1 -0
  254. package/corpus/backend/no_raw_outbound_http/violation-03/expected-findings.json +7 -0
  255. package/corpus/backend/no_raw_outbound_http/violation-03/modules/notes/service.py +1 -0
  256. package/corpus/backend/no_raw_outbound_http/violation-04/expected-findings.json +12 -0
  257. package/corpus/backend/no_raw_outbound_http/violation-04/modules/notes/service.py +7 -0
  258. package/corpus/backend/no_raw_outbound_http/violation-05/expected-findings.json +12 -0
  259. package/corpus/backend/no_raw_outbound_http/violation-05/modules/notes/service.py +5 -0
  260. package/corpus/backend/no_raw_outbound_http/violation-06/expected-findings.json +12 -0
  261. package/corpus/backend/no_raw_outbound_http/violation-06/modules/notes/service.py +2 -0
  262. package/corpus/backend/no_raw_session_construction/compliant-01/modules/notes/service.py +3 -0
  263. package/corpus/backend/no_raw_session_construction/violation-01/modules/notes/service.py +3 -0
  264. package/corpus/backend/no_star_imports/compliant-01/modules/notes/service.py +1 -0
  265. package/corpus/backend/no_star_imports/violation-01/modules/notes/service.py +1 -0
  266. package/corpus/backend/no_todo_fixme/compliant-01/modules/notes/service.py +3 -0
  267. package/corpus/backend/no_todo_fixme/violation-01/modules/notes/service.py +3 -0
  268. package/corpus/backend/no_unique_columns_on_soft_delete_models/compliant-01/modules/notes/models.py +17 -0
  269. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-01/expected-findings.json +7 -0
  270. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-01/modules/notes/models.py +6 -0
  271. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-02/expected-findings.json +7 -0
  272. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-02/modules/notes/models.py +13 -0
  273. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-03/expected-findings.json +7 -0
  274. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-03/modules/notes/models.py +16 -0
  275. package/corpus/backend/offset_queries_declare_ordering/compliant-01/modules/notes/service.py +4 -0
  276. package/corpus/backend/offset_queries_declare_ordering/violation-01/modules/notes/service.py +2 -0
  277. package/corpus/backend/path_id_params_are_uuid/compliant-01/modules/notes/router.py +6 -0
  278. package/corpus/backend/path_id_params_are_uuid/violation-01/modules/notes/router.py +6 -0
  279. package/corpus/backend/policy_refs_resolve/compliant-01/modules/notes/module.py +7 -0
  280. package/corpus/backend/policy_refs_resolve/violation-01/modules/notes/module.py +8 -0
  281. package/corpus/backend/public_modules_are_read_only/compliant-01/modules/notes/module.py +7 -0
  282. package/corpus/backend/public_modules_are_read_only/compliant-01/modules/notes/router.py +3 -0
  283. package/corpus/backend/public_modules_are_read_only/violation-01/expected-findings.json +7 -0
  284. package/corpus/backend/public_modules_are_read_only/violation-01/modules/notes/module.py +7 -0
  285. package/corpus/backend/public_modules_are_read_only/violation-01/modules/notes/router.py +3 -0
  286. package/corpus/backend/public_modules_are_read_only/violation-02/expected-findings.json +7 -0
  287. package/corpus/backend/public_modules_are_read_only/violation-02/modules/notes/module.py +7 -0
  288. package/corpus/backend/public_modules_are_read_only/violation-02/modules/notes/router.py +5 -0
  289. package/corpus/backend/reads_use_base_query/compliant-01/modules/notes/models.py +6 -0
  290. package/corpus/backend/reads_use_base_query/compliant-01/modules/notes/service.py +8 -0
  291. package/corpus/backend/reads_use_base_query/compliant-02/modules/notes/models.py +9 -0
  292. package/corpus/backend/reads_use_base_query/compliant-02/modules/notes/service.py +9 -0
  293. package/corpus/backend/reads_use_base_query/violation-01/modules/notes/models.py +6 -0
  294. package/corpus/backend/reads_use_base_query/violation-01/modules/notes/service.py +9 -0
  295. package/corpus/backend/reads_use_base_query/violation-02/expected-findings.json +7 -0
  296. package/corpus/backend/reads_use_base_query/violation-02/modules/notes/models.py +6 -0
  297. package/corpus/backend/reads_use_base_query/violation-02/modules/notes/service.py +8 -0
  298. package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/models.py +6 -0
  299. package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/router.py +3 -0
  300. package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/schemas.py +5 -0
  301. package/corpus/backend/response_model_not_table_model/violation-01/modules/notes/models.py +6 -0
  302. package/corpus/backend/response_model_not_table_model/violation-01/modules/notes/router.py +3 -0
  303. package/corpus/backend/routes_declare_response_model/compliant-01/modules/notes/router.py +8 -0
  304. package/corpus/backend/routes_declare_response_model/violation-01/modules/notes/router.py +3 -0
  305. package/corpus/backend/safe_methods_are_read_only/compliant-01/modules/notes/router.py +6 -0
  306. package/corpus/backend/safe_methods_are_read_only/compliant-02/modules/notes/router.py +5 -0
  307. package/corpus/backend/safe_methods_are_read_only/violation-01/expected-findings.json +7 -0
  308. package/corpus/backend/safe_methods_are_read_only/violation-01/modules/notes/router.py +3 -0
  309. package/corpus/backend/safe_methods_are_read_only/violation-02/expected-findings.json +7 -0
  310. package/corpus/backend/safe_methods_are_read_only/violation-02/modules/notes/router.py +5 -0
  311. package/corpus/backend/safe_methods_are_read_only/violation-03/expected-findings.json +7 -0
  312. package/corpus/backend/safe_methods_are_read_only/violation-03/modules/notes/router.py +3 -0
  313. package/corpus/backend/schemas_exclude_sensitive_fields/compliant-01/modules/users/schemas.py +5 -0
  314. package/corpus/backend/schemas_exclude_sensitive_fields/compliant-02/modules/accounts/schemas.py +12 -0
  315. package/corpus/backend/schemas_exclude_sensitive_fields/violation-01/expected-findings.json +7 -0
  316. package/corpus/backend/schemas_exclude_sensitive_fields/violation-01/modules/users/schemas.py +3 -0
  317. package/corpus/backend/schemas_exclude_sensitive_fields/violation-02/expected-findings.json +7 -0
  318. package/corpus/backend/schemas_exclude_sensitive_fields/violation-02/modules/connectors/router.py +8 -0
  319. package/corpus/backend/schemas_exclude_sensitive_fields/violation-03/expected-findings.json +17 -0
  320. package/corpus/backend/schemas_exclude_sensitive_fields/violation-03/modules/integrations/schemas.py +7 -0
  321. package/corpus/backend/session_imported_from_sqlmodel/compliant-01/modules/notes/service.py +5 -0
  322. package/corpus/backend/session_imported_from_sqlmodel/violation-01/modules/notes/service.py +5 -0
  323. package/corpus/backend/table_models_use_base_table/compliant-01/modules/notes/models.py +6 -0
  324. package/corpus/backend/table_models_use_base_table/violation-01/modules/notes/models.py +5 -0
  325. package/corpus/backend/tables_have_migrations/compliant-01/modules/notes/migrations/versions/0a1b2c3d4e5f_create_notes_tables.py +22 -0
  326. package/corpus/backend/tables_have_migrations/compliant-01/modules/notes/models.py +6 -0
  327. package/corpus/backend/tables_have_migrations/compliant-02/capabilities/ledger/models.py +6 -0
  328. package/corpus/backend/tables_have_migrations/violation-01/expected-findings.json +7 -0
  329. package/corpus/backend/tables_have_migrations/violation-01/modules/notes/models.py +6 -0
  330. package/corpus/backend/tables_have_migrations/violation-02/expected-findings.json +7 -0
  331. package/corpus/backend/tables_have_migrations/violation-02/modules/notes/models.py +10 -0
  332. package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-01/modules/projects/models.py +6 -0
  333. package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-01/modules/projects/service.py +5 -0
  334. package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-02/modules/projects/models.py +8 -0
  335. package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-02/modules/projects/service.py +5 -0
  336. package/corpus/backend/tenant_scoped_models_use_scoped_service/violation-01/modules/projects/models.py +6 -0
  337. package/corpus/backend/tenant_scoped_models_use_scoped_service/violation-01/modules/projects/service.py +5 -0
  338. package/corpus/backend/ungoverned_escape_hatch/compliant-01/modules/notes/service.py +5 -0
  339. package/corpus/backend/ungoverned_escape_hatch/compliant-02/modules/notes/service.py +4 -0
  340. package/corpus/backend/ungoverned_escape_hatch/violation-01/modules/notes/service.py +2 -0
  341. package/corpus/backend/update_schemas_inherit_base_update_schema/compliant-01/modules/notes/schemas.py +2 -0
  342. package/corpus/backend/update_schemas_inherit_base_update_schema/violation-01/modules/notes/schemas.py +2 -0
  343. package/corpus/backend/update_schemas_inherit_base_update_schema/violation-02/modules/notes/schemas.py +5 -0
  344. package/corpus/frontend/escape-hatch/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  345. package/corpus/frontend/escape-hatch/violation-01/src/modules/widgets/Widget.tsx +4 -0
  346. package/corpus/frontend/escape-hatch/violation-02/src/modules/widgets/Widget.tsx +7 -0
  347. package/corpus/frontend/escape-hatch/violation-03/src/modules/widgets/Widget.tsx +7 -0
  348. package/corpus/frontend/generated-client-only/compliant-01/src/modules/widgets/Widget.tsx +6 -0
  349. package/corpus/frontend/generated-client-only/compliant-02/src/modules/widgets/Widget.tsx +21 -0
  350. package/corpus/frontend/generated-client-only/compliant-03/src/modules/widgets/Widget.tsx +7 -0
  351. package/corpus/frontend/generated-client-only/compliant-04/src/modules/widgets/Widget.tsx +17 -0
  352. package/corpus/frontend/generated-client-only/violation-01/src/modules/widgets/Widget.tsx +3 -0
  353. package/corpus/frontend/generated-client-only/violation-02/src/modules/widgets/Widget.tsx +9 -0
  354. package/corpus/frontend/generated-client-only/violation-03/src/modules/widgets/Widget.tsx +14 -0
  355. package/corpus/frontend/generated-client-only/violation-04/src/modules/widgets/Widget.tsx +6 -0
  356. package/corpus/frontend/layout-contract/compliant-01/layout-contract.json +3 -0
  357. package/corpus/frontend/layout-contract/compliant-01/src/modules/widgets/Widget.tsx +8 -0
  358. package/corpus/frontend/layout-contract/violation-01/layout-contract.json +3 -0
  359. package/corpus/frontend/layout-contract/violation-01/src/modules/widgets/Widget.tsx +8 -0
  360. package/corpus/frontend/no-cross-module-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  361. package/corpus/frontend/no-cross-module-imports/violation-01/src/modules/widgets/Widget.tsx +3 -0
  362. package/corpus/frontend/no-deep-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  363. package/corpus/frontend/no-deep-imports/violation-01/src/modules/widgets/Widget.tsx +2 -0
  364. package/corpus/frontend/no-dom-html-injection/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  365. package/corpus/frontend/no-dom-html-injection/compliant-02/src/modules/widgets/Widget.tsx +11 -0
  366. package/corpus/frontend/no-dom-html-injection/violation-01/src/modules/widgets/Widget.tsx +3 -0
  367. package/corpus/frontend/no-dom-html-injection/violation-02/src/modules/widgets/Widget.tsx +7 -0
  368. package/corpus/frontend/no-dom-html-injection/violation-03/src/modules/widgets/Widget.tsx +12 -0
  369. package/corpus/frontend/no-eval/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  370. package/corpus/frontend/no-eval/compliant-02/src/modules/widgets/Widget.tsx +11 -0
  371. package/corpus/frontend/no-eval/violation-01/src/modules/widgets/Widget.tsx +3 -0
  372. package/corpus/frontend/no-eval/violation-02/src/modules/widgets/Widget.tsx +7 -0
  373. package/corpus/frontend/no-eval/violation-03/src/modules/widgets/Widget.tsx +10 -0
  374. package/corpus/frontend/no-eval/violation-04/src/modules/widgets/Widget.tsx +6 -0
  375. package/corpus/frontend/no-inline-styling/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  376. package/corpus/frontend/no-inline-styling/violation-01/src/modules/widgets/Widget.tsx +3 -0
  377. package/corpus/frontend/no-inline-styling/violation-02/src/modules/widgets/Widget.tsx +6 -0
  378. package/corpus/frontend/no-style-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  379. package/corpus/frontend/no-style-imports/violation-01/src/modules/widgets/Widget.tsx +2 -0
  380. package/corpus/frontend/no-unsafe-href/compliant-01/src/modules/widgets/Widget.tsx +3 -0
  381. package/corpus/frontend/no-unsafe-href/violation-01/src/modules/widgets/Widget.tsx +3 -0
  382. package/corpus/frontend/no-unsafe-target-blank/compliant-01/src/modules/widgets/Widget.tsx +3 -0
  383. package/corpus/frontend/no-unsafe-target-blank/violation-01/src/modules/widgets/Widget.tsx +3 -0
  384. package/corpus/frontend/router-links/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  385. package/corpus/frontend/router-links/violation-01/src/modules/widgets/Widget.tsx +3 -0
  386. package/corpus/frontend/token-styled-elements/compliant-01/src/modules/widgets/Widget.tsx +4 -0
  387. package/corpus/frontend/token-styled-elements/compliant-02/src/modules/widgets/Widget.tsx +4 -0
  388. package/corpus/frontend/token-styled-elements/violation-01/src/modules/widgets/Widget.tsx +3 -0
  389. package/findings.schema.json +40 -0
  390. package/package.json +23 -0
  391. package/restricted-surface.json +10 -0
  392. package/scorecard.schema.json +52 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/no_raw_outbound_http",
3
+ "surface": "backend",
4
+ "title": "App modules do not import raw HTTP clients; outbound calls use a capability",
5
+ "intent": "Directly importing an HTTP client library — or the lower-level socket / protocol primitives that reach the same network — makes SSRF protection, allowlists, egress auditing, and timeout policy a per-call-site choice. Outbound traffic belongs behind a declared capability that centralizes those controls. As a security rule this also scans test and migration files inside a module — they are importable code, so they are application surface too.",
6
+ "layer": "static-portable",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_no_raw_outbound_http"
12
+ }
13
+ ],
14
+ "reference": "httpx / requests / urllib.request / urllib3 / aiohttp imports and the socket / http.client escape routes are refused in app modules; outbound calls go through a declared egress capability.",
15
+ "opt_out": "# arch-allow-no-raw-outbound-http: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "Import-form rule: the sanctioned egress capability itself drives an HTTP client at runtime, so client presence and traffic cannot be attributed to an app module from inside the process. Network-level egress policy is deployment configuration, not a per-module framework seam."
19
+ },
20
+ "guide_topic": "capability",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/no_raw_session_construction",
3
+ "surface": "backend",
4
+ "title": "App code never constructs a database session or engine; it uses the injected request session",
5
+ "intent": "The injected request session is where the framework's runtime controls live — the write guard, row scoping, and the audit hooks all ride on the session the framework hands out. A hand-constructed session or engine sits outside every one of those chokepoints by definition: its reads are unscoped, its writes are unaudited and unguarded, and it silently forks the app onto a second connection lifecycle.",
6
+ "layer": "static-portable",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_no_raw_session_construction"
12
+ }
13
+ ],
14
+ "reference": "SessionDep injects the guarded request session; Session(...) / create_engine / sessionmaker construction in app code is refused.",
15
+ "opt_out": "# arch-allow-no-raw-session-construction: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "Nothing in Python lets the framework refuse construction of a third-party class: a hand-built Session or engine never passes through any framework seam -- which is exactly why it must be caught in source. The guarded request session (SessionDep) is the surface the framework does control; an app-constructed one sits outside every chokepoint by definition."
19
+ },
20
+ "guide_topic": "service",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/no_star_imports",
3
+ "surface": "backend",
4
+ "title": "Import names explicitly, never with a wildcard",
5
+ "intent": "A wildcard import pulls an unknown, changeable set of names into a namespace, so the module's real dependency surface is invisible and one upstream rename can silently shadow a local name. Naming each import keeps the dependency graph legible to readers and to the boundary checks that police it.",
6
+ "layer": "static-portable",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_no_star_imports"
12
+ }
13
+ ],
14
+ "reference": "A 'from <module> import *' statement is refused; list the specific names the module uses instead.",
15
+ "opt_out": "# arch-allow-no-star-imports: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "The invariant is a property of the authored import statement. Once the module is loaded the wildcard has already been expanded into concrete bindings, indistinguishable from explicit imports, so no runtime seam can reconstruct which spelling was written — the only faithful check reads the source."
19
+ },
20
+ "guide_topic": "module",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/no_todo_fixme",
3
+ "surface": "backend",
4
+ "title": "No placeholder comments for deferred work",
5
+ "intent": "A TODO, FIXME, HACK, or XXX comment marks unfinished work that ships anyway and is almost never revisited, so the gap it names silently becomes permanent. Finish the behaviour or delete the dead branch — do not leave a note promising a fix that will not come.",
6
+ "layer": "static-portable",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_no_todo_fixme"
12
+ }
13
+ ],
14
+ "reference": "The markers TODO, FIXME, HACK, and XXX are refused when they appear in a real comment token; matching ignores identical text inside strings or docstrings.",
15
+ "opt_out": "# arch-allow-no-todo-fixme: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "The invariant is a property of the authored comments, which the interpreter discards entirely — there is nothing at runtime to observe. Only a source-form check can see the placeholder."
19
+ },
20
+ "guide_topic": "module",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/no_unique_columns_on_soft_delete_models",
3
+ "surface": "backend",
4
+ "title": "Soft-delete models never declare a full-table unique constraint (dead rows block reuse)",
5
+ "intent": "A soft-deleted row stays in the table, so it keeps occupying every full-table unique index: the \"deleted\" value (an email, a slug, a code) can never be used again, surfacing as an inexplicable conflict long after the delete. Scope uniqueness to the live rows with a partial unique index that excludes soft-deleted rows — which this rule accepts — or deactivate instead of deleting.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_no_unique_columns_on_soft_delete_models"
12
+ }
13
+ ],
14
+ "reference": "A partial unique index in __table_args__ (unique=True with postgresql_where / sqlite_where on deleted_at IS NULL) is the accepted shape; the identity user table keeps email unique by deactivating instead of deleting.",
15
+ "opt_out": "# arch-allow-no-unique-columns-on-soft-delete-models: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "The constraint is baked into migration-produced DDL; at runtime the schema already exists, and the failure mode (a dead row blocking reuse) surfaces as the database's own constraint error, mapped to the uniform 409 envelope -- handled, but only preventable when the model is authored."
19
+ },
20
+ "guide_topic": "module",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/offset_queries_declare_ordering",
3
+ "surface": "backend",
4
+ "title": "An offset-paginated query must declare an explicit ordering",
5
+ "intent": "Row order without an explicit ordering clause is undefined, so paging a query by a numeric offset over an unordered result can silently skip or repeat rows between pages. A query that skips a number of rows must also declare a deterministic ordering so the page sequence is stable and every row is seen exactly once.",
6
+ "layer": "static-portable",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_offset_queries_declare_ordering"
12
+ }
13
+ ],
14
+ "reference": "A function that calls .offset(...) but no .order_by(...) is flagged at the offset call; add an explicit ordering, or page through the framework's ordered pagination helper.",
15
+ "opt_out": "# arch-allow-offset-queries-declare-ordering: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "The unordered-offset defect is a property of the authored query expression; the database executes an unordered offset without error, so there is no fail-closed runtime signal, and the ordering guarantee exists only by constraining the source."
19
+ },
20
+ "guide_topic": "service",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/path_id_params_are_uuid",
3
+ "surface": "backend",
4
+ "title": "A route path parameter naming a resource id must be typed as a UUID",
5
+ "intent": "A URL path segment that names a resource id (spelled id or ending in _id) identifies a specific row, and the platform issues UUID identifiers. Typing that path parameter as a UUID rejects a malformed identifier at the request boundary, before it reaches the data layer, instead of letting an untyped or wrongly-typed value through. Only path parameters are in scope; query and body parameters are unaffected.",
6
+ "layer": "static-portable",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_path_id_params_are_uuid"
12
+ }
13
+ ],
14
+ "reference": "A handler parameter that also appears in the route decorator's URL template and is named id or ends in _id must be annotated uuid.UUID (a uuid.UUID attribute or a bare UUID name); a missing or non-UUID annotation is flagged at the handler.",
15
+ "opt_out": "# arch-allow-path-id-params-are-uuid: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "The rule constrains the authored route signature (the declared parameter type), a source-form property of the handler rather than an observable runtime event; the framework does not re-derive intended id-parameter types at mount time, so a mis-typed parameter merely validates against the wrong type with no separate signal that a UUID was intended."
19
+ },
20
+ "guide_topic": "module",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "backend/policy_refs_resolve",
3
+ "surface": "backend",
4
+ "title": "Every typed authority a policy cites resolves in the app's authority registry",
5
+ "intent": "The build-time half of control-plane registry resolution: boot validation already refuses an undeclared authority at runtime; this rule catches the same drift at the gate, before the app ever boots. Any reference that traces to the app's authority registry — via a module alias or a name imported from the registry — must name something the registry actually declares. References the scan cannot trace to the registry (kernel default roles, locally built objects) are left to the runtime check, so the rule stays precise, never heuristic.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_policy_refs_resolve"
12
+ },
13
+ {
14
+ "kind": "runtime",
15
+ "tool": "terp.core",
16
+ "ref": "validation_errors"
17
+ }
18
+ ],
19
+ "reference": "control_plane/permissions.py is the registry; aliased references like perms.BILLING_READ must resolve there, kernel defaults like Roles.EDITOR are left to ControlPlane.validation_errors at boot.",
20
+ "opt_out": "# arch-allow-policy-refs-resolve: <reason>",
21
+ "runtime": {
22
+ "applicability": "required",
23
+ "rationale": "Boot validation refuses an undeclared authority before the app serves (ControlPlane.validation_errors -> BootError); the build-time rule catches the same drift earlier, at the gate."
24
+ },
25
+ "guide_topic": "policy",
26
+ "corpus": true
27
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "backend/public_modules_are_read_only",
3
+ "surface": "backend",
4
+ "title": "A public (unauthenticated) module must not expose a mutating route",
5
+ "intent": "A public policy drops authentication for the whole module, so a mutating route under it is an unauthenticated write — almost always an accident, and the broken-access-control footgun the deny-by-default posture exists to prevent. A genuinely public write (a sign-up / contact form / webhook receiver) is rare and deliberate, so it stays available through the governed escape hatch: a justified opt-out marker ratcheted by the escape-hatch budget, making the unauthenticated write visible and budgeted rather than silent. Gate the writes behind a policy with a write role, or justify the public write explicitly. The runtime half is the boot refusal of a public module that exposes a mutating route unless its policy opts in explicitly with a reason — each layer carries its own justified opt-out.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_public_modules_are_read_only"
12
+ },
13
+ {
14
+ "kind": "runtime",
15
+ "tool": "terp.core",
16
+ "ref": "_validate_public_modules_read_only"
17
+ }
18
+ ],
19
+ "runtime": {
20
+ "applicability": "required"
21
+ },
22
+ "reference": "Policy.public(reason=...) declares the public module; create_app -> _validate_public_modules_read_only refuses public writes unless the policy opts in via Policy.public_write(reason=...).",
23
+ "opt_out": "# arch-allow-public-modules-are-read-only: <reason>",
24
+ "guide_topic": "policy",
25
+ "corpus": true
26
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "backend/reads_use_base_query",
3
+ "surface": "backend",
4
+ "title": "A scope-trait model is read through the composed scoped query, never a raw query built from scratch",
5
+ "intent": "A model that mixes a soft-delete or tenant-scope trait carries row scope. A bespoke read that queries the model directly — instead of building on the composed scoped query — drops that scope, leaking soft-deleted or cross-tenant rows (closing the composition point to overrides did not close a new read method that never calls it). Build reads on the scoped query and the declared filter seam; the request session re-applies the scope to single-entity reads as the runtime backstop, and this rule is the build-time early warning. The one sanctioned raw query — the scoped composition point itself — lives in the framework, not a module, so it is never scanned here.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_reads_use_base_query"
12
+ },
13
+ {
14
+ "kind": "runtime",
15
+ "tool": "terp.core",
16
+ "ref": "apply_row_scope"
17
+ }
18
+ ],
19
+ "reference": "select(Model) on a SoftDeleteMixin / TenantScopedMixin model is refused in modules (ADR 0017, F1); build on base_query() / business_filters(), with apply_row_scope as the runtime backstop.",
20
+ "opt_out": "# arch-allow-reads-use-base-query: <reason>",
21
+ "runtime": {
22
+ "applicability": "required",
23
+ "rationale": "The request session re-scopes user-facing ORM reads idempotently (apply_row_scope): a bespoke select(Model) that skipped base_query() is filtered for soft-delete / registered predicates again at execution, so the leak the rule warns about is closed even when the source pattern slips through."
24
+ },
25
+ "guide_topic": "service",
26
+ "corpus": true
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "backend/response_model_not_table_model",
3
+ "surface": "backend",
4
+ "title": "A route's declared response type is a read DTO, never the persisted table model",
5
+ "intent": "routes_declare_response_model proves a response type is declared; this rule proves it is not the persisted table itself. A declared response type set to a table model — directly or wrapped in a page envelope or collection — serializes the stored row, so a column such as a password hash leaks straight through the boundary. Return a read DTO listing exactly the safe fields instead.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_response_model_not_table_model"
12
+ },
13
+ {
14
+ "kind": "runtime",
15
+ "tool": "terp.core",
16
+ "ref": "_validate_router_response_models"
17
+ }
18
+ ],
19
+ "reference": "response_model set to a table=True model, directly or wrapped in Page[...] / list[...] (the Page[User] footgun), is refused; return a *Read schema on terp.core.BaseSchema.",
20
+ "opt_out": "# arch-allow-response-model-not-table-model: <reason>",
21
+ "runtime": {
22
+ "applicability": "required",
23
+ "rationale": "Boot refuses a route whose response_model is (or wraps) a table=True ORM model (_validate_router_response_models -> BootError), with cross-package and nested-router fidelity a source scan cannot have."
24
+ },
25
+ "guide_topic": "module",
26
+ "corpus": true
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "backend/routes_declare_response_model",
3
+ "surface": "backend",
4
+ "title": "Every content route declares its response type (no bare data out)",
5
+ "intent": "A route with neither a declared response type nor a no-body status code can serialize a bare stored object out of the boundary — whatever the handler happens to return, including columns that were never meant to leave the app. Declaring the response type makes the boundary shape explicit and reviewable; both decorator routes and imperative route registration are checked.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_routes_declare_response_model"
12
+ },
13
+ {
14
+ "kind": "runtime",
15
+ "tool": "terp.core",
16
+ "ref": "_validate_routes_declare_response_model"
17
+ }
18
+ ],
19
+ "reference": "response_model= on @router.<verb>(...) decorators and router.add_api_route(...); a no-body status_code (204/205/304) is the accepted alternative.",
20
+ "opt_out": "# arch-allow-routes-declare-response-model: <reason>",
21
+ "runtime": {
22
+ "applicability": "required",
23
+ "rationale": "At boot, the route scan on the composition seam (the same seam as _validate_router_response_models) walks every composed route \u2014 decorator, imperative add_api_route, and nested included routers alike \u2014 and raises a BootError naming this rule and the route when a content route declares no response_model. No-body 204/205/304 statuses and Response-subclass-annotated non-content routes stay exempt, so the control refuses exactly the routes that would serialize a bare object out of the boundary."
24
+ },
25
+ "guide_topic": "module",
26
+ "corpus": true
27
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "id": "backend/safe_methods_are_read_only",
3
+ "surface": "backend",
4
+ "title": "A handler reachable via a safe HTTP method must not mutate",
5
+ "intent": "The deny-by-default guard derives the required role tier from the HTTP method: a safe method (GET / HEAD / OPTIONS) is authorized against the policy's read requirement, a mutating one against the write requirement. So a handler reachable through a safe method that calls a mutating service method performs a write a read-tier caller cleared — a privilege-tier escape (a viewer triggering an editor/admin write via a read request). This holds for a mixed-method route too: the safe-method invocation runs at the read tier, so a handler that always mutates is flagged (split it, or branch on the method behind a mutating route). Both decorator and imperative route registration are checked. Put the write behind a mutating method so it is authorized at the write tier. The runtime half marks a safe-method request read-only, so the write chokepoint refuses the write.",
6
+ "layer": "black-box",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_safe_methods_are_read_only"
12
+ },
13
+ {
14
+ "kind": "runtime",
15
+ "tool": "terp.core",
16
+ "ref": "build_read_only_request_binder"
17
+ },
18
+ {
19
+ "kind": "black-box",
20
+ "tool": "@terp/conformance",
21
+ "ref": "standard: safe methods observably mutate nothing"
22
+ }
23
+ ],
24
+ "opt_out": "# arch-allow-safe-methods-are-read-only: <reason>",
25
+ "runtime": {
26
+ "applicability": "required"
27
+ },
28
+ "reference": "Mutating BaseService calls (create/update/delete/_save/_remove) in safe-method handlers are flagged; create_app binds safe-method requests read-only (build_read_only_request_binder).",
29
+ "guide_topic": "module",
30
+ "corpus": true
31
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "id": "backend/schemas_exclude_sensitive_fields",
3
+ "surface": "backend",
4
+ "title": "A read / response DTO never exposes a credential-shaped field",
5
+ "intent": "Routes serialize a response DTO out of the boundary, so a field whose name reads like a secret — a password, a password hash, or a *secret / *api_key / *token suffix — would leak the credential to every caller. A response DTO is a schema in the response role: a read/response base schema or any class wired as a route's declared response type (so an input DTO mistakenly reused as a response is caught), excluding the inputs that are only ever request bodies (a client supplies a password) and table models (a table may store the hash). Plain helper classes are not policed. This guards the gap response_model_not_table_model leaves: a hand-rolled read DTO that copies the stored hash. (Version counters are integers, not secrets.)",
6
+ "layer": "black-box",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_schemas_exclude_sensitive_fields"
12
+ },
13
+ {
14
+ "kind": "black-box",
15
+ "tool": "@terp/conformance",
16
+ "ref": "standard: responses never expose credential-shaped fields"
17
+ },
18
+ {
19
+ "kind": "runtime",
20
+ "tool": "terp.core",
21
+ "ref": "_validate_schemas_exclude_sensitive_fields"
22
+ }
23
+ ],
24
+ "reference": "BaseSchema / BaseUpdateSchema models and response_model= classes are scanned for password / hashed_password / *secret / *api_key / *token field names; request-body-only inputs and table=True models are excluded.",
25
+ "opt_out": "# arch-allow-schemas-exclude-sensitive-fields: <reason>",
26
+ "runtime": {
27
+ "applicability": "required",
28
+ "rationale": "At boot, the route scan on the composition seam (the same seam as _validate_router_response_models) walks every composed route \u2014 decorator, imperative add_api_route, and nested included routers alike \u2014 and raises a BootError naming this rule and the route when a declared response DTO carries a credential-shaped field (the same underscore-delimited word match, with the token_version / version / trailing-token exclusions, as the build-time rule; framework-vetted terp.* DTOs exempt). The black-box probe remains a conformance-suite observation on top of this in-app fail-closed control."
29
+ },
30
+ "guide_topic": "module",
31
+ "corpus": true
32
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/session_imported_from_sqlmodel",
3
+ "surface": "backend",
4
+ "title": "The ORM session type is imported from the framework's canonical source, never the underlying library",
5
+ "intent": "The framework standardises on one session type everywhere — the injected request session, the service layer, the write guard, and the migrations all speak the same type, re-exported from one canonical source. Importing the session type from the underlying ORM library instead quietly forks the app onto a second session type, so the rule names the one canonical import. (Constructing a session is separately banned by no_raw_session_construction — this only fixes the spelling.)",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_session_imported_from_sqlmodel"
12
+ }
13
+ ],
14
+ "reference": "from sqlmodel import Session is canonical (SQLModel re-exports SQLAlchemy's); imports from sqlalchemy / sqlalchemy.orm are refused.",
15
+ "opt_out": "# arch-allow-session-imported-from-sqlmodel: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "Which module a symbol was imported from is erased at runtime: sqlmodel's Session subclasses SQLAlchemy's, and the object in play is the framework-provided session either way. The typed exec/scoping benefits are carried by the session the framework hands out."
19
+ },
20
+ "guide_topic": "service",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/table_models_use_base_table",
3
+ "surface": "backend",
4
+ "title": "Every ORM table model inherits the platform base table (no bare tables)",
5
+ "intent": "A table model that skips the platform base table bypasses the framework's managed identity, timestamps, and optimistic-concurrency version — a model living outside the control-plane contract, which every chokepoint (the service layer, audit, concurrency) presupposes.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_table_models_use_base_table"
12
+ }
13
+ ],
14
+ "reference": "BaseTable supplies the UUID id, created_at/updated_at and the OCC version; a bare SQLModel table=True model is refused.",
15
+ "opt_out": "# arch-allow-table-models-use-base-table: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "Inheritance is an authoring-shape fact, and Python generics are not runtime-enforced. The chokepoint contracts that matter at runtime (OCC version, managed columns, audit identity) presuppose BaseTable rather than police it -- a bare SQLModel table simply never fits the service seam."
19
+ },
20
+ "guide_topic": "module",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "backend/tables_have_migrations",
3
+ "surface": "backend",
4
+ "title": "Every app module that defines a table model ships a packaged migration history",
5
+ "intent": "A deployed Terp app builds its schema from packaged migrations, never from dev-time schema auto-creation — so a module that declares a table model but ships no migration revision would deploy with that table missing, and the first request would fail on a nonexistent table. This rule fails the build first, and the running system's migration guard refuses the same violation at boot (the two halves of the migration control). Generate and commit the module's migration revision.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_tables_have_migrations"
12
+ },
13
+ {
14
+ "kind": "runtime",
15
+ "tool": "terp.migrations",
16
+ "ref": "assert_no_missing_histories"
17
+ }
18
+ ],
19
+ "reference": "terp migrate make <name> generates the module's migrations/versions/ revision; assert_migrations_current is the production boot guard (it runs assert_no_missing_histories, so a table-owning package without any history refuses to boot), create_all the refused dev shortcut.",
20
+ "opt_out": "# arch-allow-tables-have-migrations: <reason>",
21
+ "runtime": {
22
+ "applicability": "required",
23
+ "rationale": "The boot guard refuses the standalone missing-history case this rule exists for: a declared package that defines table models but ships no migration history at all raises a MissingMigrationsError naming the packages -- fail closed at boot, before a request can hit a table that was never created. The pending-revisions half of the same guard keeps covering histories that exist but are behind head."
24
+ },
25
+ "guide_topic": "migrations",
26
+ "corpus": true
27
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "backend/tenant_scoped_models_use_scoped_service",
3
+ "surface": "backend",
4
+ "title": "A tenant-scoped model's service extends the tenant-scoped service base",
5
+ "intent": "This makes tenant isolation structural on the write side: reads of a tenant-scoped model are already filtered centrally by the registered tenant scope predicate, but the tenant-scoped service base is what stamps the tenant on create — so a plain service (which would insert an unstamped, never-visible row) is rejected at build time.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_tenant_scoped_models_use_scoped_service"
12
+ }
13
+ ],
14
+ "reference": "A TenantScopedMixin model's service extends TenantScopedService (stamps tenant_id on create, ADR 0017); a plain BaseService is refused.",
15
+ "opt_out": "# arch-allow-tenant-scoped-models-use-scoped-service: <reason>",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "Service lineage is an authoring-shape fact. The leak-relevant runtime control is the registered row-scope predicate (apply_row_scope, declared under the scoping rules), which filters reads for the mixin regardless of which service touched the row; extending TenantScopedService is what makes writes stamp tenant_id so rows stay reachable in their tenant."
19
+ },
20
+ "guide_topic": "tenancy",
21
+ "corpus": true
22
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "id": "backend/ungoverned_escape_hatch",
3
+ "surface": "backend",
4
+ "title": "The fail-closed ungoverned-opt-out condition, as structured violations",
5
+ "intent": "An opt-out marker the governance contract does not honour is itself a violation, reported in-band as structured findings (one per marker line) rather than crashing the harness: a marker used with no escape-hatch budget governing it, and a marker whose missing justification means it never suppressed anything. Either way the fix is to govern the opt-out — add the budget and the reason — never to silently honour it.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "ungoverned_marker_violations"
12
+ }
13
+ ],
14
+ "reference": "assert_app_clean raises on an ungoverned # arch-allow-* marker; ungoverned_marker_violations projects the budget-less condition and _apply_suppressions re-reports an unjustified marker under the same rule, for terp check --format json. The rule carries no opt_out: governance cannot be waived by the mechanism it governs.",
15
+ "runtime": {
16
+ "applicability": "not-applicable",
17
+ "rationale": "A justification marker is a source comment; compiled bytecode retains neither the marker nor its reason. The escape-hatch contract governs authored source at the gate."
18
+ },
19
+ "guide_topic": "rules",
20
+ "corpus": true
21
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "id": "backend/update_schemas_inherit_base_update_schema",
3
+ "surface": "backend",
4
+ "title": "An update request contract must require the optimistic-concurrency token",
5
+ "intent": "Optimistic concurrency only protects a row when the client echoes the version it loaded and the update path checks it before writing. That guarantee hinges on the update request contract demanding the token as a required field: an update contract that omits it lets a client send a blind write, so the check has nothing to compare and a concurrent edit is silently overwritten (a lost update). Every update request contract must inherit the shared concurrency-bearing base that makes the token a required field, rather than an ordinary contract that leaves it out. The token is not redeclared on the contract — it is a managed field that arrives by inheritance.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "terp.arch",
11
+ "ref": "check_update_schemas_inherit_base_update_schema"
12
+ },
13
+ {
14
+ "kind": "runtime",
15
+ "tool": "terp.core",
16
+ "ref": "BaseUpdateSchema"
17
+ },
18
+ {
19
+ "kind": "runtime",
20
+ "tool": "terp.core",
21
+ "ref": "StaleDataError"
22
+ }
23
+ ],
24
+ "runtime": {
25
+ "applicability": "required",
26
+ "rationale": "Two runtime halves back the source contract: BaseUpdateSchema makes the version token a required field, so a client that omits it is rejected at request validation (422); and BaseService.update matches the echoed token against the loaded row and raises a conflict (409) when they diverge. The build-time rule is the early-warning half — it ensures every update DTO actually inherits that base so it opts into both runtime checks, since a source scan is where an off-base update DTO is visible before it ever reaches a request."
27
+ },
28
+ "opt_out": "# arch-allow-update-schemas-inherit-base-update-schema: <reason>",
29
+ "reference": "class NoteUpdate(BaseSchema) is refused; class NoteUpdate(BaseUpdateSchema) (directly or transitively) is compliant, as is a class wired as build_crud_router(update_schema=...). BaseUpdateSchema supplies the required version field; BaseService.update checks it.",
30
+ "guide_topic": "module",
31
+ "corpus": true
32
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "id": "frontend/escape-hatch",
3
+ "surface": "frontend",
4
+ "title": "Every opt-out marker is justified and governed by the budget ratchet",
5
+ "intent": "The one opt-out is a justified inline marker naming the rule; an unjustified marker is itself an error, and marker counts must exactly match the app's checked-in escape-hatch budget so opt-outs stay visible and can only shrink.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "@terp/eslint-boundaries",
11
+ "ref": "terp/escape-hatch",
12
+ "reported_as": "terp/escape-hatch"
13
+ }
14
+ ],
15
+ "reference": "// terp-allow-<rule>: <reason> markers reconciled against the app's checked-in escape-hatch-budget.json (ADR 0059). The rule carries no opt_out: governance cannot be waived by the mechanism it governs.",
16
+ "runtime": {
17
+ "applicability": "not-applicable",
18
+ "rationale": "Markers and budgets are source/repository artifacts; the built bundle retains neither. The controls the markers opt out of keep their own runtime halves regardless."
19
+ },
20
+ "corpus": true
21
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "frontend/generated-client-only",
3
+ "surface": "frontend",
4
+ "title": "All network egress goes through the generated, typed client",
5
+ "intent": "Raw fetch / XMLHttpRequest / WebSocket / EventSource (and navigator.sendBeacon) bypass the app's single audited, typed egress path. One client means one place for auth, errors, and contract types. The refused globals and member calls are declared in restricted-surface.json (restrictedGlobals, restrictedMemberCalls).",
6
+ "layer": "static-portable",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "@terp/eslint-boundaries",
11
+ "ref": "BOUNDARY_SPEC.restrictedGlobals",
12
+ "reported_as": "no-restricted-globals"
13
+ }
14
+ ],
15
+ "reference": "useTerpClient() + unwrap from @terp/react-core for request/response; useRealtimeChannel() from @terp/react-core for typed SSE/WebSocket subscriptions (one-use ticket minted through the generated client).",
16
+ "opt_out": "// terp-allow-generated-client-only: <reason>",
17
+ "runtime": {
18
+ "applicability": "not-applicable",
19
+ "rationale": "In the running bundle the sanctioned generated client is itself built on fetch, so egress-primitive use cannot be attributed to an app module from inside the app. One typed egress path is an authoring contract; network policy beyond it (CSP connect-src) is deployment configuration the framework does not own per module."
20
+ },
21
+ "restricted_surface": [
22
+ "restrictedGlobals",
23
+ "restrictedMemberCalls"
24
+ ],
25
+ "corpus": true
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "frontend/layout-contract",
3
+ "surface": "frontend",
4
+ "title": "An opted-in app's archetype body slots accept only the contract's components",
5
+ "intent": "With a checked-in layout contract, each page archetype's body slot is constrained to the contract's sanctioned components, so screens stay structurally consistent and the failure message tells an agent exactly how to build the screen. Paired with the fail-closed runtime check on the rendered slot.",
6
+ "layer": "static-bespoke",
7
+ "enforcement": [
8
+ {
9
+ "kind": "build-time",
10
+ "tool": "@terp/eslint-boundaries",
11
+ "ref": "terp/layout-contract",
12
+ "reported_as": "terp/layout-contract"
13
+ },
14
+ {
15
+ "kind": "runtime",
16
+ "tool": "@terp/react-core",
17
+ "ref": "verifySlotChildren"
18
+ }
19
+ ],
20
+ "opt_out": "// terp-allow-layout-contract: <reason>",
21
+ "reference": "frontend/layout-contract.json + the layoutContract bootstrap option (ADR 0079); verifySlotChildren in @terp/react-core is the runtime DOM check.",
22
+ "runtime": {
23
+ "applicability": "required"
24
+ },
25
+ "corpus": true
26
+ }