generated from coulomb/repo-seed
vocabulary ref and schema guard test
This commit is contained in:
30
docs/adr-ep-cap-003-vocabulary-ref-guard.md
Normal file
30
docs/adr-ep-cap-003-vocabulary-ref-guard.md
Normal file
@@ -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.
|
||||||
@@ -37,6 +37,69 @@ def test_initialize_is_idempotent_and_applies_expected_columns(tmp_path):
|
|||||||
assert "content_chunks" in tables
|
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):
|
def test_delete_repository_cascades_registry_and_review_rows(tmp_path):
|
||||||
service = make_service(tmp_path)
|
service = make_service(tmp_path)
|
||||||
repository = service.register_repository(
|
repository = service.register_repository(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Repository Ability Registry — Production Hardening"
|
title: "Repository Ability Registry — Production Hardening"
|
||||||
domain: capabilities
|
domain: capabilities
|
||||||
repo: repo-registry
|
repo: repo-registry
|
||||||
status: active
|
status: done
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: foerster-capabilities
|
topic_slug: foerster-capabilities
|
||||||
created: "2026-04-26"
|
created: "2026-04-26"
|
||||||
@@ -111,7 +111,7 @@ deliberate and visible in tests; agent-facing endpoints have stable models.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0002-T07
|
id: RREG-WP-0002-T07
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "6f06d8bb-0ed9-47f1-8b3e-40f725e6ece6"
|
state_hub_task_id: "6f06d8bb-0ed9-47f1-8b3e-40f725e6ece6"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user