feat(ai): add tenant-safe hierarchical expense learning

This commit is contained in:
caoxiaozhu
2026-07-16 14:30:41 +08:00
parent 6bdf65bc24
commit ee88a36baf
65 changed files with 6909 additions and 232 deletions

View File

@@ -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)}; "

View File

@@ -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",
}
)