diff --git a/docs/adr-ep-cap-003-vocabulary-ref-guard.md b/docs/adr-ep-cap-003-vocabulary-ref-guard.md new file mode 100644 index 0000000..a71262c --- /dev/null +++ b/docs/adr-ep-cap-003-vocabulary-ref-guard.md @@ -0,0 +1,30 @@ +# ADR: EP-CAP-003 Vocabulary Reference Migration Guard + +Status: accepted + +Context: approved ability, capability, and feature names are free text in v0.1. +EP-CAP-003 expects a later nullable `vocabulary_ref` column so approved entries +can be anchored to a named vocabulary version after terminology standardises. + +Decision: keep the v0.1 approved registry schema ID-based and free of natural-key +constraints on `name`. + +The current schema leaves the migration path open: + +- `approved_abilities.name`, `approved_capabilities.name`, and + `approved_features.name` have no `CHECK` constraints. +- There are no unique indexes on `name` alone. +- Approved table foreign keys point to integer row IDs, not name strings. +- `review_decisions` records `repository_id`, optional `analysis_run_id`, + `action`, and `notes`; it does not reference ability, capability, or feature + names as durable identifiers. + +The guard test in `tests/test_storage_migrations.py` introspects the SQLite +schema and fails if a future migration adds name-only uniqueness, name-based +foreign keys, or review decision name references that would make +`name + vocabulary_ref` a painful future key. + +Consequence: no `vocabulary_ref` column is added now. When EP-CAP-003 is +implemented, the expected migration is additive: add nullable vocabulary columns, +backfill where vocabulary mappings exist, then introduce any future composite +constraints deliberately after legacy rows have been reconciled. diff --git a/tests/test_storage_migrations.py b/tests/test_storage_migrations.py index b800379..3732514 100644 --- a/tests/test_storage_migrations.py +++ b/tests/test_storage_migrations.py @@ -37,6 +37,69 @@ def test_initialize_is_idempotent_and_applies_expected_columns(tmp_path): assert "content_chunks" in tables +def test_approved_registry_schema_allows_future_nullable_vocabulary_ref(tmp_path): + database_path = tmp_path / "registry.sqlite3" + store = RegistryStore(database_path) + store.initialize() + + approved_tables = { + "approved_abilities", + "approved_capabilities", + "approved_features", + } + + with sqlite3.connect(database_path) as connection: + table_sql = { + table: connection.execute( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?", + (table,), + ).fetchone()[0] + for table in approved_tables + } + indexes = { + table: [ + { + "name": row[1], + "unique": bool(row[2]), + "columns": [ + column[2] + for column in connection.execute( + f"PRAGMA index_info({row[1]!r})" + ) + ], + } + for row in connection.execute(f"PRAGMA index_list({table})") + ] + for table in approved_tables + } + foreign_keys = { + table: [ + { + "from": row[3], + "to_table": row[2], + "to_column": row[4], + } + for row in connection.execute(f"PRAGMA foreign_key_list({table})") + ] + for table in approved_tables | {"review_decisions"} + } + review_columns = { + row[1] for row in connection.execute("PRAGMA table_info(review_decisions)") + } + + assert all("CHECK" not in sql.upper() for sql in table_sql.values()) + for table_indexes in indexes.values(): + assert all( + not (index["unique"] and index["columns"] == ["name"]) + for index in table_indexes + ) + for table_foreign_keys in foreign_keys.values(): + assert all(key["to_column"] != "name" for key in table_foreign_keys) + assert all(key["from"] != "name" for key in table_foreign_keys) + assert {"repository_id", "analysis_run_id", "action", "notes"} <= review_columns + assert not {"ability_name", "capability_name", "feature_name"} & review_columns + + def test_delete_repository_cascades_registry_and_review_rows(tmp_path): service = make_service(tmp_path) repository = service.register_repository( diff --git a/workplans/RREG-WP-0002-production-hardening.md b/workplans/RREG-WP-0002-production-hardening.md index 6f4bb66..bf3bab8 100644 --- a/workplans/RREG-WP-0002-production-hardening.md +++ b/workplans/RREG-WP-0002-production-hardening.md @@ -4,7 +4,7 @@ type: workplan title: "Repository Ability Registry — Production Hardening" domain: capabilities repo: repo-registry -status: active +status: done owner: codex topic_slug: foerster-capabilities created: "2026-04-26" @@ -111,7 +111,7 @@ deliberate and visible in tests; agent-facing endpoints have stable models. ```task id: RREG-WP-0002-T07 -status: todo +status: done priority: medium state_hub_task_id: "6f06d8bb-0ed9-47f1-8b3e-40f725e6ece6" ```