@terpjs/spec 0.14.0 → 0.16.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.
- package/LICENSE +201 -201
- package/README.md +417 -417
- package/VERSION +1 -1
- package/app-check-report.schema.json +122 -122
- package/assurance-profile.schema.json +70 -70
- package/catalog/backend/alembic_downgrades_not_empty.json +22 -22
- package/catalog/backend/base_query_not_overridden.json +27 -27
- package/catalog/backend/canonical_module_shape.json +22 -22
- package/catalog/backend/datetime_columns_are_timezone_aware.json +23 -23
- package/catalog/backend/escape_hatch_budget.json +21 -21
- package/catalog/backend/events_reference_catalog.json +27 -27
- package/catalog/backend/input_schemas_exclude_managed_columns.json +27 -27
- package/catalog/backend/input_str_fields_have_max_length.json +22 -22
- package/catalog/backend/jobs_reference_catalog.json +27 -27
- package/catalog/backend/list_routes_paginate.json +32 -32
- package/catalog/backend/modules_declare_policy.json +26 -26
- package/catalog/backend/mutations_emit_audit.json +26 -26
- package/catalog/backend/mutations_require_write_role.json +32 -32
- package/catalog/backend/no_adhoc_background_runtime.json +22 -22
- package/catalog/backend/no_adhoc_config_decrypt.json +27 -27
- package/catalog/backend/no_adhoc_logging_config.json +22 -22
- package/catalog/backend/no_adhoc_middleware.json +27 -27
- package/catalog/backend/no_adhoc_permission_literals.json +27 -27
- package/catalog/backend/no_app_instantiation.json +22 -22
- package/catalog/backend/no_blocking_sleep.json +22 -22
- package/catalog/backend/no_cross_module_imports.json +21 -21
- package/catalog/backend/no_dependency_overrides.json +27 -27
- package/catalog/backend/no_destructive_migrations.json +22 -22
- package/catalog/backend/no_dynamic_sql.json +22 -22
- package/catalog/backend/no_empty_tests.json +22 -22
- package/catalog/backend/no_eval_or_exec.json +22 -22
- package/catalog/backend/no_hardcoded_credentials.json +21 -21
- package/catalog/backend/no_internal_imports.json +22 -22
- package/catalog/backend/no_manual_actor_stamping.json +27 -27
- package/catalog/backend/no_manual_ownership_checks.json +32 -32
- package/catalog/backend/no_manual_scope_filtering.json +27 -27
- package/catalog/backend/no_manual_table_schema.json +22 -22
- package/catalog/backend/no_manual_version_assignment.json +22 -22
- package/catalog/backend/no_mutable_default_args.json +22 -22
- package/catalog/backend/no_naive_datetime.json +22 -22
- package/catalog/backend/no_oversized_python_files.json +22 -22
- package/catalog/backend/no_print.json +22 -22
- package/catalog/backend/no_raw_app_routes.json +26 -26
- package/catalog/backend/no_raw_connection_access.json +27 -27
- package/catalog/backend/no_raw_file_references.json +27 -27
- package/catalog/backend/no_raw_outbound_http.json +22 -22
- package/catalog/backend/no_raw_session_construction.json +22 -22
- package/catalog/backend/no_star_imports.json +22 -22
- package/catalog/backend/no_todo_fixme.json +22 -22
- package/catalog/backend/no_unique_columns_on_soft_delete_models.json +22 -22
- package/catalog/backend/offset_queries_declare_ordering.json +22 -22
- package/catalog/backend/path_id_params_are_uuid.json +22 -22
- package/catalog/backend/policy_refs_resolve.json +27 -27
- package/catalog/backend/public_modules_are_read_only.json +26 -26
- package/catalog/backend/reads_use_base_query.json +27 -27
- package/catalog/backend/response_model_not_table_model.json +27 -27
- package/catalog/backend/routes_declare_response_model.json +27 -27
- package/catalog/backend/safe_methods_are_read_only.json +31 -31
- package/catalog/backend/schemas_exclude_sensitive_fields.json +32 -32
- package/catalog/backend/session_imported_from_sqlmodel.json +22 -22
- package/catalog/backend/table_models_use_base_table.json +22 -22
- package/catalog/backend/tables_have_migrations.json +27 -27
- package/catalog/backend/tenant_scoped_models_use_scoped_service.json +22 -22
- package/catalog/backend/ungoverned_escape_hatch.json +21 -21
- package/catalog/backend/update_schemas_inherit_base_update_schema.json +32 -32
- package/catalog/frontend/escape-hatch.json +21 -21
- package/catalog/frontend/generated-client-only.json +26 -26
- package/catalog/frontend/layout-contract.json +26 -26
- package/catalog/frontend/no-cross-module-imports.json +21 -21
- package/catalog/frontend/no-deep-imports.json +25 -25
- package/catalog/frontend/no-dom-html-injection.json +22 -22
- package/catalog/frontend/no-eval.json +21 -21
- package/catalog/frontend/no-inline-styling.json +25 -25
- package/catalog/frontend/no-style-imports.json +24 -24
- package/catalog/frontend/no-unsafe-href.json +21 -21
- package/catalog/frontend/no-unsafe-target-blank.json +21 -21
- package/catalog/frontend/router-links.json +22 -22
- package/catalog/frontend/token-styled-elements.json +25 -25
- package/catalog/schema.json +95 -95
- package/corpus/PENDING.json +4 -4
- package/corpus/RESIDUALS.json +21 -21
- package/corpus/backend/alembic_downgrades_not_empty/compliant-01/modules/notes/migrations/versions/0001_change.py +6 -6
- package/corpus/backend/alembic_downgrades_not_empty/violation-01/modules/notes/migrations/versions/0001_change.py +6 -6
- package/corpus/backend/base_query_not_overridden/compliant-01/modules/notes/service.py +8 -8
- package/corpus/backend/base_query_not_overridden/compliant-02/modules/notes/service.py +8 -8
- package/corpus/backend/base_query_not_overridden/violation-01/modules/notes/service.py +8 -8
- package/corpus/backend/base_query_not_overridden/violation-02/expected-findings.json +7 -7
- package/corpus/backend/base_query_not_overridden/violation-02/modules/notes/service.py +8 -8
- package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/models.py +6 -6
- package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/module.py +7 -7
- package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/router.py +3 -3
- package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/schemas.py +10 -10
- package/corpus/backend/canonical_module_shape/compliant-01/modules/notes/service.py +5 -5
- package/corpus/backend/canonical_module_shape/violation-01/modules/notes/router.py +3 -3
- package/corpus/backend/datetime_columns_are_timezone_aware/compliant-01/modules/notes/models.py +12 -12
- package/corpus/backend/datetime_columns_are_timezone_aware/violation-01/modules/notes/models.py +12 -12
- package/corpus/backend/datetime_columns_are_timezone_aware/violation-02/modules/notes/models.py +13 -13
- package/corpus/backend/datetime_columns_are_timezone_aware/violation-03/modules/notes/models.py +15 -15
- package/corpus/backend/escape_hatch_budget/compliant-01/escape-hatch-budget.json +3 -3
- package/corpus/backend/escape_hatch_budget/compliant-01/modules/notes/service.py +2 -2
- package/corpus/backend/escape_hatch_budget/compliant-02/escape-hatch-budget.json +1 -1
- package/corpus/backend/escape_hatch_budget/compliant-02/modules/notes/service.py +4 -4
- package/corpus/backend/escape_hatch_budget/violation-01/escape-hatch-budget.json +1 -1
- package/corpus/backend/escape_hatch_budget/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/escape_hatch_budget/violation-02/escape-hatch-budget.json +3 -3
- package/corpus/backend/escape_hatch_budget/violation-02/modules/notes/service.py +4 -4
- package/corpus/backend/events_reference_catalog/compliant-01/modules/notes/module.py +9 -9
- package/corpus/backend/events_reference_catalog/violation-01/modules/notes/module.py +8 -8
- package/corpus/backend/input_schemas_exclude_managed_columns/compliant-01/modules/notes/schemas.py +6 -6
- package/corpus/backend/input_schemas_exclude_managed_columns/violation-01/expected-findings.json +7 -7
- package/corpus/backend/input_schemas_exclude_managed_columns/violation-01/modules/notes/schemas.py +9 -9
- package/corpus/backend/input_schemas_exclude_managed_columns/violation-02/expected-findings.json +12 -12
- package/corpus/backend/input_schemas_exclude_managed_columns/violation-02/modules/notes/router.py +14 -14
- package/corpus/backend/input_str_fields_have_max_length/compliant-01/modules/notes/schemas.py +4 -4
- package/corpus/backend/input_str_fields_have_max_length/violation-01/modules/notes/schemas.py +2 -2
- package/corpus/backend/jobs_reference_catalog/compliant-01/modules/notes/service.py +5 -5
- package/corpus/backend/jobs_reference_catalog/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/list_routes_paginate/compliant-01/modules/notes/router.py +6 -6
- package/corpus/backend/list_routes_paginate/violation-01/modules/notes/router.py +3 -3
- package/corpus/backend/modules_declare_policy/compliant-01/modules/notes/module.py +7 -7
- package/corpus/backend/modules_declare_policy/violation-01/modules/notes/module.py +6 -6
- package/corpus/backend/mutations_emit_audit/compliant-01/modules/notes/service.py +9 -9
- package/corpus/backend/mutations_emit_audit/violation-01/modules/notes/service.py +4 -4
- package/corpus/backend/mutations_require_write_role/compliant-01/modules/notes/module.py +7 -7
- package/corpus/backend/mutations_require_write_role/compliant-01/modules/notes/router.py +3 -3
- package/corpus/backend/mutations_require_write_role/compliant-02/modules/notes/module.py +7 -7
- package/corpus/backend/mutations_require_write_role/compliant-02/modules/notes/router.py +3 -3
- package/corpus/backend/mutations_require_write_role/violation-01/expected-findings.json +7 -7
- package/corpus/backend/mutations_require_write_role/violation-01/modules/notes/module.py +7 -7
- package/corpus/backend/mutations_require_write_role/violation-01/modules/notes/router.py +3 -3
- package/corpus/backend/mutations_require_write_role/violation-02/expected-findings.json +7 -7
- package/corpus/backend/mutations_require_write_role/violation-02/modules/notes/module.py +7 -7
- package/corpus/backend/mutations_require_write_role/violation-02/modules/notes/router.py +5 -5
- package/corpus/backend/mutations_require_write_role/violation-03/expected-findings.json +7 -7
- package/corpus/backend/mutations_require_write_role/violation-03/modules/notes/module.py +10 -10
- package/corpus/backend/mutations_require_write_role/violation-03/modules/notes/router.py +3 -3
- package/corpus/backend/no_adhoc_background_runtime/compliant-01/modules/notes/service.py +9 -9
- package/corpus/backend/no_adhoc_background_runtime/violation-01/modules/notes/service.py +5 -5
- package/corpus/backend/no_adhoc_background_runtime/violation-02/modules/notes/service.py +3 -3
- package/corpus/backend/no_adhoc_config_decrypt/compliant-01/modules/billing/service.py +2 -2
- package/corpus/backend/no_adhoc_config_decrypt/violation-01/modules/billing/service.py +2 -2
- package/corpus/backend/no_adhoc_logging_config/compliant-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_adhoc_logging_config/violation-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_adhoc_middleware/compliant-01/main.py +3 -3
- package/corpus/backend/no_adhoc_middleware/violation-01/main.py +4 -4
- package/corpus/backend/no_adhoc_middleware/violation-02/main.py +5 -5
- package/corpus/backend/no_adhoc_permission_literals/compliant-01/modules/notes/module.py +3 -3
- package/corpus/backend/no_adhoc_permission_literals/compliant-02/modules/notes/router.py +5 -5
- package/corpus/backend/no_adhoc_permission_literals/violation-01/expected-findings.json +7 -7
- package/corpus/backend/no_adhoc_permission_literals/violation-01/modules/notes/module.py +1 -1
- package/corpus/backend/no_adhoc_permission_literals/violation-02/expected-findings.json +7 -7
- package/corpus/backend/no_adhoc_permission_literals/violation-02/modules/notes/router.py +2 -2
- package/corpus/backend/no_adhoc_permission_literals/violation-03/expected-findings.json +12 -12
- package/corpus/backend/no_adhoc_permission_literals/violation-03/modules/notes/module.py +10 -10
- package/corpus/backend/no_app_instantiation/compliant-01/main.py +3 -3
- package/corpus/backend/no_app_instantiation/violation-01/main.py +3 -3
- package/corpus/backend/no_blocking_sleep/compliant-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_blocking_sleep/violation-01/modules/notes/service.py +5 -5
- package/corpus/backend/no_cross_module_imports/compliant-01/modules/a/service.py +1 -1
- package/corpus/backend/no_cross_module_imports/violation-01/modules/a/service.py +1 -1
- package/corpus/backend/no_cross_module_imports/violation-02/modules/a/service.py +1 -1
- package/corpus/backend/no_dependency_overrides/compliant-01/main.py +12 -12
- package/corpus/backend/no_dependency_overrides/violation-01/main.py +11 -11
- package/corpus/backend/no_destructive_migrations/compliant-01/modules/notes/migrations/versions/0001_change.py +3 -3
- package/corpus/backend/no_destructive_migrations/compliant-02/modules/notes/migrations/versions/0001_change.py +8 -8
- package/corpus/backend/no_destructive_migrations/violation-01/expected-findings.json +7 -7
- package/corpus/backend/no_destructive_migrations/violation-01/modules/notes/migrations/versions/0001_change.py +2 -2
- package/corpus/backend/no_destructive_migrations/violation-02/expected-findings.json +7 -7
- package/corpus/backend/no_destructive_migrations/violation-02/modules/notes/migrations/versions/0001_change.py +2 -2
- package/corpus/backend/no_destructive_migrations/violation-03/expected-findings.json +7 -7
- package/corpus/backend/no_destructive_migrations/violation-03/modules/notes/migrations/versions/0001_change.py +3 -3
- package/corpus/backend/no_destructive_migrations/violation-04/expected-findings.json +12 -12
- package/corpus/backend/no_destructive_migrations/violation-04/modules/notes/migrations/versions/0001_change.py +6 -6
- package/corpus/backend/no_destructive_migrations/violation-05/expected-findings.json +12 -12
- package/corpus/backend/no_destructive_migrations/violation-05/modules/notes/migrations/versions/0001_change.py +3 -3
- package/corpus/backend/no_dynamic_sql/compliant-01/modules/notes/service.py +1 -1
- package/corpus/backend/no_dynamic_sql/compliant-02/modules/notes/service.py +20 -20
- package/corpus/backend/no_dynamic_sql/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_dynamic_sql/violation-02/modules/notes/service.py +2 -2
- package/corpus/backend/no_dynamic_sql/violation-03/modules/notes/service.py +7 -7
- package/corpus/backend/no_dynamic_sql/violation-04/modules/notes/service.py +16 -16
- package/corpus/backend/no_empty_tests/compliant-01/tests/test_notes.py +2 -2
- package/corpus/backend/no_empty_tests/violation-01/tests/test_notes.py +2 -2
- package/corpus/backend/no_eval_or_exec/compliant-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_eval_or_exec/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_hardcoded_credentials/compliant-01/modules/billing/service.py +3 -3
- package/corpus/backend/no_hardcoded_credentials/compliant-02/modules/billing/service.py +18 -18
- package/corpus/backend/no_hardcoded_credentials/violation-01/expected-findings.json +7 -7
- package/corpus/backend/no_hardcoded_credentials/violation-01/modules/billing/service.py +2 -2
- package/corpus/backend/no_hardcoded_credentials/violation-02/expected-findings.json +7 -7
- package/corpus/backend/no_hardcoded_credentials/violation-02/modules/billing/service.py +1 -1
- package/corpus/backend/no_hardcoded_credentials/violation-03/expected-findings.json +22 -22
- package/corpus/backend/no_hardcoded_credentials/violation-03/modules/billing/service.py +11 -11
- package/corpus/backend/no_hardcoded_credentials/violation-04/expected-findings.json +12 -12
- package/corpus/backend/no_hardcoded_credentials/violation-04/modules/billing/service.py +8 -8
- package/corpus/backend/no_hardcoded_credentials/violation-05/expected-findings.json +12 -12
- package/corpus/backend/no_hardcoded_credentials/violation-05/modules/billing/service.py +4 -4
- package/corpus/backend/no_internal_imports/compliant-01/modules/notes/service.py +1 -1
- package/corpus/backend/no_internal_imports/violation-01/modules/notes/service.py +1 -1
- package/corpus/backend/no_manual_actor_stamping/compliant-01/modules/notes/service.py +5 -5
- package/corpus/backend/no_manual_actor_stamping/compliant-02/modules/notes/schemas.py +12 -12
- package/corpus/backend/no_manual_actor_stamping/violation-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_manual_actor_stamping/violation-02/expected-findings.json +7 -7
- package/corpus/backend/no_manual_actor_stamping/violation-02/modules/notes/service.py +4 -4
- package/corpus/backend/no_manual_ownership_checks/compliant-01/modules/journals/service.py +3 -3
- package/corpus/backend/no_manual_ownership_checks/compliant-02/modules/journals/schemas.py +11 -11
- package/corpus/backend/no_manual_ownership_checks/violation-01/modules/journals/service.py +3 -3
- package/corpus/backend/no_manual_ownership_checks/violation-02/expected-findings.json +7 -7
- package/corpus/backend/no_manual_ownership_checks/violation-02/modules/journals/service.py +9 -9
- package/corpus/backend/no_manual_ownership_checks/violation-03/expected-findings.json +7 -7
- package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/jobs.py +1 -1
- package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/models.py +7 -7
- package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/module.py +11 -11
- package/corpus/backend/no_manual_ownership_checks/violation-03/modules/notes/service.py +7 -7
- package/corpus/backend/no_manual_scope_filtering/compliant-01/modules/notes/service.py +8 -8
- package/corpus/backend/no_manual_scope_filtering/compliant-02/modules/notes/schemas.py +12 -12
- package/corpus/backend/no_manual_scope_filtering/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_manual_scope_filtering/violation-02/modules/notes/service.py +2 -2
- package/corpus/backend/no_manual_scope_filtering/violation-03/expected-findings.json +7 -7
- package/corpus/backend/no_manual_scope_filtering/violation-03/modules/notes/service.py +7 -7
- package/corpus/backend/no_manual_table_schema/compliant-01/modules/notes/models.py +6 -6
- package/corpus/backend/no_manual_table_schema/violation-01/modules/notes/models.py +8 -8
- package/corpus/backend/no_manual_version_assignment/compliant-01/modules/notes/service.py +5 -5
- package/corpus/backend/no_manual_version_assignment/violation-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_manual_version_assignment/violation-02/modules/notes/service.py +3 -3
- package/corpus/backend/no_manual_version_assignment/violation-03/modules/notes/service.py +3 -3
- package/corpus/backend/no_mutable_default_args/compliant-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_mutable_default_args/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_naive_datetime/compliant-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_naive_datetime/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_naive_datetime/violation-02/modules/notes/service.py +2 -2
- package/corpus/backend/no_oversized_python_files/compliant-01/modules/notes/service.py +9 -9
- package/corpus/backend/no_oversized_python_files/violation-01/modules/notes/service.py +530 -530
- package/corpus/backend/no_print/compliant-01/modules/notes/service.py +7 -7
- package/corpus/backend/no_print/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_raw_app_routes/compliant-01/main.py +12 -12
- package/corpus/backend/no_raw_app_routes/violation-01/main.py +10 -10
- package/corpus/backend/no_raw_app_routes/violation-02/main.py +16 -16
- package/corpus/backend/no_raw_app_routes/violation-03/main.py +12 -12
- package/corpus/backend/no_raw_app_routes/violation-04/main.py +10 -10
- package/corpus/backend/no_raw_app_routes/violation-05/main.py +12 -12
- package/corpus/backend/no_raw_app_routes/violation-06/main.py +10 -10
- package/corpus/backend/no_raw_connection_access/compliant-01/modules/notes/service.py +5 -5
- package/corpus/backend/no_raw_connection_access/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_raw_connection_access/violation-02/modules/notes/service.py +2 -2
- package/corpus/backend/no_raw_file_references/compliant-01/modules/notes/models.py +10 -10
- package/corpus/backend/no_raw_file_references/violation-01/modules/notes/models.py +9 -9
- package/corpus/backend/no_raw_outbound_http/compliant-01/modules/notes/service.py +2 -2
- package/corpus/backend/no_raw_outbound_http/compliant-03/modules/notes/service.py +12 -12
- package/corpus/backend/no_raw_outbound_http/violation-01/expected-findings.json +7 -7
- package/corpus/backend/no_raw_outbound_http/violation-01/modules/notes/service.py +1 -1
- package/corpus/backend/no_raw_outbound_http/violation-02/expected-findings.json +7 -7
- package/corpus/backend/no_raw_outbound_http/violation-02/modules/notes/service.py +1 -1
- package/corpus/backend/no_raw_outbound_http/violation-03/expected-findings.json +7 -7
- package/corpus/backend/no_raw_outbound_http/violation-03/modules/notes/service.py +1 -1
- package/corpus/backend/no_raw_outbound_http/violation-04/expected-findings.json +12 -12
- package/corpus/backend/no_raw_outbound_http/violation-04/modules/notes/service.py +7 -7
- package/corpus/backend/no_raw_outbound_http/violation-05/expected-findings.json +12 -12
- package/corpus/backend/no_raw_outbound_http/violation-05/modules/notes/service.py +5 -5
- package/corpus/backend/no_raw_outbound_http/violation-06/expected-findings.json +12 -12
- package/corpus/backend/no_raw_outbound_http/violation-06/modules/notes/service.py +2 -2
- package/corpus/backend/no_raw_session_construction/compliant-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_raw_session_construction/violation-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_star_imports/compliant-01/modules/notes/service.py +1 -1
- package/corpus/backend/no_star_imports/violation-01/modules/notes/service.py +1 -1
- package/corpus/backend/no_todo_fixme/compliant-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_todo_fixme/violation-01/modules/notes/service.py +3 -3
- package/corpus/backend/no_unique_columns_on_soft_delete_models/compliant-01/modules/notes/models.py +17 -17
- package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-01/expected-findings.json +7 -7
- package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-01/modules/notes/models.py +6 -6
- package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-02/expected-findings.json +7 -7
- package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-02/modules/notes/models.py +13 -13
- package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-03/expected-findings.json +7 -7
- package/corpus/backend/no_unique_columns_on_soft_delete_models/violation-03/modules/notes/models.py +16 -16
- package/corpus/backend/offset_queries_declare_ordering/compliant-01/modules/notes/service.py +4 -4
- package/corpus/backend/offset_queries_declare_ordering/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/path_id_params_are_uuid/compliant-01/modules/notes/router.py +6 -6
- package/corpus/backend/path_id_params_are_uuid/violation-01/modules/notes/router.py +6 -6
- package/corpus/backend/policy_refs_resolve/compliant-01/modules/notes/module.py +7 -7
- package/corpus/backend/policy_refs_resolve/violation-01/modules/notes/module.py +8 -8
- package/corpus/backend/public_modules_are_read_only/compliant-01/modules/notes/module.py +7 -7
- package/corpus/backend/public_modules_are_read_only/compliant-01/modules/notes/router.py +3 -3
- package/corpus/backend/public_modules_are_read_only/violation-01/expected-findings.json +7 -7
- package/corpus/backend/public_modules_are_read_only/violation-01/modules/notes/module.py +7 -7
- package/corpus/backend/public_modules_are_read_only/violation-01/modules/notes/router.py +3 -3
- package/corpus/backend/public_modules_are_read_only/violation-02/expected-findings.json +7 -7
- package/corpus/backend/public_modules_are_read_only/violation-02/modules/notes/module.py +7 -7
- package/corpus/backend/public_modules_are_read_only/violation-02/modules/notes/router.py +5 -5
- package/corpus/backend/reads_use_base_query/compliant-01/modules/notes/models.py +6 -6
- package/corpus/backend/reads_use_base_query/compliant-01/modules/notes/service.py +8 -8
- package/corpus/backend/reads_use_base_query/compliant-02/modules/notes/models.py +9 -9
- package/corpus/backend/reads_use_base_query/compliant-02/modules/notes/service.py +9 -9
- package/corpus/backend/reads_use_base_query/violation-01/modules/notes/models.py +6 -6
- package/corpus/backend/reads_use_base_query/violation-01/modules/notes/service.py +9 -9
- package/corpus/backend/reads_use_base_query/violation-02/expected-findings.json +7 -7
- package/corpus/backend/reads_use_base_query/violation-02/modules/notes/models.py +6 -6
- package/corpus/backend/reads_use_base_query/violation-02/modules/notes/service.py +8 -8
- package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/models.py +6 -6
- package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/router.py +3 -3
- package/corpus/backend/response_model_not_table_model/compliant-01/modules/notes/schemas.py +5 -5
- package/corpus/backend/response_model_not_table_model/violation-01/modules/notes/models.py +6 -6
- package/corpus/backend/response_model_not_table_model/violation-01/modules/notes/router.py +3 -3
- package/corpus/backend/routes_declare_response_model/compliant-01/modules/notes/router.py +8 -8
- package/corpus/backend/routes_declare_response_model/violation-01/modules/notes/router.py +3 -3
- package/corpus/backend/safe_methods_are_read_only/compliant-01/modules/notes/router.py +6 -6
- package/corpus/backend/safe_methods_are_read_only/compliant-02/modules/notes/router.py +5 -5
- package/corpus/backend/safe_methods_are_read_only/violation-01/expected-findings.json +7 -7
- package/corpus/backend/safe_methods_are_read_only/violation-01/modules/notes/router.py +3 -3
- package/corpus/backend/safe_methods_are_read_only/violation-02/expected-findings.json +7 -7
- package/corpus/backend/safe_methods_are_read_only/violation-02/modules/notes/router.py +5 -5
- package/corpus/backend/safe_methods_are_read_only/violation-03/expected-findings.json +7 -7
- package/corpus/backend/safe_methods_are_read_only/violation-03/modules/notes/router.py +3 -3
- package/corpus/backend/schemas_exclude_sensitive_fields/compliant-01/modules/users/schemas.py +5 -5
- package/corpus/backend/schemas_exclude_sensitive_fields/compliant-02/modules/accounts/schemas.py +12 -12
- package/corpus/backend/schemas_exclude_sensitive_fields/violation-01/expected-findings.json +7 -7
- package/corpus/backend/schemas_exclude_sensitive_fields/violation-01/modules/users/schemas.py +3 -3
- package/corpus/backend/schemas_exclude_sensitive_fields/violation-02/expected-findings.json +7 -7
- package/corpus/backend/schemas_exclude_sensitive_fields/violation-02/modules/connectors/router.py +8 -8
- package/corpus/backend/schemas_exclude_sensitive_fields/violation-03/expected-findings.json +17 -17
- package/corpus/backend/schemas_exclude_sensitive_fields/violation-03/modules/integrations/schemas.py +7 -7
- package/corpus/backend/session_imported_from_sqlmodel/compliant-01/modules/notes/service.py +5 -5
- package/corpus/backend/session_imported_from_sqlmodel/violation-01/modules/notes/service.py +5 -5
- package/corpus/backend/table_models_use_base_table/compliant-01/modules/notes/models.py +6 -6
- package/corpus/backend/table_models_use_base_table/violation-01/modules/notes/models.py +5 -5
- package/corpus/backend/tables_have_migrations/compliant-01/modules/notes/migrations/versions/0a1b2c3d4e5f_create_notes_tables.py +22 -22
- package/corpus/backend/tables_have_migrations/compliant-01/modules/notes/models.py +6 -6
- package/corpus/backend/tables_have_migrations/compliant-02/capabilities/ledger/models.py +6 -6
- package/corpus/backend/tables_have_migrations/violation-01/expected-findings.json +7 -7
- package/corpus/backend/tables_have_migrations/violation-01/modules/notes/models.py +6 -6
- package/corpus/backend/tables_have_migrations/violation-02/expected-findings.json +7 -7
- package/corpus/backend/tables_have_migrations/violation-02/modules/notes/models.py +10 -10
- package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-01/modules/projects/models.py +6 -6
- package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-01/modules/projects/service.py +5 -5
- package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-02/modules/projects/models.py +8 -8
- package/corpus/backend/tenant_scoped_models_use_scoped_service/compliant-02/modules/projects/service.py +5 -5
- package/corpus/backend/tenant_scoped_models_use_scoped_service/violation-01/modules/projects/models.py +6 -6
- package/corpus/backend/tenant_scoped_models_use_scoped_service/violation-01/modules/projects/service.py +5 -5
- package/corpus/backend/ungoverned_escape_hatch/compliant-01/modules/notes/service.py +5 -5
- package/corpus/backend/ungoverned_escape_hatch/compliant-02/modules/notes/service.py +4 -4
- package/corpus/backend/ungoverned_escape_hatch/violation-01/modules/notes/service.py +2 -2
- package/corpus/backend/update_schemas_inherit_base_update_schema/compliant-01/modules/notes/schemas.py +2 -2
- package/corpus/backend/update_schemas_inherit_base_update_schema/violation-01/modules/notes/schemas.py +2 -2
- package/corpus/backend/update_schemas_inherit_base_update_schema/violation-02/modules/notes/schemas.py +5 -5
- package/corpus/frontend/escape-hatch/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/escape-hatch/violation-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/escape-hatch/violation-02/src/modules/widgets/Widget.tsx +7 -7
- package/corpus/frontend/escape-hatch/violation-03/src/modules/widgets/Widget.tsx +7 -7
- package/corpus/frontend/generated-client-only/compliant-01/src/modules/widgets/Widget.tsx +6 -6
- package/corpus/frontend/generated-client-only/compliant-02/src/modules/widgets/Widget.tsx +21 -21
- package/corpus/frontend/generated-client-only/compliant-03/src/modules/widgets/Widget.tsx +7 -7
- package/corpus/frontend/generated-client-only/compliant-04/src/modules/widgets/Widget.tsx +17 -17
- package/corpus/frontend/generated-client-only/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/generated-client-only/violation-02/src/modules/widgets/Widget.tsx +9 -9
- package/corpus/frontend/generated-client-only/violation-03/src/modules/widgets/Widget.tsx +14 -14
- package/corpus/frontend/generated-client-only/violation-04/src/modules/widgets/Widget.tsx +6 -6
- package/corpus/frontend/layout-contract/compliant-01/layout-contract.json +3 -3
- package/corpus/frontend/layout-contract/compliant-01/src/modules/widgets/Widget.tsx +8 -8
- package/corpus/frontend/layout-contract/violation-01/layout-contract.json +3 -3
- package/corpus/frontend/layout-contract/violation-01/src/modules/widgets/Widget.tsx +8 -8
- package/corpus/frontend/no-cross-module-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/no-cross-module-imports/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/no-deep-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/no-deep-imports/violation-01/src/modules/widgets/Widget.tsx +2 -2
- package/corpus/frontend/no-dom-html-injection/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/no-dom-html-injection/compliant-02/src/modules/widgets/Widget.tsx +11 -11
- package/corpus/frontend/no-dom-html-injection/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/no-dom-html-injection/violation-02/src/modules/widgets/Widget.tsx +7 -7
- package/corpus/frontend/no-dom-html-injection/violation-03/src/modules/widgets/Widget.tsx +12 -12
- package/corpus/frontend/no-eval/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/no-eval/compliant-02/src/modules/widgets/Widget.tsx +11 -11
- package/corpus/frontend/no-eval/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/no-eval/violation-02/src/modules/widgets/Widget.tsx +7 -7
- package/corpus/frontend/no-eval/violation-03/src/modules/widgets/Widget.tsx +10 -10
- package/corpus/frontend/no-eval/violation-04/src/modules/widgets/Widget.tsx +6 -6
- package/corpus/frontend/no-inline-styling/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/no-inline-styling/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/no-inline-styling/violation-02/src/modules/widgets/Widget.tsx +6 -6
- package/corpus/frontend/no-style-imports/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/no-style-imports/violation-01/src/modules/widgets/Widget.tsx +2 -2
- package/corpus/frontend/no-unsafe-href/compliant-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/no-unsafe-href/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/no-unsafe-target-blank/compliant-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/no-unsafe-target-blank/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/router-links/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/router-links/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/corpus/frontend/token-styled-elements/compliant-01/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/token-styled-elements/compliant-02/src/modules/widgets/Widget.tsx +4 -4
- package/corpus/frontend/token-styled-elements/violation-01/src/modules/widgets/Widget.tsx +3 -3
- package/findings.schema.json +40 -40
- package/package.json +23 -23
- package/restricted-surface.json +10 -10
- 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 `@
|
|
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
|
-
`@
|
|
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 `@
|
|
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
|
-
(`@
|
|
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` + `@
|
|
392
|
-
`terp.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 `@terpjs/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
|
+
`@terpjs/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 `@terpjs/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
|
+
(`@terpjs/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` + `@terpjs/eslint-boundaries` reference harness on top of
|
|
392
|
+
`terp.core` / `@terpjs/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**.
|