feat(ai): add tenant-safe hierarchical expense learning
This commit is contained in:
@@ -79,10 +79,67 @@ MIGRATION_OWNED_TABLES_BY_REVISION: dict[str, frozenset[str]] = {
|
||||
"workflow_outcomes",
|
||||
}
|
||||
),
|
||||
"20260716_0007": frozenset(
|
||||
{
|
||||
"expense_cases",
|
||||
"expense_case_links",
|
||||
"business_events",
|
||||
"auth_sessions",
|
||||
"attachment_association_jobs",
|
||||
"ai_application_preview_decisions",
|
||||
"ai_decisions",
|
||||
"ai_decision_feedback",
|
||||
"memory_entries",
|
||||
"memory_evidence_links",
|
||||
"workflow_outcomes",
|
||||
}
|
||||
),
|
||||
"20260716_0008": frozenset(
|
||||
{
|
||||
"expense_cases",
|
||||
"expense_case_links",
|
||||
"business_events",
|
||||
"auth_sessions",
|
||||
"attachment_association_jobs",
|
||||
"ai_application_preview_decisions",
|
||||
"ai_decisions",
|
||||
"ai_decision_feedback",
|
||||
"memory_entries",
|
||||
"memory_evidence_links",
|
||||
"risk_observations",
|
||||
"risk_observation_feedback",
|
||||
"few_shot_samples",
|
||||
"workflow_outcomes",
|
||||
}
|
||||
),
|
||||
"20260716_0009": frozenset(
|
||||
{
|
||||
"expense_cases",
|
||||
"expense_case_links",
|
||||
"business_events",
|
||||
"auth_sessions",
|
||||
"attachment_association_jobs",
|
||||
"ai_application_preview_decisions",
|
||||
"ai_decisions",
|
||||
"ai_decision_feedback",
|
||||
"memory_entries",
|
||||
"memory_evidence_links",
|
||||
"risk_observations",
|
||||
"risk_observation_feedback",
|
||||
"few_shot_samples",
|
||||
"workflow_outcomes",
|
||||
}
|
||||
),
|
||||
}
|
||||
if MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0006"] != MIGRATION_OWNED_TABLES:
|
||||
if MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0009"] != MIGRATION_OWNED_TABLES:
|
||||
raise RuntimeError("latest Alembic revision must own the centralized migration table set")
|
||||
|
||||
# 0008 之前这三张表由旧 bootstrap / 风险服务按需创建。迁移前置检查允许
|
||||
# 它们作为完整或部分旧资产存在,由 0008 统一收编;其他未来表仍严格拒绝。
|
||||
LEGACY_ADOPTABLE_HISTORICAL_CASE_TABLES = frozenset(
|
||||
{"risk_observations", "risk_observation_feedback", "few_shot_samples"}
|
||||
)
|
||||
|
||||
|
||||
class MigrationPreflightError(RuntimeError):
|
||||
"""Raised when the database schema cannot be safely advanced by Alembic."""
|
||||
@@ -103,10 +160,11 @@ def _validate_connection(connection: Connection) -> MigrationPreflightState:
|
||||
owned_tables = table_names & MIGRATION_OWNED_TABLES
|
||||
|
||||
if "alembic_version" not in table_names:
|
||||
if owned_tables:
|
||||
unsafe_owned_tables = owned_tables - LEGACY_ADOPTABLE_HISTORICAL_CASE_TABLES
|
||||
if unsafe_owned_tables:
|
||||
raise MigrationPreflightError(
|
||||
"unversioned database contains migration-owned tables "
|
||||
f"({_format_tables(owned_tables)}); refusing to guess, stamp, or repair"
|
||||
f"({_format_tables(unsafe_owned_tables)}); refusing to guess, stamp, or repair"
|
||||
)
|
||||
return MigrationPreflightState(revision=None, owned_tables=owned_tables)
|
||||
|
||||
@@ -117,10 +175,11 @@ def _validate_connection(connection: Connection) -> MigrationPreflightState:
|
||||
).scalars()
|
||||
)
|
||||
if not revisions:
|
||||
if owned_tables:
|
||||
unsafe_owned_tables = owned_tables - LEGACY_ADOPTABLE_HISTORICAL_CASE_TABLES
|
||||
if unsafe_owned_tables:
|
||||
raise MigrationPreflightError(
|
||||
"alembic_version has no recorded revision but migration-owned tables exist "
|
||||
f"({_format_tables(owned_tables)}); refusing to guess, stamp, or repair"
|
||||
f"({_format_tables(unsafe_owned_tables)}); refusing to guess, stamp, or repair"
|
||||
)
|
||||
return MigrationPreflightState(revision=None, owned_tables=owned_tables)
|
||||
|
||||
@@ -137,9 +196,14 @@ def _validate_connection(connection: Connection) -> MigrationPreflightState:
|
||||
f"unknown Alembic revision {revision!r}; refusing to run migrations"
|
||||
)
|
||||
|
||||
if owned_tables != expected_tables:
|
||||
missing_tables = expected_tables - owned_tables
|
||||
unexpected_tables = owned_tables - expected_tables
|
||||
adoptable_tables = (
|
||||
LEGACY_ADOPTABLE_HISTORICAL_CASE_TABLES
|
||||
if revision not in {"20260716_0008", "20260716_0009"}
|
||||
else frozenset()
|
||||
)
|
||||
missing_tables = expected_tables - owned_tables
|
||||
unexpected_tables = owned_tables - expected_tables - adoptable_tables
|
||||
if missing_tables or unexpected_tables:
|
||||
raise MigrationPreflightError(
|
||||
f"migration-owned table set does not match revision {revision}: "
|
||||
f"missing={_format_tables(missing_tables)}; "
|
||||
|
||||
@@ -16,6 +16,9 @@ MIGRATION_OWNED_TABLES: frozenset[str] = frozenset(
|
||||
"business_events",
|
||||
"memory_entries",
|
||||
"memory_evidence_links",
|
||||
"risk_observations",
|
||||
"risk_observation_feedback",
|
||||
"few_shot_samples",
|
||||
"workflow_outcomes",
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user