@terpjs/spec 0.14.0 → 0.15.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 -201
  2. package/README.md +417 -417
  3. package/VERSION +1 -1
  4. package/app-check-report.schema.json +122 -122
  5. package/assurance-profile.schema.json +70 -70
  6. package/catalog/backend/alembic_downgrades_not_empty.json +22 -22
  7. package/catalog/backend/base_query_not_overridden.json +27 -27
  8. package/catalog/backend/canonical_module_shape.json +22 -22
  9. package/catalog/backend/datetime_columns_are_timezone_aware.json +23 -23
  10. package/catalog/backend/escape_hatch_budget.json +21 -21
  11. package/catalog/backend/events_reference_catalog.json +27 -27
  12. package/catalog/backend/input_schemas_exclude_managed_columns.json +27 -27
  13. package/catalog/backend/input_str_fields_have_max_length.json +22 -22
  14. package/catalog/backend/jobs_reference_catalog.json +27 -27
  15. package/catalog/backend/list_routes_paginate.json +32 -32
  16. package/catalog/backend/modules_declare_policy.json +26 -26
  17. package/catalog/backend/mutations_emit_audit.json +26 -26
  18. package/catalog/backend/mutations_require_write_role.json +32 -32
  19. package/catalog/backend/no_adhoc_background_runtime.json +22 -22
  20. package/catalog/backend/no_adhoc_config_decrypt.json +27 -27
  21. package/catalog/backend/no_adhoc_logging_config.json +22 -22
  22. package/catalog/backend/no_adhoc_middleware.json +27 -27
  23. package/catalog/backend/no_adhoc_permission_literals.json +27 -27
  24. package/catalog/backend/no_app_instantiation.json +22 -22
  25. package/catalog/backend/no_blocking_sleep.json +22 -22
  26. package/catalog/backend/no_cross_module_imports.json +21 -21
  27. package/catalog/backend/no_dependency_overrides.json +27 -27
  28. package/catalog/backend/no_destructive_migrations.json +22 -22
  29. package/catalog/backend/no_dynamic_sql.json +22 -22
  30. package/catalog/backend/no_empty_tests.json +22 -22
  31. package/catalog/backend/no_eval_or_exec.json +22 -22
  32. package/catalog/backend/no_hardcoded_credentials.json +21 -21
  33. package/catalog/backend/no_internal_imports.json +22 -22
  34. package/catalog/backend/no_manual_actor_stamping.json +27 -27
  35. package/catalog/backend/no_manual_ownership_checks.json +32 -32
  36. package/catalog/backend/no_manual_scope_filtering.json +27 -27
  37. package/catalog/backend/no_manual_table_schema.json +22 -22
  38. package/catalog/backend/no_manual_version_assignment.json +22 -22
  39. package/catalog/backend/no_mutable_default_args.json +22 -22
  40. package/catalog/backend/no_naive_datetime.json +22 -22
  41. package/catalog/backend/no_oversized_python_files.json +22 -22
  42. package/catalog/backend/no_print.json +22 -22
  43. package/catalog/backend/no_raw_app_routes.json +26 -26
  44. package/catalog/backend/no_raw_connection_access.json +27 -27
  45. package/catalog/backend/no_raw_file_references.json +27 -27
  46. package/catalog/backend/no_raw_outbound_http.json +22 -22
  47. package/catalog/backend/no_raw_session_construction.json +22 -22
  48. package/catalog/backend/no_star_imports.json +22 -22
  49. package/catalog/backend/no_todo_fixme.json +22 -22
  50. package/catalog/backend/no_unique_columns_on_soft_delete_models.json +22 -22
  51. package/catalog/backend/offset_queries_declare_ordering.json +22 -22
  52. package/catalog/backend/path_id_params_are_uuid.json +22 -22
  53. package/catalog/backend/policy_refs_resolve.json +27 -27
  54. package/catalog/backend/public_modules_are_read_only.json +26 -26
  55. package/catalog/backend/reads_use_base_query.json +27 -27
  56. package/catalog/backend/response_model_not_table_model.json +27 -27
  57. package/catalog/backend/routes_declare_response_model.json +27 -27
  58. package/catalog/backend/safe_methods_are_read_only.json +31 -31
  59. package/catalog/backend/schemas_exclude_sensitive_fields.json +32 -32
  60. package/catalog/backend/session_imported_from_sqlmodel.json +22 -22
  61. package/catalog/backend/table_models_use_base_table.json +22 -22
  62. package/catalog/backend/tables_have_migrations.json +27 -27
  63. package/catalog/backend/tenant_scoped_models_use_scoped_service.json +22 -22
  64. package/catalog/backend/ungoverned_escape_hatch.json +21 -21
  65. package/catalog/backend/update_schemas_inherit_base_update_schema.json +32 -32
  66. package/catalog/frontend/escape-hatch.json +21 -21
  67. package/catalog/frontend/generated-client-only.json +26 -26
  68. package/catalog/frontend/layout-contract.json +26 -26
  69. package/catalog/frontend/no-cross-module-imports.json +21 -21
  70. package/catalog/frontend/no-deep-imports.json +25 -25
  71. package/catalog/frontend/no-dom-html-injection.json +22 -22
  72. package/catalog/frontend/no-eval.json +21 -21
  73. package/catalog/frontend/no-inline-styling.json +25 -25
  74. package/catalog/frontend/no-style-imports.json +24 -24
  75. package/catalog/frontend/no-unsafe-href.json +21 -21
  76. package/catalog/frontend/no-unsafe-target-blank.json +21 -21
  77. package/catalog/frontend/router-links.json +22 -22
  78. package/catalog/frontend/token-styled-elements.json +25 -25
  79. package/catalog/schema.json +95 -95
  80. package/corpus/PENDING.json +4 -4
  81. package/corpus/RESIDUALS.json +21 -21
  82. package/corpus/backend/alembic_downgrades_not_empty/compliant-01/modules/notes/migrations/versions/0001_change.py +6 -6
  83. package/corpus/backend/alembic_downgrades_not_empty/violation-01/modules/notes/migrations/versions/0001_change.py +6 -6
  84. package/corpus/backend/base_query_not_overridden/compliant-01/modules/notes/service.py +8 -8
  85. package/corpus/backend/base_query_not_overridden/compliant-02/modules/notes/service.py +8 -8
  86. package/corpus/backend/base_query_not_overridden/violation-01/modules/notes/service.py +8 -8
  87. package/corpus/backend/base_query_not_overridden/violation-02/expected-findings.json +7 -7
  88. package/corpus/backend/base_query_not_overridden/violation-02/modules/notes/service.py +8 -8
  89. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/models.py +6 -6
  90. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/module.py +7 -7
  91. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/router.py +3 -3
  92. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/schemas.py +10 -10
  93. package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/service.py +5 -5
  94. package/corpus/backend/canonical_module_shape/violation-01/modules/notes/router.py +3 -3
  95. package/corpus/backend/datetime_columns_are_timezone_aware/compliant-01/modules/notes/models.py +12 -12
  96. package/corpus/backend/datetime_columns_are_timezone_aware/violation-01/modules/notes/models.py +12 -12
  97. package/corpus/backend/datetime_columns_are_timezone_aware/violation-02/modules/notes/models.py +13 -13
  98. package/corpus/backend/datetime_columns_are_timezone_aware/violation-03/modules/notes/models.py +15 -15
  99. package/corpus/backend/escape_hatch_budget/compliant-01/escape-hatch-budget.json +3 -3
  100. package/corpus/backend/escape_hatch_budget/compliant-01/modules/notes/service.py +2 -2
  101. package/corpus/backend/escape_hatch_budget/compliant-02/escape-hatch-budget.json +1 -1
  102. package/corpus/backend/escape_hatch_budget/compliant-02/modules/notes/service.py +4 -4
  103. package/corpus/backend/escape_hatch_budget/violation-01/escape-hatch-budget.json +1 -1
  104. package/corpus/backend/escape_hatch_budget/violation-01/modules/notes/service.py +2 -2
  105. package/corpus/backend/escape_hatch_budget/violation-02/escape-hatch-budget.json +3 -3
  106. package/corpus/backend/escape_hatch_budget/violation-02/modules/notes/service.py +4 -4
  107. package/corpus/backend/events_reference_catalog/compliant-01/modules/notes/module.py +9 -9
  108. package/corpus/backend/events_reference_catalog/violation-01/modules/notes/module.py +8 -8
  109. package/corpus/backend/input_schemas_exclude_managed_columns/compliant-01/modules/notes/schemas.py +6 -6
  110. package/corpus/backend/input_schemas_exclude_managed_columns/violation-01/expected-findings.json +7 -7
  111. package/corpus/backend/input_schemas_exclude_managed_columns/violation-01/modules/notes/schemas.py +9 -9
  112. package/corpus/backend/input_schemas_exclude_managed_columns/violation-02/expected-findings.json +12 -12
  113. package/corpus/backend/input_schemas_exclude_managed_columns/violation-02/modules/notes/router.py +14 -14
  114. package/corpus/backend/input_str_fields_have_max_length/compliant-01/modules/notes/schemas.py +4 -4
  115. package/corpus/backend/input_str_fields_have_max_length/violation-01/modules/notes/schemas.py +2 -2
  116. package/corpus/backend/jobs_reference_catalog/compliant-01/modules/notes/service.py +5 -5
  117. package/corpus/backend/jobs_reference_catalog/violation-01/modules/notes/service.py +2 -2
  118. package/corpus/backend/list_routes_paginate/compliant-01/modules/notes/router.py +6 -6
  119. package/corpus/backend/list_routes_paginate/violation-01/modules/notes/router.py +3 -3
  120. package/corpus/backend/modules_declare_policy/compliant-01/modules/notes/module.py +7 -7
  121. package/corpus/backend/modules_declare_policy/violation-01/modules/notes/module.py +6 -6
  122. package/corpus/backend/mutations_emit_audit/compliant-01/modules/notes/service.py +9 -9
  123. package/corpus/backend/mutations_emit_audit/violation-01/modules/notes/service.py +4 -4
  124. package/corpus/backend/mutations_require_write_role/compliant-01/modules/notes/module.py +7 -7
  125. package/corpus/backend/mutations_require_write_role/compliant-01/modules/notes/router.py +3 -3
  126. package/corpus/backend/mutations_require_write_role/compliant-02/modules/notes/module.py +7 -7
  127. package/corpus/backend/mutations_require_write_role/compliant-02/modules/notes/router.py +3 -3
  128. package/corpus/backend/mutations_require_write_role/violation-01/expected-findings.json +7 -7
  129. package/corpus/backend/mutations_require_write_role/violation-01/modules/notes/module.py +7 -7
  130. package/corpus/backend/mutations_require_write_role/violation-01/modules/notes/router.py +3 -3
  131. package/corpus/backend/mutations_require_write_role/violation-02/expected-findings.json +7 -7
  132. package/corpus/backend/mutations_require_write_role/violation-02/modules/notes/module.py +7 -7
  133. package/corpus/backend/mutations_require_write_role/violation-02/modules/notes/router.py +5 -5
  134. package/corpus/backend/mutations_require_write_role/violation-03/expected-findings.json +7 -7
  135. package/corpus/backend/mutations_require_write_role/violation-03/modules/notes/module.py +10 -10
  136. package/corpus/backend/mutations_require_write_role/violation-03/modules/notes/router.py +3 -3
  137. package/corpus/backend/no_adhoc_background_runtime/compliant-01/modules/notes/service.py +9 -9
  138. package/corpus/backend/no_adhoc_background_runtime/violation-01/modules/notes/service.py +5 -5
  139. package/corpus/backend/no_adhoc_background_runtime/violation-02/modules/notes/service.py +3 -3
  140. package/corpus/backend/no_adhoc_config_decrypt/compliant-01/modules/billing/service.py +2 -2
  141. package/corpus/backend/no_adhoc_config_decrypt/violation-01/modules/billing/service.py +2 -2
  142. package/corpus/backend/no_adhoc_logging_config/compliant-01/modules/notes/service.py +3 -3
  143. package/corpus/backend/no_adhoc_logging_config/violation-01/modules/notes/service.py +3 -3
  144. package/corpus/backend/no_adhoc_middleware/compliant-01/main.py +3 -3
  145. package/corpus/backend/no_adhoc_middleware/violation-01/main.py +4 -4
  146. package/corpus/backend/no_adhoc_middleware/violation-02/main.py +5 -5
  147. package/corpus/backend/no_adhoc_permission_literals/compliant-01/modules/notes/module.py +3 -3
  148. package/corpus/backend/no_adhoc_permission_literals/compliant-02/modules/notes/router.py +5 -5
  149. package/corpus/backend/no_adhoc_permission_literals/violation-01/expected-findings.json +7 -7
  150. package/corpus/backend/no_adhoc_permission_literals/violation-01/modules/notes/module.py +1 -1
  151. package/corpus/backend/no_adhoc_permission_literals/violation-02/expected-findings.json +7 -7
  152. package/corpus/backend/no_adhoc_permission_literals/violation-02/modules/notes/router.py +2 -2
  153. package/corpus/backend/no_adhoc_permission_literals/violation-03/expected-findings.json +12 -12
  154. package/corpus/backend/no_adhoc_permission_literals/violation-03/modules/notes/module.py +10 -10
  155. package/corpus/backend/no_app_instantiation/compliant-01/main.py +3 -3
  156. package/corpus/backend/no_app_instantiation/violation-01/main.py +3 -3
  157. package/corpus/backend/no_blocking_sleep/compliant-01/modules/notes/service.py +2 -2
  158. package/corpus/backend/no_blocking_sleep/violation-01/modules/notes/service.py +5 -5
  159. package/corpus/backend/no_cross_module_imports/compliant-01/modules/a/service.py +1 -1
  160. package/corpus/backend/no_cross_module_imports/violation-01/modules/a/service.py +1 -1
  161. package/corpus/backend/no_cross_module_imports/violation-02/modules/a/service.py +1 -1
  162. package/corpus/backend/no_dependency_overrides/compliant-01/main.py +12 -12
  163. package/corpus/backend/no_dependency_overrides/violation-01/main.py +11 -11
  164. package/corpus/backend/no_destructive_migrations/compliant-01/modules/notes/migrations/versions/0001_change.py +3 -3
  165. package/corpus/backend/no_destructive_migrations/compliant-02/modules/notes/migrations/versions/0001_change.py +8 -8
  166. package/corpus/backend/no_destructive_migrations/violation-01/expected-findings.json +7 -7
  167. package/corpus/backend/no_destructive_migrations/violation-01/modules/notes/migrations/versions/0001_change.py +2 -2
  168. package/corpus/backend/no_destructive_migrations/violation-02/expected-findings.json +7 -7
  169. package/corpus/backend/no_destructive_migrations/violation-02/modules/notes/migrations/versions/0001_change.py +2 -2
  170. package/corpus/backend/no_destructive_migrations/violation-03/expected-findings.json +7 -7
  171. package/corpus/backend/no_destructive_migrations/violation-03/modules/notes/migrations/versions/0001_change.py +3 -3
  172. package/corpus/backend/no_destructive_migrations/violation-04/expected-findings.json +12 -12
  173. package/corpus/backend/no_destructive_migrations/violation-04/modules/notes/migrations/versions/0001_change.py +6 -6
  174. package/corpus/backend/no_destructive_migrations/violation-05/expected-findings.json +12 -12
  175. package/corpus/backend/no_destructive_migrations/violation-05/modules/notes/migrations/versions/0001_change.py +3 -3
  176. package/corpus/backend/no_dynamic_sql/compliant-01/modules/notes/service.py +1 -1
  177. package/corpus/backend/no_dynamic_sql/compliant-02/modules/notes/service.py +20 -20
  178. package/corpus/backend/no_dynamic_sql/violation-01/modules/notes/service.py +2 -2
  179. package/corpus/backend/no_dynamic_sql/violation-02/modules/notes/service.py +2 -2
  180. package/corpus/backend/no_dynamic_sql/violation-03/modules/notes/service.py +7 -7
  181. package/corpus/backend/no_dynamic_sql/violation-04/modules/notes/service.py +16 -16
  182. package/corpus/backend/no_empty_tests/compliant-01/tests/test_notes.py +2 -2
  183. package/corpus/backend/no_empty_tests/violation-01/tests/test_notes.py +2 -2
  184. package/corpus/backend/no_eval_or_exec/compliant-01/modules/notes/service.py +2 -2
  185. package/corpus/backend/no_eval_or_exec/violation-01/modules/notes/service.py +2 -2
  186. package/corpus/backend/no_hardcoded_credentials/compliant-01/modules/billing/service.py +3 -3
  187. package/corpus/backend/no_hardcoded_credentials/compliant-02/modules/billing/service.py +18 -18
  188. package/corpus/backend/no_hardcoded_credentials/violation-01/expected-findings.json +7 -7
  189. package/corpus/backend/no_hardcoded_credentials/violation-01/modules/billing/service.py +2 -2
  190. package/corpus/backend/no_hardcoded_credentials/violation-02/expected-findings.json +7 -7
  191. package/corpus/backend/no_hardcoded_credentials/violation-02/modules/billing/service.py +1 -1
  192. package/corpus/backend/no_hardcoded_credentials/violation-03/expected-findings.json +22 -22
  193. package/corpus/backend/no_hardcoded_credentials/violation-03/modules/billing/service.py +11 -11
  194. package/corpus/backend/no_hardcoded_credentials/violation-04/expected-findings.json +12 -12
  195. package/corpus/backend/no_hardcoded_credentials/violation-04/modules/billing/service.py +8 -8
  196. package/corpus/backend/no_hardcoded_credentials/violation-05/expected-findings.json +12 -12
  197. package/corpus/backend/no_hardcoded_credentials/violation-05/modules/billing/service.py +4 -4
  198. package/corpus/backend/no_internal_imports/compliant-01/modules/notes/service.py +1 -1
  199. package/corpus/backend/no_internal_imports/violation-01/modules/notes/service.py +1 -1
  200. package/corpus/backend/no_manual_actor_stamping/compliant-01/modules/notes/service.py +5 -5
  201. package/corpus/backend/no_manual_actor_stamping/compliant-02/modules/notes/schemas.py +12 -12
  202. package/corpus/backend/no_manual_actor_stamping/violation-01/modules/notes/service.py +3 -3
  203. package/corpus/backend/no_manual_actor_stamping/violation-02/expected-findings.json +7 -7
  204. package/corpus/backend/no_manual_actor_stamping/violation-02/modules/notes/service.py +4 -4
  205. package/corpus/backend/no_manual_ownership_checks/compliant-01/modules/journals/service.py +3 -3
  206. package/corpus/backend/no_manual_ownership_checks/compliant-02/modules/journals/schemas.py +11 -11
  207. package/corpus/backend/no_manual_ownership_checks/violation-01/modules/journals/service.py +3 -3
  208. package/corpus/backend/no_manual_ownership_checks/violation-02/expected-findings.json +7 -7
  209. package/corpus/backend/no_manual_ownership_checks/violation-02/modules/journals/service.py +9 -9
  210. package/corpus/backend/no_manual_ownership_checks/violation-03/expected-findings.json +7 -7
  211. package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/jobs.py +1 -1
  212. package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/models.py +7 -7
  213. package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/module.py +11 -11
  214. package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/service.py +7 -7
  215. package/corpus/backend/no_manual_scope_filtering/compliant-01/modules/notes/service.py +8 -8
  216. package/corpus/backend/no_manual_scope_filtering/compliant-02/modules/notes/schemas.py +12 -12
  217. package/corpus/backend/no_manual_scope_filtering/violation-01/modules/notes/service.py +2 -2
  218. package/corpus/backend/no_manual_scope_filtering/violation-02/modules/notes/service.py +2 -2
  219. package/corpus/backend/no_manual_scope_filtering/violation-03/expected-findings.json +7 -7
  220. package/corpus/backend/no_manual_scope_filtering/violation-03/modules/notes/service.py +7 -7
  221. package/corpus/backend/no_manual_table_schema/compliant-01/modules/notes/models.py +6 -6
  222. package/corpus/backend/no_manual_table_schema/violation-01/modules/notes/models.py +8 -8
  223. package/corpus/backend/no_manual_version_assignment/compliant-01/modules/notes/service.py +5 -5
  224. package/corpus/backend/no_manual_version_assignment/violation-01/modules/notes/service.py +3 -3
  225. package/corpus/backend/no_manual_version_assignment/violation-02/modules/notes/service.py +3 -3
  226. package/corpus/backend/no_manual_version_assignment/violation-03/modules/notes/service.py +3 -3
  227. package/corpus/backend/no_mutable_default_args/compliant-01/modules/notes/service.py +3 -3
  228. package/corpus/backend/no_mutable_default_args/violation-01/modules/notes/service.py +2 -2
  229. package/corpus/backend/no_naive_datetime/compliant-01/modules/notes/service.py +2 -2
  230. package/corpus/backend/no_naive_datetime/violation-01/modules/notes/service.py +2 -2
  231. package/corpus/backend/no_naive_datetime/violation-02/modules/notes/service.py +2 -2
  232. package/corpus/backend/no_oversized_python_files/compliant-01/modules/notes/service.py +9 -9
  233. package/corpus/backend/no_oversized_python_files/violation-01/modules/notes/service.py +530 -530
  234. package/corpus/backend/no_print/compliant-01/modules/notes/service.py +7 -7
  235. package/corpus/backend/no_print/violation-01/modules/notes/service.py +2 -2
  236. package/corpus/backend/no_raw_app_routes/compliant-01/main.py +12 -12
  237. package/corpus/backend/no_raw_app_routes/violation-01/main.py +10 -10
  238. package/corpus/backend/no_raw_app_routes/violation-02/main.py +16 -16
  239. package/corpus/backend/no_raw_app_routes/violation-03/main.py +12 -12
  240. package/corpus/backend/no_raw_app_routes/violation-04/main.py +10 -10
  241. package/corpus/backend/no_raw_app_routes/violation-05/main.py +12 -12
  242. package/corpus/backend/no_raw_app_routes/violation-06/main.py +10 -10
  243. package/corpus/backend/no_raw_connection_access/compliant-01/modules/notes/service.py +5 -5
  244. package/corpus/backend/no_raw_connection_access/violation-01/modules/notes/service.py +2 -2
  245. package/corpus/backend/no_raw_connection_access/violation-02/modules/notes/service.py +2 -2
  246. package/corpus/backend/no_raw_file_references/compliant-01/modules/notes/models.py +10 -10
  247. package/corpus/backend/no_raw_file_references/violation-01/modules/notes/models.py +9 -9
  248. package/corpus/backend/no_raw_outbound_http/compliant-01/modules/notes/service.py +2 -2
  249. package/corpus/backend/no_raw_outbound_http/compliant-03/modules/notes/service.py +12 -12
  250. package/corpus/backend/no_raw_outbound_http/violation-01/expected-findings.json +7 -7
  251. package/corpus/backend/no_raw_outbound_http/violation-01/modules/notes/service.py +1 -1
  252. package/corpus/backend/no_raw_outbound_http/violation-02/expected-findings.json +7 -7
  253. package/corpus/backend/no_raw_outbound_http/violation-02/modules/notes/service.py +1 -1
  254. package/corpus/backend/no_raw_outbound_http/violation-03/expected-findings.json +7 -7
  255. package/corpus/backend/no_raw_outbound_http/violation-03/modules/notes/service.py +1 -1
  256. package/corpus/backend/no_raw_outbound_http/violation-04/expected-findings.json +12 -12
  257. package/corpus/backend/no_raw_outbound_http/violation-04/modules/notes/service.py +7 -7
  258. package/corpus/backend/no_raw_outbound_http/violation-05/expected-findings.json +12 -12
  259. package/corpus/backend/no_raw_outbound_http/violation-05/modules/notes/service.py +5 -5
  260. package/corpus/backend/no_raw_outbound_http/violation-06/expected-findings.json +12 -12
  261. package/corpus/backend/no_raw_outbound_http/violation-06/modules/notes/service.py +2 -2
  262. package/corpus/backend/no_raw_session_construction/compliant-01/modules/notes/service.py +3 -3
  263. package/corpus/backend/no_raw_session_construction/violation-01/modules/notes/service.py +3 -3
  264. package/corpus/backend/no_star_imports/compliant-01/modules/notes/service.py +1 -1
  265. package/corpus/backend/no_star_imports/violation-01/modules/notes/service.py +1 -1
  266. package/corpus/backend/no_todo_fixme/compliant-01/modules/notes/service.py +3 -3
  267. package/corpus/backend/no_todo_fixme/violation-01/modules/notes/service.py +3 -3
  268. package/corpus/backend/no_unique_columns_on_soft_delete_models/compliant-01/modules/notes/models.py +17 -17
  269. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-01/expected-findings.json +7 -7
  270. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-01/modules/notes/models.py +6 -6
  271. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-02/expected-findings.json +7 -7
  272. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-02/modules/notes/models.py +13 -13
  273. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-03/expected-findings.json +7 -7
  274. package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-03/modules/notes/models.py +16 -16
  275. package/corpus/backend/offset_queries_declare_ordering/compliant-01/modules/notes/service.py +4 -4
  276. package/corpus/backend/offset_queries_declare_ordering/violation-01/modules/notes/service.py +2 -2
  277. package/corpus/backend/path_id_params_are_uuid/compliant-01/modules/notes/router.py +6 -6
  278. package/corpus/backend/path_id_params_are_uuid/violation-01/modules/notes/router.py +6 -6
  279. package/corpus/backend/policy_refs_resolve/compliant-01/modules/notes/module.py +7 -7
  280. package/corpus/backend/policy_refs_resolve/violation-01/modules/notes/module.py +8 -8
  281. package/corpus/backend/public_modules_are_read_only/compliant-01/modules/notes/module.py +7 -7
  282. package/corpus/backend/public_modules_are_read_only/compliant-01/modules/notes/router.py +3 -3
  283. package/corpus/backend/public_modules_are_read_only/violation-01/expected-findings.json +7 -7
  284. package/corpus/backend/public_modules_are_read_only/violation-01/modules/notes/module.py +7 -7
  285. package/corpus/backend/public_modules_are_read_only/violation-01/modules/notes/router.py +3 -3
  286. package/corpus/backend/public_modules_are_read_only/violation-02/expected-findings.json +7 -7
  287. package/corpus/backend/public_modules_are_read_only/violation-02/modules/notes/module.py +7 -7
  288. package/corpus/backend/public_modules_are_read_only/violation-02/modules/notes/router.py +5 -5
  289. package/corpus/backend/reads_use_base_query/compliant-01/modules/notes/models.py +6 -6
  290. package/corpus/backend/reads_use_base_query/compliant-01/modules/notes/service.py +8 -8
  291. package/corpus/backend/reads_use_base_query/compliant-02/modules/notes/models.py +9 -9
  292. package/corpus/backend/reads_use_base_query/compliant-02/modules/notes/service.py +9 -9
  293. package/corpus/backend/reads_use_base_query/violation-01/modules/notes/models.py +6 -6
  294. package/corpus/backend/reads_use_base_query/violation-01/modules/notes/service.py +9 -9
  295. package/corpus/backend/reads_use_base_query/violation-02/expected-findings.json +7 -7
  296. package/corpus/backend/reads_use_base_query/violation-02/modules/notes/models.py +6 -6
  297. package/corpus/backend/reads_use_base_query/violation-02/modules/notes/service.py +8 -8
  298. package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/models.py +6 -6
  299. package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/router.py +3 -3
  300. package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/schemas.py +5 -5
  301. package/corpus/backend/response_model_not_table_model/violation-01/modules/notes/models.py +6 -6
  302. package/corpus/backend/response_model_not_table_model/violation-01/modules/notes/router.py +3 -3
  303. package/corpus/backend/routes_declare_response_model/compliant-01/modules/notes/router.py +8 -8
  304. package/corpus/backend/routes_declare_response_model/violation-01/modules/notes/router.py +3 -3
  305. package/corpus/backend/safe_methods_are_read_only/compliant-01/modules/notes/router.py +6 -6
  306. package/corpus/backend/safe_methods_are_read_only/compliant-02/modules/notes/router.py +5 -5
  307. package/corpus/backend/safe_methods_are_read_only/violation-01/expected-findings.json +7 -7
  308. package/corpus/backend/safe_methods_are_read_only/violation-01/modules/notes/router.py +3 -3
  309. package/corpus/backend/safe_methods_are_read_only/violation-02/expected-findings.json +7 -7
  310. package/corpus/backend/safe_methods_are_read_only/violation-02/modules/notes/router.py +5 -5
  311. package/corpus/backend/safe_methods_are_read_only/violation-03/expected-findings.json +7 -7
  312. package/corpus/backend/safe_methods_are_read_only/violation-03/modules/notes/router.py +3 -3
  313. package/corpus/backend/schemas_exclude_sensitive_fields/compliant-01/modules/users/schemas.py +5 -5
  314. package/corpus/backend/schemas_exclude_sensitive_fields/compliant-02/modules/accounts/schemas.py +12 -12
  315. package/corpus/backend/schemas_exclude_sensitive_fields/violation-01/expected-findings.json +7 -7
  316. package/corpus/backend/schemas_exclude_sensitive_fields/violation-01/modules/users/schemas.py +3 -3
  317. package/corpus/backend/schemas_exclude_sensitive_fields/violation-02/expected-findings.json +7 -7
  318. package/corpus/backend/schemas_exclude_sensitive_fields/violation-02/modules/connectors/router.py +8 -8
  319. package/corpus/backend/schemas_exclude_sensitive_fields/violation-03/expected-findings.json +17 -17
  320. package/corpus/backend/schemas_exclude_sensitive_fields/violation-03/modules/integrations/schemas.py +7 -7
  321. package/corpus/backend/session_imported_from_sqlmodel/compliant-01/modules/notes/service.py +5 -5
  322. package/corpus/backend/session_imported_from_sqlmodel/violation-01/modules/notes/service.py +5 -5
  323. package/corpus/backend/table_models_use_base_table/compliant-01/modules/notes/models.py +6 -6
  324. package/corpus/backend/table_models_use_base_table/violation-01/modules/notes/models.py +5 -5
  325. package/corpus/backend/tables_have_migrations/compliant-01/modules/notes/migrations/versions/0a1b2c3d4e5f_create_notes_tables.py +22 -22
  326. package/corpus/backend/tables_have_migrations/compliant-01/modules/notes/models.py +6 -6
  327. package/corpus/backend/tables_have_migrations/compliant-02/capabilities/ledger/models.py +6 -6
  328. package/corpus/backend/tables_have_migrations/violation-01/expected-findings.json +7 -7
  329. package/corpus/backend/tables_have_migrations/violation-01/modules/notes/models.py +6 -6
  330. package/corpus/backend/tables_have_migrations/violation-02/expected-findings.json +7 -7
  331. package/corpus/backend/tables_have_migrations/violation-02/modules/notes/models.py +10 -10
  332. package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-01/modules/projects/models.py +6 -6
  333. package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-01/modules/projects/service.py +5 -5
  334. package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-02/modules/projects/models.py +8 -8
  335. package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-02/modules/projects/service.py +5 -5
  336. package/corpus/backend/tenant_scoped_models_use_scoped_service/violation-01/modules/projects/models.py +6 -6
  337. package/corpus/backend/tenant_scoped_models_use_scoped_service/violation-01/modules/projects/service.py +5 -5
  338. package/corpus/backend/ungoverned_escape_hatch/compliant-01/modules/notes/service.py +5 -5
  339. package/corpus/backend/ungoverned_escape_hatch/compliant-02/modules/notes/service.py +4 -4
  340. package/corpus/backend/ungoverned_escape_hatch/violation-01/modules/notes/service.py +2 -2
  341. package/corpus/backend/update_schemas_inherit_base_update_schema/compliant-01/modules/notes/schemas.py +2 -2
  342. package/corpus/backend/update_schemas_inherit_base_update_schema/violation-01/modules/notes/schemas.py +2 -2
  343. package/corpus/backend/update_schemas_inherit_base_update_schema/violation-02/modules/notes/schemas.py +5 -5
  344. package/corpus/frontend/escape-hatch/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  345. package/corpus/frontend/escape-hatch/violation-01/src/modules/widgets/Widget.tsx +4 -4
  346. package/corpus/frontend/escape-hatch/violation-02/src/modules/widgets/Widget.tsx +7 -7
  347. package/corpus/frontend/escape-hatch/violation-03/src/modules/widgets/Widget.tsx +7 -7
  348. package/corpus/frontend/generated-client-only/compliant-01/src/modules/widgets/Widget.tsx +6 -6
  349. package/corpus/frontend/generated-client-only/compliant-02/src/modules/widgets/Widget.tsx +21 -21
  350. package/corpus/frontend/generated-client-only/compliant-03/src/modules/widgets/Widget.tsx +7 -7
  351. package/corpus/frontend/generated-client-only/compliant-04/src/modules/widgets/Widget.tsx +17 -17
  352. package/corpus/frontend/generated-client-only/violation-01/src/modules/widgets/Widget.tsx +3 -3
  353. package/corpus/frontend/generated-client-only/violation-02/src/modules/widgets/Widget.tsx +9 -9
  354. package/corpus/frontend/generated-client-only/violation-03/src/modules/widgets/Widget.tsx +14 -14
  355. package/corpus/frontend/generated-client-only/violation-04/src/modules/widgets/Widget.tsx +6 -6
  356. package/corpus/frontend/layout-contract/compliant-01/layout-contract.json +3 -3
  357. package/corpus/frontend/layout-contract/compliant-01/src/modules/widgets/Widget.tsx +8 -8
  358. package/corpus/frontend/layout-contract/violation-01/layout-contract.json +3 -3
  359. package/corpus/frontend/layout-contract/violation-01/src/modules/widgets/Widget.tsx +8 -8
  360. package/corpus/frontend/no-cross-module-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  361. package/corpus/frontend/no-cross-module-imports/violation-01/src/modules/widgets/Widget.tsx +3 -3
  362. package/corpus/frontend/no-deep-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  363. package/corpus/frontend/no-deep-imports/violation-01/src/modules/widgets/Widget.tsx +2 -2
  364. package/corpus/frontend/no-dom-html-injection/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  365. package/corpus/frontend/no-dom-html-injection/compliant-02/src/modules/widgets/Widget.tsx +11 -11
  366. package/corpus/frontend/no-dom-html-injection/violation-01/src/modules/widgets/Widget.tsx +3 -3
  367. package/corpus/frontend/no-dom-html-injection/violation-02/src/modules/widgets/Widget.tsx +7 -7
  368. package/corpus/frontend/no-dom-html-injection/violation-03/src/modules/widgets/Widget.tsx +12 -12
  369. package/corpus/frontend/no-eval/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  370. package/corpus/frontend/no-eval/compliant-02/src/modules/widgets/Widget.tsx +11 -11
  371. package/corpus/frontend/no-eval/violation-01/src/modules/widgets/Widget.tsx +3 -3
  372. package/corpus/frontend/no-eval/violation-02/src/modules/widgets/Widget.tsx +7 -7
  373. package/corpus/frontend/no-eval/violation-03/src/modules/widgets/Widget.tsx +10 -10
  374. package/corpus/frontend/no-eval/violation-04/src/modules/widgets/Widget.tsx +6 -6
  375. package/corpus/frontend/no-inline-styling/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  376. package/corpus/frontend/no-inline-styling/violation-01/src/modules/widgets/Widget.tsx +3 -3
  377. package/corpus/frontend/no-inline-styling/violation-02/src/modules/widgets/Widget.tsx +6 -6
  378. package/corpus/frontend/no-style-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  379. package/corpus/frontend/no-style-imports/violation-01/src/modules/widgets/Widget.tsx +2 -2
  380. package/corpus/frontend/no-unsafe-href/compliant-01/src/modules/widgets/Widget.tsx +3 -3
  381. package/corpus/frontend/no-unsafe-href/violation-01/src/modules/widgets/Widget.tsx +3 -3
  382. package/corpus/frontend/no-unsafe-target-blank/compliant-01/src/modules/widgets/Widget.tsx +3 -3
  383. package/corpus/frontend/no-unsafe-target-blank/violation-01/src/modules/widgets/Widget.tsx +3 -3
  384. package/corpus/frontend/router-links/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  385. package/corpus/frontend/router-links/violation-01/src/modules/widgets/Widget.tsx +3 -3
  386. package/corpus/frontend/token-styled-elements/compliant-01/src/modules/widgets/Widget.tsx +4 -4
  387. package/corpus/frontend/token-styled-elements/compliant-02/src/modules/widgets/Widget.tsx +4 -4
  388. package/corpus/frontend/token-styled-elements/violation-01/src/modules/widgets/Widget.tsx +3 -3
  389. package/findings.schema.json +40 -40
  390. package/package.json +23 -23
  391. package/restricted-surface.json +10 -10
  392. package/scorecard.schema.json +52 -52
package/README.md CHANGED
@@ -1,417 +1,417 @@
1
- # The Terp Standard — rule catalog + violation corpus
2
-
3
- This repository is the **stack-neutral specification** of Terp's
4
- secure-by-default rules (ADR 0080, made consumable by ADR 0081). The framework
5
- ([terp-framework](https://github.com/AITT-NL/terp-framework)) is the
6
- **reference implementation**; this spec is what any other stack would
7
- implement and be verified against. It is deliberately self-contained (no
8
- `terp.*` imports, plain JSON + sample files) and consumed as a **package**,
9
- never a repo path (ADR 0082).
10
-
11
- ## The package seam (ADR 0082)
12
-
13
- The directory doubles as two thin distributions over one data set:
14
-
15
- - **`terp-spec`** (Python, `pyproject.toml`): a dependency-free `terp_spec`
16
- accessor — `spec_dir()` returns the on-disk spec root, `spec_version()` the
17
- `VERSION` semver. A uv workspace member; the framework's certification tests
18
- locate the spec only through it.
19
- - **`@terpjs/spec`** (npm, `package.json`): data only; consumers resolve the
20
- spec root via `require.resolve("@terpjs/spec/package.json")`. An npm workspace
21
- member; the ESLint adapter's corpus/surface tests depend on it.
22
-
23
- Both are **published from the tag workflow** (ADR 0086) — `terp-spec` on PyPI,
24
- `@terpjs/spec` on npm, via Trusted Publishing (OIDC), so a consumer pins an
25
- ordinary version. Pinning the git tag instead keeps working.
26
-
27
- Both manifests carry the **spec version** (`VERSION`) — independent of the
28
- platform's lockstep release version, per ADR 0081's certification model; the
29
- standalone suite holds the three declarations equal.
30
-
31
- The spec-only validations (versioning, schema validity, the refused surface's
32
- shape, the corpus ratchet and directory discipline) live in `spec/tests/` and
33
- run standalone (`python -m pytest` from `spec/`; CI: the path-filtered
34
- `spec.yml` workflow). The framework gate keeps the *parity* half — everything
35
- that needs the live implementations. Because the framework consumes a **pinned
36
- release**, this repository's CI additionally runs `certify-against-reference`:
37
- it checks out the reference framework, substitutes the candidate spec for the
38
- pinned `terp-spec` / `@terpjs/spec`, and runs the framework's parity + corpus
39
- certification — so a catalog or corpus change is proven against the live
40
- implementations *before* release, and the framework's later pin bump re-proves
41
- it in the framework's own gate. Splitting the spec out is then purely a
42
- manifest change: move `spec/` + `spec.yml`, repin `terp-spec` / `@terpjs/spec`
43
- from workspace sources to a git tag or registry release
44
- (`tests/architecture/test_repo_split_readiness.py` fails the build if code
45
- re-couples the units by path).
46
-
47
- The artifacts:
48
-
49
- 1. **`VERSION`** — the semver of the standard. A checker certified against the
50
- corpus records the spec version it was certified for.
51
- 2. **The catalog** (`catalog/`) — one JSON document per rule (validated by
52
- `catalog/schema.json`): what the rule is, why it exists, and how it is
53
- enforced today.
54
- 3. **The corpus** (`corpus/`) — violating and compliant code samples per rule:
55
- the executable meaning of the rule. `corpus/PENDING.json` is the coverage
56
- ratchet — the explicit list of rules still without cases, which only shrinks.
57
- 4. **The finding format** (`findings.schema.json`) — what a conformant checker
58
- emits, so checkers are interoperable and a corpus harness can be generic.
59
- 5. **The check-report format** (`app-check-report.schema.json`) — the complete
60
- result of one checker invocation over one application tree: the finding
61
- format plus the run's own evaluated-rule inventory, spec version, checker
62
- identity and verdict, so a driving tool can join per-rule verdicts to the
63
- catalog fail-closed.
64
- 6. **The refused surface** (`restricted-surface.json`) — the stack-neutral,
65
- normative declaration of the raw frontend primitives an app module must not
66
- author (elements, attributes, egress globals/member calls, stylesheet
67
- extensions, deep-import segments). The portable prohibition rules cite it
68
- structurally (the `restricted_surface` catalog field); which sanctioned
69
- component answers each primitive is per-stack configuration.
70
- 7. **The residual ratchet** (`corpus/RESIDUALS.json`) — the statically-erased
71
- or renamed forms deliberately outside the corpus contract, per rule, as
72
- shrink-only data (see "Detector boundaries" below).
73
- 8. **The scorecard format** (`scorecard.schema.json`) — the machine-readable
74
- certification summary a conformant checker emits (spec version, per-rule
75
- verdicts over the corpus, residuals claimed), so "certified against spec
76
- X.Y.Z" is a verifiable artifact instead of a claim.
77
- 9. **The changelog** (`CHANGELOG.md`) — the change history keyed to `VERSION`
78
- (the top entry must match, held by the spec suite), so a checker certified
79
- against an earlier version can see exactly what changed since.
80
- 10. **The rule pages** (`docs/rules/`) — plain-language documentation generated
81
- from the catalog (`tools/generate_rule_docs.py`; regenerate-and-compare
82
- parity in the spec suite, so the pages cannot drift from the data).
83
-
84
- Everything is locked to the live implementations by build-time parity tests
85
- (`tests/architecture/test_spec_catalog.py`, `tests/architecture/test_spec_corpus.py`,
86
- `packages/frontend/eslint-boundaries/src/corpus.test.js` and
87
- `packages/frontend/eslint-boundaries/src/surface.test.js` — the latter holds the
88
- reference adapter to `restricted-surface.json` both structurally and
89
- behaviourally), following the
90
- same "docs can't lie" discipline as the rest of the gate: a rule cannot ship
91
- without a catalog entry, a catalog entry cannot outlive its rule, and every
92
- enforcement reference must resolve to real code.
93
-
94
- ## Scope: what the standard claims — and delegates
95
-
96
- The catalog is **Terp-specific secure architecture, not complete application
97
- security**. A rule is admitted only when Terp provides a *privileged seam* or
98
- a *more precise invariant* than a generic analyzer can state — a framework
99
- chokepoint to pair with (`runtime.applicability: required`), a
100
- refused-surface entry, a trait/registry the rule holds code to. Generic
101
- vulnerability classes that stock security analyzers already detect well
102
- (command injection, unsafe deserialization, weak security randomness, …) are
103
- **delegated, never duplicated**: a conformant toolchain
104
- runs a generic security baseline for its language *next to* the Terp rules
105
- (the reference stack pins ruff's bandit-derived `S` rules, wired into the
106
- platform repo, the client template, and each generated project's own gate —
107
- platform ADR 0085). Classes no stock analyzer detects well — path traversal,
108
- secrets in logs, browser-storage auth material — are addressed
109
- **constructively** by the reference framework (streamed storage behind
110
- declared references, central log redaction, the session/refresh model) and
111
- earn a detective catalog rule only through the same admission bar, never as a
112
- checkbox. Baseline findings are **not** Terp findings: the finding
113
- format's `rule` pattern admits only catalog ids, so a generic finding can
114
- never masquerade as (or dilute) a Terp verdict — it travels as the baseline
115
- tool's own output (or the envelope's `unattributed` bucket, ADR 0083).
116
-
117
- Catalog entries deliberately carry **no CWE/OWASP mappings**: no governance
118
- consumer exists for them, and the delegated baseline's own documentation
119
- already maps the generic classes. Metadata joins the catalog when something
120
- machine-consumes it, not before.
121
-
122
- ## Catalog format
123
-
124
- `catalog/<surface>/<rule>.json`, validated against `catalog/schema.json` (the
125
- schema is normative and travels with the spec). The normative statement of a
126
- rule is its `title` + `intent`, and that prose is **stack-neutral by
127
- construction**: plain prose, no docstring markup, no reference-implementation
128
- symbols, package paths, marker spellings, or repo-internal pointers (sibling
129
- rules are cited by catalog rule name). The reference realisation lives in the
130
- non-normative fields (`enforcement`, `reference`, `opt_out`, `guide_topic`).
131
- The standalone suite enforces the split
132
- (`test_normative_prose_is_stack_neutral`).
133
-
134
- | Field | Meaning |
135
- |---|---|
136
- | `id` | `<surface>/<rule>` — `backend/<snake_case>` (a `terp.arch` rule) or `frontend/<kebab-case>` (a boundary rule). **Findings are attributed to this id**, never to a tool-internal rule id. |
137
- | `surface` | `backend` or `frontend`. |
138
- | `title` | One-line statement of the invariant. |
139
- | `intent` | Why the rule exists — the drift or threat it prevents. |
140
- | `layer` | Cheapest faithful verification for a *new* stack: see below. |
141
- | `enforcement` | How the reference implementation enforces it (first entry: the `build-time` check). A `runtime` entry names the fail-closed runtime control pairing with it (the two-layer discipline a Level 3 stack must reproduce for the rules that require it); a `black-box` entry names the `@terp/conformance` probe title. For frontend rules, `reported_as` is the ESLint rule id violations surface as — several catalog rules share one core rule id, so the adapter publishes a `catalogRuleId()` mapping and findings are attributed through it. |
142
- | `runtime` | **Mandatory.** The rule's runtime-applicability classification (`required` / `not-applicable` / `deferred`) plus a `rationale` (mandatory for exemptions) and, for `deferred`, a `tracking` reference naming where the deferral is tracked — see “Runtime applicability” below. |
143
- | `restricted_surface` | Frontend prohibition rules only: the `restricted-surface.json` keys the rule realises — the structural citation the spec suite resolves (every key must be claimed by some rule; a prose mention in `intent` must agree with the field). |
144
- | `opt_out` | The *reference realisation* of the abstract escape-hatch contract (see below). |
145
- | `reference` | Optional reference-implementation metadata: the compliant realisation the reference stack offers (component / helper names). **Not normative** — the `title` and `intent` state the stack-neutral invariant; another stack ships its own realisation. |
146
- | `guide_topic` | Reference-implementation metadata (backend only): the `terp guide` topic teaching the compliant pattern. Not normative for other stacks. |
147
- | `corpus` | Whether `corpus/<id>/` cases exist yet (the parity test holds this flag to the directory truth; a covered rule needs at least one `violation-*` **and** one `compliant-*` case). |
148
-
149
- ### Enforcement layers
150
-
151
- - **`black-box`** — the invariant is observable by probing a *running* app
152
- (no source access needed). Portable to any stack via a conformance suite;
153
- each such rule names its probe in a `black-box` enforcement entry
154
- (`packages/frontend/conformance/tests/standard.spec.ts`).
155
- - **`static-portable`** — expressible as source patterns a generic engine
156
- (Semgrep/ast-grep-class) can realise per language from this spec.
157
- - **`static-bespoke`** — needs deep framework knowledge (traits, `ModuleSpec`,
158
- archetype slots); built per officially certified stack only.
159
-
160
- The classification is a judgment about *porting cost*, not a limit on the
161
- reference implementation — today every rule is enforced by `terp.arch` or
162
- `@terp/eslint-boundaries` regardless of layer.
163
-
164
- ### Runtime applicability (the two-layer discipline, per rule)
165
-
166
- Whether a rule *additionally* pairs with a fail-closed runtime control is not a
167
- blanket claim — it is recorded per rule in the mandatory `runtime` block and
168
- held coherent by the spec suite:
169
-
170
- - **`required`** — the invariant is observable in the running system and the
171
- reference implementation owns a fail-closed control for it; the entry **must**
172
- declare that control as a `kind: "runtime"` enforcement entry (whose `ref`
173
- must resolve to a real symbol — the framework's parity test fails otherwise).
174
- - **`not-applicable`** — the invariant is a property of the authored artifact
175
- (source form, imports, justification markers, checked-in files) that is
176
- erased or already materialised by the time the app runs, so no runtime seam
177
- can enforce it. The mandatory `rationale` states why; the build-time check is
178
- the control, by recorded decision.
179
- - **`deferred`** — a runtime control would add independent fidelity on a seam
180
- the reference framework owns, but has not shipped yet: an explicit, reviewed
181
- gap, never a silent one. The mandatory `rationale` names the seam, and the
182
- mandatory `tracking` reference names where the deferral is tracked (an issue
183
- URL or the reference implementation's tracker document plus the seam) — so a
184
- deferral has a lifecycle instead of being open-ended.
185
-
186
- Fail-closed consistency (spec suite): `required` iff a `runtime` enforcement
187
- entry exists; an exemption always carries a non-empty rationale; a deferral
188
- always carries a tracking reference.
189
-
190
- ### The escape-hatch contract
191
-
192
- Every rule has **at most one** governed opt-out, and its *semantics* are the
193
- normative part: a **justified inline marker** on (or immediately above) the
194
- violating line that names the **catalog rule name** (the `<rule>` half of the
195
- id — never a tool-internal rule id, mirroring findings attribution) and states
196
- a reason; an unjustified marker is itself a violation; marker counts must
197
- exactly match a checked-in per-app budget that can only shrink (the ratchet).
198
- The `opt_out` field records the reference realisation's concrete spelling
199
- (`# arch-allow-<rule>: <reason>` in Python, `// terp-allow-<rule>: <reason>`
200
- in TypeScript) — the spelling is derived from the rule id, and the spec suite
201
- holds the derivation. The escape-hatch **governance rules themselves** (the
202
- budget ratchet, the ungoverned-marker condition) carry no `opt_out`:
203
- governance cannot be waived by the mechanism it governs. Another stack
204
- implements the same contract with its own comment syntax.
205
-
206
- The `<reason>` is free text, and MAY carry structured metadata tokens so a
207
- long-lived exception stays visible and auditable rather than eternal:
208
- `owner:<who>` (who answers for the exception), `ticket:<ref>` (where its
209
- removal is tracked), and `review-by:<YYYY-MM-DD>` (when it must be re-justified).
210
- The tokens are a convention, not a gate — the marker's semantics are unchanged,
211
- and a checker MUST NOT reject a reason without them — but a toolchain SHOULD
212
- surface expired `review-by:` dates in its reporting.
213
-
214
- ## Finding format
215
-
216
- A conformant checker emits an array of findings per checked tree, shaped by
217
- `findings.schema.json`: each finding names the **catalog rule id** it realises
218
- (`rule`), the file `path` relative to the checked tree's root, and optionally a
219
- `line`, a directive `message`, a `fix_hint` (the compliant construct, for agent
220
- consumers acting on findings without re-reading the catalog), and a
221
- `fingerprint` (a stable, checker-chosen per-instance identifier so a finding
222
- can be tracked across line-shifting edits). Attribution is always to the
223
- stack-neutral catalog id — the reference ESLint adapter, whose core rule ids
224
- are shared between several catalog rules, publishes this mapping as
225
- `catalogRuleId()` in `@terp/eslint-boundaries`.
226
-
227
- ## Check-report format
228
-
229
- Findings alone cannot support a per-rule verdict: a rule with zero findings is
230
- only *passing* if the run actually evaluated it, and the consumer must never
231
- supply that knowledge itself — its own catalog copy can be newer or older than
232
- the checked app's pinned toolchain (version skew), and some enforcement is
233
- conditional (an escape-hatch budget only when one is checked in, a layout
234
- contract only when the app opts in). So a conformant checker reports a whole
235
- run as one **application check report** (`app-check-report.schema.json`): a
236
- format marker (`terp_check_report: 1`), the **spec version** its rule ids
237
- resolve against, the **checker identity** (the same identity its certification
238
- scorecard carries), the run **verdict** (`ok`, or an explicit `error` for a
239
- run that failed to complete — an erroring run never claims ok and never claims
240
- rules it did not finish evaluating), the **evaluated-rule inventory**
241
- (`rules`), the opt-in rules published as **`not_applicable`** (their own
242
- state — never passing, never unknown), the **findings** (exactly the finding
243
- format's item shape — the spec suite holds the two identical), and
244
- **`unattributed`** messages (diagnostics outside the standard, surfaced rather
245
- than dropped). A consumer joins per-rule verdicts to the catalog exclusively
246
- through the report's inventory, fail closed: pass = evaluated with no
247
- attributed finding; a rule the run did not publish renders unknown, never
248
- green. A multi-surface toolchain emits one report per checker run and a
249
- consumer merges reports through their inventories.
250
-
251
- ## Scorecard format
252
-
253
- A checker claiming certification emits a scorecard (`scorecard.schema.json`):
254
- the spec version it certified against, the checker's identity, and one entry
255
- per claimed rule with its pass/fail verdict over the corpus and the residuals
256
- it relies on (which must be a subset of `corpus/RESIDUALS.json` for the rule —
257
- claiming an unrecorded residual is a conformance failure). A consumer can
258
- re-run the corpus and reproduce the scorecard, making the certification claim
259
- verifiable.
260
-
261
- ## Assurance profile
262
-
263
- Conformance to the rule catalog proves the *Terp-specific* half of release
264
- readiness. The **assurance profile** (`assurance-profile.schema.json`) is the
265
- machine-readable composition of that with the generic evidence lanes a release
266
- also stands on — one document a toolchain emits from its release verification
267
- profile, so "this build is releasable" becomes a checkable artifact instead of
268
- a habit.
269
-
270
- The lane vocabulary and each lane's requirement level are **normative** and
271
- fixed here — an emitter cannot demote a required lane, which is why the
272
- requirement level is deliberately not a field of the document:
273
-
274
- | Lane | Requirement | Evidence |
275
- |---|---|---|
276
- | `terp-standard` | **required** | The standard's own enforcement surfaces ran and passed: the architecture gate and the frontend boundary lint, publishing their evaluated-rule inventories (check reports). |
277
- | `appsec-baseline` | **required** | The delegated generic AppSec baseline passed (the reference realisation: `ruff` with the flake8-bandit `S` rules). |
278
- | `dependency-audit` | **required** | Dependency trees were audited against known-vulnerability databases (the reference realisation: `pip-audit` and `npm audit`). |
279
- | `a11y` | recommended | Automated accessibility checks over the running app (e.g. axe). |
280
- | `blackbox-conformance` | recommended | The black-box behavioural conformance suite over the running workbench. |
281
-
282
- The claim (`ok`) is true exactly when every **required** lane passed;
283
- recommended lanes inform the reader but never carry the claim. Every lane of
284
- the vocabulary appears exactly once — a lane the toolchain does not realise is
285
- reported `not-run` (with no composing checks), never dropped and never counted
286
- as passed. Each realised lane names the verification-check ids whose verdicts
287
- compose it, so a consumer can trace the claim into the toolchain's own
288
- verification envelope.
289
-
290
- ## Corpus format
291
-
292
- ```text
293
- corpus/backend/<rule>/violation-01/ # files rooted at the app package root
294
- corpus/backend/<rule>/compliant-01/ # e.g. modules/notes/service.py
295
- corpus/frontend/<rule>/violation-01/ # files rooted at the frontend app root
296
- corpus/frontend/<rule>/compliant-01/ # e.g. src/modules/widgets/Widget.tsx
297
- ```
298
-
299
- A case may ship an extra checker input at its root when the rule takes one:
300
- the `backend/escape_hatch_budget` cases carry the `escape-hatch-budget.json`
301
- the ratchet is verified against, and a frontend layout-contract case activates
302
- via a `layout-contract.json` at its root.
303
-
304
- A violation case MAY additionally ship an **`expected-findings.json`** at its
305
- root: the exact findings (rule + path + line, shaped by
306
- `findings.schema.json`) a maximally-precise checker emits for the rule under
307
- test. A harness may assert the manifest exactly — hardening the per-case
308
- contract from "flags something" to "flags the right line" — while the loose
309
- contract below remains the floor for cases without one (and the ceiling a
310
- checker must reach to conform). The spec suite holds every checked-in manifest
311
- to the finding shape and to the case it sits in.
312
-
313
- The contract for a checker claiming conformance for a rule is stated **per
314
- rule**, over the finding format above:
315
-
316
- - every `violation-*` case produces at least one finding attributed to that
317
- rule's catalog id, and
318
- - every `compliant-*` case produces **no** findings for that rule.
319
-
320
- A compliant case is minimal, not a full application — it may legitimately be
321
- incomplete with respect to *other* rules, so a multi-rule checker scopes the
322
- corpus run to the rule under test. (The reference frontend harness happens to
323
- hold its compliant cases fully clean across all boundary rules — a stricter
324
- bar than the contract requires.)
325
-
326
- ### Detector boundaries (what the corpus deliberately does not require)
327
-
328
- The executable corpus **is** the interoperability contract: a checker is
329
- conformant for a rule when it flags every `violation-*` case and stays silent
330
- on every `compliant-*` case — nothing more. The violation cases include the
331
- evasion shapes the rules are expected to see through (qualified/attribute
332
- calls, multiline constructs, values routed through a local variable or
333
- `.format()`/`%` building, aliased and parenthesized imports, computed
334
- `window["fetch"]`-style member access for the egress family), and the
335
- compliant cases pin the near-misses that must **not** fire (adjacent-literal
336
- SQL that merely looks concatenated, credential-shaped names with dynamic
337
- values, `*_mock`/`socketserver`-style name cousins, member calls like
338
- `repo.fetch(...)` / `interpreter.eval(...)` on local objects, forbidden syntax
339
- quoted inside comments and strings).
340
-
341
- Some statically-erased or renamed forms are **deliberately outside** the
342
- contract — known limits of precise, low-false-positive detection, kept out of
343
- the corpus so a second implementation neither over-fits nor under-claims.
344
- These residuals are recorded per rule in **`corpus/RESIDUALS.json`** — a
345
- machine-readable ratchet governed like `corpus/PENDING.json`: the list only
346
- shrinks, and closing a residual means seeding the corpus case that contracts
347
- it and deleting the entry, never silently. Today it records:
348
-
349
- - an alias-renamed symbol import (`from sqlalchemy import text as sql_text`)
350
- is not required to be resolved to `text`;
351
- - dynamic import (`importlib.import_module("httpx")`) is not required to be
352
- seen as an import;
353
- - a computed sink or global (`el["innerHTML"] = …`, `window["eval"]`) is not
354
- required to be recognised for the *eval/DOM-sink* rules (the egress family
355
- **does** contract the computed forms — its cases include them);
356
- - `Function("…")` called without `new`, and egress through receivers other
357
- than `window`/`globalThis` (`self.fetch`), are not required.
358
-
359
- These residuals are governed the usual way: the escape-hatch contract makes
360
- sanctioned exceptions visible, and the paired runtime controls (where
361
- `runtime.applicability` is `required`) hold the invariant regardless of
362
- spelling. Widening a detector past the contract is always allowed — the
363
- corpus states the floor, not the ceiling.
364
-
365
- The reference implementations are themselves held to this contract in CI:
366
- `test_spec_corpus.py` runs each catalogued `terp.arch` rule over the backend
367
- corpus, and `corpus.test.js` runs the ESLint adapter over the frontend corpus,
368
- attributing findings via `catalogRuleId()`. Corpus sample files are
369
- intentionally violating code — they are excluded from repo-wide lint
370
- (`ruff` `extend-exclude`) and are never imported or executed.
371
-
372
- Coverage is governed by **`corpus/PENDING.json`**: the exact list of rules
373
- still without cases. Seeding a rule's corpus removes it from the list; a new
374
- rule shipped without corpus must be listed there explicitly — so uncovered
375
- rules stay visible and the list only shrinks. Every `static-portable` backend
376
- rule must have cases (enforced by the parity test); today every backend and
377
- frontend rule has cases and the pending list is empty — it exists so a *new*
378
- rule can ship with its gap explicit and reviewed.
379
-
380
- ## Conformance levels
381
-
382
- - **Level 1 — black-box:** the app passes the runnable conformance suite
383
- (`@terp/conformance`) for the capabilities it claims, including the
384
- `standard:` probes the `black-box` catalog entries name.
385
- - **Level 2 — static rule pack:** additionally, a checker validated against
386
- this corpus enforces the `static-portable` rules for the app's language(s),
387
- emitting findings per `findings.schema.json`.
388
- - **Level 3 — full harness:** additionally, the `static-bespoke` rules, the
389
- paired `runtime` controls of every rule whose `runtime.applicability` is
390
- `required`, and the governed escape-hatch budget ratchet are enforced (today:
391
- the `terp.arch` + `@terp/eslint-boundaries` reference harness on top of
392
- `terp.core` / `@terp/react-core`).
393
-
394
- ## Growing the spec
395
-
396
- - **New rule** → ship the rule and its catalog entry together (the parity test
397
- fails otherwise); classify its runtime applicability deliberately (`required`
398
- with the declared control, or an exemption with its rationale — a deferral
399
- also names its `tracking` reference); seed corpus cases when the rule is
400
- portable, or list it in `corpus/PENDING.json` explicitly; regenerate the
401
- rule pages (`python tools/generate_rule_docs.py`).
402
- - **New corpus cases** → add `violation-*`/`compliant-*` directories, flip the
403
- entry's `corpus` flag to `true`, and drop the rule from `corpus/PENDING.json`;
404
- the harness picks the cases up by convention. Ship an `expected-findings.json`
405
- when the violating lines are pinned. Closing a documented detector residual
406
- also deletes its `corpus/RESIDUALS.json` entry.
407
- - **New stack** → implement the `static-portable` rules however you like; the
408
- corpus is the acceptance test, findings must attribute to catalog ids, and
409
- the certification summary is a scorecard (`scorecard.schema.json`).
410
- - **Format change** → bump `VERSION` and add the matching `CHANGELOG.md` entry
411
- (the suite holds the top entry to the version). Pre-1.0, a **changed
412
- contract** — a new mandatory catalog field (0.5.0's `runtime` block), a
413
- changed finding shape —
414
- bumps the **minor** (the strongest signal 0.x semver carries; a 0.x major
415
- would claim a stability this spec does not yet promise); purely additive
416
- fields and new rules also bump the minor; prose bumps the patch. From 1.0.0
417
- a changed contract bumps the **major**.
1
+ # The Terp Standard — rule catalog + violation corpus
2
+
3
+ This repository is the **stack-neutral specification** of Terp's
4
+ secure-by-default rules (ADR 0080, made consumable by ADR 0081). The framework
5
+ ([terp-framework](https://github.com/AITT-NL/terp-framework)) is the
6
+ **reference implementation**; this spec is what any other stack would
7
+ implement and be verified against. It is deliberately self-contained (no
8
+ `terp.*` imports, plain JSON + sample files) and consumed as a **package**,
9
+ never a repo path (ADR 0082).
10
+
11
+ ## The package seam (ADR 0082)
12
+
13
+ The directory doubles as two thin distributions over one data set:
14
+
15
+ - **`terp-spec`** (Python, `pyproject.toml`): a dependency-free `terp_spec`
16
+ accessor — `spec_dir()` returns the on-disk spec root, `spec_version()` the
17
+ `VERSION` semver. A uv workspace member; the framework's certification tests
18
+ locate the spec only through it.
19
+ - **`@terpjs/spec`** (npm, `package.json`): data only; consumers resolve the
20
+ spec root via `require.resolve("@terpjs/spec/package.json")`. An npm workspace
21
+ member; the ESLint adapter's corpus/surface tests depend on it.
22
+
23
+ Both are **published from the tag workflow** (ADR 0086) — `terp-spec` on PyPI,
24
+ `@terpjs/spec` on npm, via Trusted Publishing (OIDC), so a consumer pins an
25
+ ordinary version. Pinning the git tag instead keeps working.
26
+
27
+ Both manifests carry the **spec version** (`VERSION`) — independent of the
28
+ platform's lockstep release version, per ADR 0081's certification model; the
29
+ standalone suite holds the three declarations equal.
30
+
31
+ The spec-only validations (versioning, schema validity, the refused surface's
32
+ shape, the corpus ratchet and directory discipline) live in `spec/tests/` and
33
+ run standalone (`python -m pytest` from `spec/`; CI: the path-filtered
34
+ `spec.yml` workflow). The framework gate keeps the *parity* half — everything
35
+ that needs the live implementations. Because the framework consumes a **pinned
36
+ release**, this repository's CI additionally runs `certify-against-reference`:
37
+ it checks out the reference framework, substitutes the candidate spec for the
38
+ pinned `terp-spec` / `@terpjs/spec`, and runs the framework's parity + corpus
39
+ certification — so a catalog or corpus change is proven against the live
40
+ implementations *before* release, and the framework's later pin bump re-proves
41
+ it in the framework's own gate. Splitting the spec out is then purely a
42
+ manifest change: move `spec/` + `spec.yml`, repin `terp-spec` / `@terpjs/spec`
43
+ from workspace sources to a git tag or registry release
44
+ (`tests/architecture/test_repo_split_readiness.py` fails the build if code
45
+ re-couples the units by path).
46
+
47
+ The artifacts:
48
+
49
+ 1. **`VERSION`** — the semver of the standard. A checker certified against the
50
+ corpus records the spec version it was certified for.
51
+ 2. **The catalog** (`catalog/`) — one JSON document per rule (validated by
52
+ `catalog/schema.json`): what the rule is, why it exists, and how it is
53
+ enforced today.
54
+ 3. **The corpus** (`corpus/`) — violating and compliant code samples per rule:
55
+ the executable meaning of the rule. `corpus/PENDING.json` is the coverage
56
+ ratchet — the explicit list of rules still without cases, which only shrinks.
57
+ 4. **The finding format** (`findings.schema.json`) — what a conformant checker
58
+ emits, so checkers are interoperable and a corpus harness can be generic.
59
+ 5. **The check-report format** (`app-check-report.schema.json`) — the complete
60
+ result of one checker invocation over one application tree: the finding
61
+ format plus the run's own evaluated-rule inventory, spec version, checker
62
+ identity and verdict, so a driving tool can join per-rule verdicts to the
63
+ catalog fail-closed.
64
+ 6. **The refused surface** (`restricted-surface.json`) — the stack-neutral,
65
+ normative declaration of the raw frontend primitives an app module must not
66
+ author (elements, attributes, egress globals/member calls, stylesheet
67
+ extensions, deep-import segments). The portable prohibition rules cite it
68
+ structurally (the `restricted_surface` catalog field); which sanctioned
69
+ component answers each primitive is per-stack configuration.
70
+ 7. **The residual ratchet** (`corpus/RESIDUALS.json`) — the statically-erased
71
+ or renamed forms deliberately outside the corpus contract, per rule, as
72
+ shrink-only data (see "Detector boundaries" below).
73
+ 8. **The scorecard format** (`scorecard.schema.json`) — the machine-readable
74
+ certification summary a conformant checker emits (spec version, per-rule
75
+ verdicts over the corpus, residuals claimed), so "certified against spec
76
+ X.Y.Z" is a verifiable artifact instead of a claim.
77
+ 9. **The changelog** (`CHANGELOG.md`) — the change history keyed to `VERSION`
78
+ (the top entry must match, held by the spec suite), so a checker certified
79
+ against an earlier version can see exactly what changed since.
80
+ 10. **The rule pages** (`docs/rules/`) — plain-language documentation generated
81
+ from the catalog (`tools/generate_rule_docs.py`; regenerate-and-compare
82
+ parity in the spec suite, so the pages cannot drift from the data).
83
+
84
+ Everything is locked to the live implementations by build-time parity tests
85
+ (`tests/architecture/test_spec_catalog.py`, `tests/architecture/test_spec_corpus.py`,
86
+ `packages/frontend/eslint-boundaries/src/corpus.test.js` and
87
+ `packages/frontend/eslint-boundaries/src/surface.test.js` — the latter holds the
88
+ reference adapter to `restricted-surface.json` both structurally and
89
+ behaviourally), following the
90
+ same "docs can't lie" discipline as the rest of the gate: a rule cannot ship
91
+ without a catalog entry, a catalog entry cannot outlive its rule, and every
92
+ enforcement reference must resolve to real code.
93
+
94
+ ## Scope: what the standard claims — and delegates
95
+
96
+ The catalog is **Terp-specific secure architecture, not complete application
97
+ security**. A rule is admitted only when Terp provides a *privileged seam* or
98
+ a *more precise invariant* than a generic analyzer can state — a framework
99
+ chokepoint to pair with (`runtime.applicability: required`), a
100
+ refused-surface entry, a trait/registry the rule holds code to. Generic
101
+ vulnerability classes that stock security analyzers already detect well
102
+ (command injection, unsafe deserialization, weak security randomness, …) are
103
+ **delegated, never duplicated**: a conformant toolchain
104
+ runs a generic security baseline for its language *next to* the Terp rules
105
+ (the reference stack pins ruff's bandit-derived `S` rules, wired into the
106
+ platform repo, the client template, and each generated project's own gate —
107
+ platform ADR 0085). Classes no stock analyzer detects well — path traversal,
108
+ secrets in logs, browser-storage auth material — are addressed
109
+ **constructively** by the reference framework (streamed storage behind
110
+ declared references, central log redaction, the session/refresh model) and
111
+ earn a detective catalog rule only through the same admission bar, never as a
112
+ checkbox. Baseline findings are **not** Terp findings: the finding
113
+ format's `rule` pattern admits only catalog ids, so a generic finding can
114
+ never masquerade as (or dilute) a Terp verdict — it travels as the baseline
115
+ tool's own output (or the envelope's `unattributed` bucket, ADR 0083).
116
+
117
+ Catalog entries deliberately carry **no CWE/OWASP mappings**: no governance
118
+ consumer exists for them, and the delegated baseline's own documentation
119
+ already maps the generic classes. Metadata joins the catalog when something
120
+ machine-consumes it, not before.
121
+
122
+ ## Catalog format
123
+
124
+ `catalog/<surface>/<rule>.json`, validated against `catalog/schema.json` (the
125
+ schema is normative and travels with the spec). The normative statement of a
126
+ rule is its `title` + `intent`, and that prose is **stack-neutral by
127
+ construction**: plain prose, no docstring markup, no reference-implementation
128
+ symbols, package paths, marker spellings, or repo-internal pointers (sibling
129
+ rules are cited by catalog rule name). The reference realisation lives in the
130
+ non-normative fields (`enforcement`, `reference`, `opt_out`, `guide_topic`).
131
+ The standalone suite enforces the split
132
+ (`test_normative_prose_is_stack_neutral`).
133
+
134
+ | Field | Meaning |
135
+ |---|---|
136
+ | `id` | `<surface>/<rule>` — `backend/<snake_case>` (a `terp.arch` rule) or `frontend/<kebab-case>` (a boundary rule). **Findings are attributed to this id**, never to a tool-internal rule id. |
137
+ | `surface` | `backend` or `frontend`. |
138
+ | `title` | One-line statement of the invariant. |
139
+ | `intent` | Why the rule exists — the drift or threat it prevents. |
140
+ | `layer` | Cheapest faithful verification for a *new* stack: see below. |
141
+ | `enforcement` | How the reference implementation enforces it (first entry: the `build-time` check). A `runtime` entry names the fail-closed runtime control pairing with it (the two-layer discipline a Level 3 stack must reproduce for the rules that require it); a `black-box` entry names the `@terp/conformance` probe title. For frontend rules, `reported_as` is the ESLint rule id violations surface as — several catalog rules share one core rule id, so the adapter publishes a `catalogRuleId()` mapping and findings are attributed through it. |
142
+ | `runtime` | **Mandatory.** The rule's runtime-applicability classification (`required` / `not-applicable` / `deferred`) plus a `rationale` (mandatory for exemptions) and, for `deferred`, a `tracking` reference naming where the deferral is tracked — see “Runtime applicability” below. |
143
+ | `restricted_surface` | Frontend prohibition rules only: the `restricted-surface.json` keys the rule realises — the structural citation the spec suite resolves (every key must be claimed by some rule; a prose mention in `intent` must agree with the field). |
144
+ | `opt_out` | The *reference realisation* of the abstract escape-hatch contract (see below). |
145
+ | `reference` | Optional reference-implementation metadata: the compliant realisation the reference stack offers (component / helper names). **Not normative** — the `title` and `intent` state the stack-neutral invariant; another stack ships its own realisation. |
146
+ | `guide_topic` | Reference-implementation metadata (backend only): the `terp guide` topic teaching the compliant pattern. Not normative for other stacks. |
147
+ | `corpus` | Whether `corpus/<id>/` cases exist yet (the parity test holds this flag to the directory truth; a covered rule needs at least one `violation-*` **and** one `compliant-*` case). |
148
+
149
+ ### Enforcement layers
150
+
151
+ - **`black-box`** — the invariant is observable by probing a *running* app
152
+ (no source access needed). Portable to any stack via a conformance suite;
153
+ each such rule names its probe in a `black-box` enforcement entry
154
+ (`packages/frontend/conformance/tests/standard.spec.ts`).
155
+ - **`static-portable`** — expressible as source patterns a generic engine
156
+ (Semgrep/ast-grep-class) can realise per language from this spec.
157
+ - **`static-bespoke`** — needs deep framework knowledge (traits, `ModuleSpec`,
158
+ archetype slots); built per officially certified stack only.
159
+
160
+ The classification is a judgment about *porting cost*, not a limit on the
161
+ reference implementation — today every rule is enforced by `terp.arch` or
162
+ `@terp/eslint-boundaries` regardless of layer.
163
+
164
+ ### Runtime applicability (the two-layer discipline, per rule)
165
+
166
+ Whether a rule *additionally* pairs with a fail-closed runtime control is not a
167
+ blanket claim — it is recorded per rule in the mandatory `runtime` block and
168
+ held coherent by the spec suite:
169
+
170
+ - **`required`** — the invariant is observable in the running system and the
171
+ reference implementation owns a fail-closed control for it; the entry **must**
172
+ declare that control as a `kind: "runtime"` enforcement entry (whose `ref`
173
+ must resolve to a real symbol — the framework's parity test fails otherwise).
174
+ - **`not-applicable`** — the invariant is a property of the authored artifact
175
+ (source form, imports, justification markers, checked-in files) that is
176
+ erased or already materialised by the time the app runs, so no runtime seam
177
+ can enforce it. The mandatory `rationale` states why; the build-time check is
178
+ the control, by recorded decision.
179
+ - **`deferred`** — a runtime control would add independent fidelity on a seam
180
+ the reference framework owns, but has not shipped yet: an explicit, reviewed
181
+ gap, never a silent one. The mandatory `rationale` names the seam, and the
182
+ mandatory `tracking` reference names where the deferral is tracked (an issue
183
+ URL or the reference implementation's tracker document plus the seam) — so a
184
+ deferral has a lifecycle instead of being open-ended.
185
+
186
+ Fail-closed consistency (spec suite): `required` iff a `runtime` enforcement
187
+ entry exists; an exemption always carries a non-empty rationale; a deferral
188
+ always carries a tracking reference.
189
+
190
+ ### The escape-hatch contract
191
+
192
+ Every rule has **at most one** governed opt-out, and its *semantics* are the
193
+ normative part: a **justified inline marker** on (or immediately above) the
194
+ violating line that names the **catalog rule name** (the `<rule>` half of the
195
+ id — never a tool-internal rule id, mirroring findings attribution) and states
196
+ a reason; an unjustified marker is itself a violation; marker counts must
197
+ exactly match a checked-in per-app budget that can only shrink (the ratchet).
198
+ The `opt_out` field records the reference realisation's concrete spelling
199
+ (`# arch-allow-<rule>: <reason>` in Python, `// terp-allow-<rule>: <reason>`
200
+ in TypeScript) — the spelling is derived from the rule id, and the spec suite
201
+ holds the derivation. The escape-hatch **governance rules themselves** (the
202
+ budget ratchet, the ungoverned-marker condition) carry no `opt_out`:
203
+ governance cannot be waived by the mechanism it governs. Another stack
204
+ implements the same contract with its own comment syntax.
205
+
206
+ The `<reason>` is free text, and MAY carry structured metadata tokens so a
207
+ long-lived exception stays visible and auditable rather than eternal:
208
+ `owner:<who>` (who answers for the exception), `ticket:<ref>` (where its
209
+ removal is tracked), and `review-by:<YYYY-MM-DD>` (when it must be re-justified).
210
+ The tokens are a convention, not a gate — the marker's semantics are unchanged,
211
+ and a checker MUST NOT reject a reason without them — but a toolchain SHOULD
212
+ surface expired `review-by:` dates in its reporting.
213
+
214
+ ## Finding format
215
+
216
+ A conformant checker emits an array of findings per checked tree, shaped by
217
+ `findings.schema.json`: each finding names the **catalog rule id** it realises
218
+ (`rule`), the file `path` relative to the checked tree's root, and optionally a
219
+ `line`, a directive `message`, a `fix_hint` (the compliant construct, for agent
220
+ consumers acting on findings without re-reading the catalog), and a
221
+ `fingerprint` (a stable, checker-chosen per-instance identifier so a finding
222
+ can be tracked across line-shifting edits). Attribution is always to the
223
+ stack-neutral catalog id — the reference ESLint adapter, whose core rule ids
224
+ are shared between several catalog rules, publishes this mapping as
225
+ `catalogRuleId()` in `@terp/eslint-boundaries`.
226
+
227
+ ## Check-report format
228
+
229
+ Findings alone cannot support a per-rule verdict: a rule with zero findings is
230
+ only *passing* if the run actually evaluated it, and the consumer must never
231
+ supply that knowledge itself — its own catalog copy can be newer or older than
232
+ the checked app's pinned toolchain (version skew), and some enforcement is
233
+ conditional (an escape-hatch budget only when one is checked in, a layout
234
+ contract only when the app opts in). So a conformant checker reports a whole
235
+ run as one **application check report** (`app-check-report.schema.json`): a
236
+ format marker (`terp_check_report: 1`), the **spec version** its rule ids
237
+ resolve against, the **checker identity** (the same identity its certification
238
+ scorecard carries), the run **verdict** (`ok`, or an explicit `error` for a
239
+ run that failed to complete — an erroring run never claims ok and never claims
240
+ rules it did not finish evaluating), the **evaluated-rule inventory**
241
+ (`rules`), the opt-in rules published as **`not_applicable`** (their own
242
+ state — never passing, never unknown), the **findings** (exactly the finding
243
+ format's item shape — the spec suite holds the two identical), and
244
+ **`unattributed`** messages (diagnostics outside the standard, surfaced rather
245
+ than dropped). A consumer joins per-rule verdicts to the catalog exclusively
246
+ through the report's inventory, fail closed: pass = evaluated with no
247
+ attributed finding; a rule the run did not publish renders unknown, never
248
+ green. A multi-surface toolchain emits one report per checker run and a
249
+ consumer merges reports through their inventories.
250
+
251
+ ## Scorecard format
252
+
253
+ A checker claiming certification emits a scorecard (`scorecard.schema.json`):
254
+ the spec version it certified against, the checker's identity, and one entry
255
+ per claimed rule with its pass/fail verdict over the corpus and the residuals
256
+ it relies on (which must be a subset of `corpus/RESIDUALS.json` for the rule —
257
+ claiming an unrecorded residual is a conformance failure). A consumer can
258
+ re-run the corpus and reproduce the scorecard, making the certification claim
259
+ verifiable.
260
+
261
+ ## Assurance profile
262
+
263
+ Conformance to the rule catalog proves the *Terp-specific* half of release
264
+ readiness. The **assurance profile** (`assurance-profile.schema.json`) is the
265
+ machine-readable composition of that with the generic evidence lanes a release
266
+ also stands on — one document a toolchain emits from its release verification
267
+ profile, so "this build is releasable" becomes a checkable artifact instead of
268
+ a habit.
269
+
270
+ The lane vocabulary and each lane's requirement level are **normative** and
271
+ fixed here — an emitter cannot demote a required lane, which is why the
272
+ requirement level is deliberately not a field of the document:
273
+
274
+ | Lane | Requirement | Evidence |
275
+ |---|---|---|
276
+ | `terp-standard` | **required** | The standard's own enforcement surfaces ran and passed: the architecture gate and the frontend boundary lint, publishing their evaluated-rule inventories (check reports). |
277
+ | `appsec-baseline` | **required** | The delegated generic AppSec baseline passed (the reference realisation: `ruff` with the flake8-bandit `S` rules). |
278
+ | `dependency-audit` | **required** | Dependency trees were audited against known-vulnerability databases (the reference realisation: `pip-audit` and `npm audit`). |
279
+ | `a11y` | recommended | Automated accessibility checks over the running app (e.g. axe). |
280
+ | `blackbox-conformance` | recommended | The black-box behavioural conformance suite over the running workbench. |
281
+
282
+ The claim (`ok`) is true exactly when every **required** lane passed;
283
+ recommended lanes inform the reader but never carry the claim. Every lane of
284
+ the vocabulary appears exactly once — a lane the toolchain does not realise is
285
+ reported `not-run` (with no composing checks), never dropped and never counted
286
+ as passed. Each realised lane names the verification-check ids whose verdicts
287
+ compose it, so a consumer can trace the claim into the toolchain's own
288
+ verification envelope.
289
+
290
+ ## Corpus format
291
+
292
+ ```text
293
+ corpus/backend/<rule>/violation-01/ # files rooted at the app package root
294
+ corpus/backend/<rule>/compliant-01/ # e.g. modules/notes/service.py
295
+ corpus/frontend/<rule>/violation-01/ # files rooted at the frontend app root
296
+ corpus/frontend/<rule>/compliant-01/ # e.g. src/modules/widgets/Widget.tsx
297
+ ```
298
+
299
+ A case may ship an extra checker input at its root when the rule takes one:
300
+ the `backend/escape_hatch_budget` cases carry the `escape-hatch-budget.json`
301
+ the ratchet is verified against, and a frontend layout-contract case activates
302
+ via a `layout-contract.json` at its root.
303
+
304
+ A violation case MAY additionally ship an **`expected-findings.json`** at its
305
+ root: the exact findings (rule + path + line, shaped by
306
+ `findings.schema.json`) a maximally-precise checker emits for the rule under
307
+ test. A harness may assert the manifest exactly — hardening the per-case
308
+ contract from "flags something" to "flags the right line" — while the loose
309
+ contract below remains the floor for cases without one (and the ceiling a
310
+ checker must reach to conform). The spec suite holds every checked-in manifest
311
+ to the finding shape and to the case it sits in.
312
+
313
+ The contract for a checker claiming conformance for a rule is stated **per
314
+ rule**, over the finding format above:
315
+
316
+ - every `violation-*` case produces at least one finding attributed to that
317
+ rule's catalog id, and
318
+ - every `compliant-*` case produces **no** findings for that rule.
319
+
320
+ A compliant case is minimal, not a full application — it may legitimately be
321
+ incomplete with respect to *other* rules, so a multi-rule checker scopes the
322
+ corpus run to the rule under test. (The reference frontend harness happens to
323
+ hold its compliant cases fully clean across all boundary rules — a stricter
324
+ bar than the contract requires.)
325
+
326
+ ### Detector boundaries (what the corpus deliberately does not require)
327
+
328
+ The executable corpus **is** the interoperability contract: a checker is
329
+ conformant for a rule when it flags every `violation-*` case and stays silent
330
+ on every `compliant-*` case — nothing more. The violation cases include the
331
+ evasion shapes the rules are expected to see through (qualified/attribute
332
+ calls, multiline constructs, values routed through a local variable or
333
+ `.format()`/`%` building, aliased and parenthesized imports, computed
334
+ `window["fetch"]`-style member access for the egress family), and the
335
+ compliant cases pin the near-misses that must **not** fire (adjacent-literal
336
+ SQL that merely looks concatenated, credential-shaped names with dynamic
337
+ values, `*_mock`/`socketserver`-style name cousins, member calls like
338
+ `repo.fetch(...)` / `interpreter.eval(...)` on local objects, forbidden syntax
339
+ quoted inside comments and strings).
340
+
341
+ Some statically-erased or renamed forms are **deliberately outside** the
342
+ contract — known limits of precise, low-false-positive detection, kept out of
343
+ the corpus so a second implementation neither over-fits nor under-claims.
344
+ These residuals are recorded per rule in **`corpus/RESIDUALS.json`** — a
345
+ machine-readable ratchet governed like `corpus/PENDING.json`: the list only
346
+ shrinks, and closing a residual means seeding the corpus case that contracts
347
+ it and deleting the entry, never silently. Today it records:
348
+
349
+ - an alias-renamed symbol import (`from sqlalchemy import text as sql_text`)
350
+ is not required to be resolved to `text`;
351
+ - dynamic import (`importlib.import_module("httpx")`) is not required to be
352
+ seen as an import;
353
+ - a computed sink or global (`el["innerHTML"] = …`, `window["eval"]`) is not
354
+ required to be recognised for the *eval/DOM-sink* rules (the egress family
355
+ **does** contract the computed forms — its cases include them);
356
+ - `Function("…")` called without `new`, and egress through receivers other
357
+ than `window`/`globalThis` (`self.fetch`), are not required.
358
+
359
+ These residuals are governed the usual way: the escape-hatch contract makes
360
+ sanctioned exceptions visible, and the paired runtime controls (where
361
+ `runtime.applicability` is `required`) hold the invariant regardless of
362
+ spelling. Widening a detector past the contract is always allowed — the
363
+ corpus states the floor, not the ceiling.
364
+
365
+ The reference implementations are themselves held to this contract in CI:
366
+ `test_spec_corpus.py` runs each catalogued `terp.arch` rule over the backend
367
+ corpus, and `corpus.test.js` runs the ESLint adapter over the frontend corpus,
368
+ attributing findings via `catalogRuleId()`. Corpus sample files are
369
+ intentionally violating code — they are excluded from repo-wide lint
370
+ (`ruff` `extend-exclude`) and are never imported or executed.
371
+
372
+ Coverage is governed by **`corpus/PENDING.json`**: the exact list of rules
373
+ still without cases. Seeding a rule's corpus removes it from the list; a new
374
+ rule shipped without corpus must be listed there explicitly — so uncovered
375
+ rules stay visible and the list only shrinks. Every `static-portable` backend
376
+ rule must have cases (enforced by the parity test); today every backend and
377
+ frontend rule has cases and the pending list is empty — it exists so a *new*
378
+ rule can ship with its gap explicit and reviewed.
379
+
380
+ ## Conformance levels
381
+
382
+ - **Level 1 — black-box:** the app passes the runnable conformance suite
383
+ (`@terp/conformance`) for the capabilities it claims, including the
384
+ `standard:` probes the `black-box` catalog entries name.
385
+ - **Level 2 — static rule pack:** additionally, a checker validated against
386
+ this corpus enforces the `static-portable` rules for the app's language(s),
387
+ emitting findings per `findings.schema.json`.
388
+ - **Level 3 — full harness:** additionally, the `static-bespoke` rules, the
389
+ paired `runtime` controls of every rule whose `runtime.applicability` is
390
+ `required`, and the governed escape-hatch budget ratchet are enforced (today:
391
+ the `terp.arch` + `@terp/eslint-boundaries` reference harness on top of
392
+ `terp.core` / `@terp/react-core`).
393
+
394
+ ## Growing the spec
395
+
396
+ - **New rule** → ship the rule and its catalog entry together (the parity test
397
+ fails otherwise); classify its runtime applicability deliberately (`required`
398
+ with the declared control, or an exemption with its rationale — a deferral
399
+ also names its `tracking` reference); seed corpus cases when the rule is
400
+ portable, or list it in `corpus/PENDING.json` explicitly; regenerate the
401
+ rule pages (`python tools/generate_rule_docs.py`).
402
+ - **New corpus cases** → add `violation-*`/`compliant-*` directories, flip the
403
+ entry's `corpus` flag to `true`, and drop the rule from `corpus/PENDING.json`;
404
+ the harness picks the cases up by convention. Ship an `expected-findings.json`
405
+ when the violating lines are pinned. Closing a documented detector residual
406
+ also deletes its `corpus/RESIDUALS.json` entry.
407
+ - **New stack** → implement the `static-portable` rules however you like; the
408
+ corpus is the acceptance test, findings must attribute to catalog ids, and
409
+ the certification summary is a scorecard (`scorecard.schema.json`).
410
+ - **Format change** → bump `VERSION` and add the matching `CHANGELOG.md` entry
411
+ (the suite holds the top entry to the version). Pre-1.0, a **changed
412
+ contract** — a new mandatory catalog field (0.5.0's `runtime` block), a
413
+ changed finding shape —
414
+ bumps the **minor** (the strongest signal 0.x semver carries; a 0.x major
415
+ would claim a stability this spec does not yet promise); purely additive
416
+ fields and new rules also bump the minor; prose bumps the patch. From 1.0.0
417
+ a changed contract bumps the **major**.