Phases 2 to 8 of the AI assistant overhaul plus fixes from real-model chats: one tool registry with native tool calls, server-kept chat logs with prompt caching, an FTS5 library index with opt-in meaning search, keys bound to their address and a guard on every AI route, page tools and opt-in web search, saved chats with streaming, Try again, a context meter and compaction, chat images, delete/category/tag permissions with Undo, constraint and test cards, card fixes and previews, questions with answers to click, the derivation NOT toggle (schema v10, community catalog 0021), request fixes for the newest Claude models, and OpenAI's Responses API. Review fixes: Try again clears the old turn's fixes and answers, category Undo removes only what it made, a failed delete rolls back in order, not_matches patterns are checked, busy chats refuse a second request, and chat search ignores stored JSON.
107 lines
4.9 KiB
Python
107 lines
4.9 KiB
Python
"""Versioned payload migration chain. Mirror of TypeScript
|
|
`src/manager/import-export/migrations.ts`.
|
|
|
|
Used by `engine/importer.py` on server-side parse paths (re-validate
|
|
on commit). Client-side migration is the source of truth for what
|
|
the user saw in the picker; server re-runs as defense in depth.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from engine.migrations.v0_to_v1 import migrate_v0_to_v1
|
|
from engine.migrations.v1_to_v2 import migrate_v1_to_v2
|
|
|
|
CURRENT_SCHEMA_VERSION = 2
|
|
|
|
# Highest schema version this runtime can natively READ + WRITE — distinct
|
|
# from CURRENT_SCHEMA_VERSION (the migration-chain head, which stays 2 because
|
|
# v2->v3 through v6->v7 are no-ops). v3 is the SP2b text-grammar bump (shape-
|
|
# identical to v2), v4 is the additive `target_select` constraint reach, v5
|
|
# is the additive `accepts` tag-axis kind, v6 the constraint `only` rule and
|
|
# v7 derivation AND/OR condition groups plus the `is_empty`/`is_not_empty` ops,
|
|
# v8 send-to-negative (`negative` text + the derivation `negative` mode),
|
|
# v9 the wildcard fallback option and v10 the derivation `not_contains` /
|
|
# `not_matches` tests; all are handled natively, so a
|
|
# payload at CURRENT < v <= MAX_KNOWN installs AS-IS with no migration.
|
|
# Mirror of TS `MAX_KNOWN_SCHEMA_VERSION` in
|
|
# `src/manager/import-export/migrations.ts`.
|
|
#
|
|
# MAINTENANCE CONTRACT: bump this whenever the TS `schemaVersionForPayload()`
|
|
# learns to stamp a new (higher) version — otherwise this commit-side
|
|
# re-validate will reject the very shapes the runtime just learned to produce.
|
|
MAX_KNOWN_SCHEMA_VERSION = 10
|
|
|
|
# Feature stamps (mirror of the TS constants beside MAX_KNOWN). Each is the
|
|
# catalog version a payload needs once it uses that feature; see
|
|
# `engine/migrations/stamping.py:schema_version_for_payload`.
|
|
SP2B_SCHEMA_VERSION = 3 # range count / `~` flag in a `{N$$…}` multi-pick
|
|
SP3_REACH_SCHEMA_VERSION = 4 # non-default constraint `target_select`
|
|
TAG_AXES_SCHEMA_VERSION = 5 # wildcard `tag_group_kinds` with an `accepts` group
|
|
CONSTRAINT_ONLY_SCHEMA_VERSION = 6 # constraint rule with mode `only`
|
|
# derivation condition group (AND / OR) or an `is_empty` / `is_not_empty` test
|
|
DERIVATION_CONDITIONS_SCHEMA_VERSION = 7
|
|
# send-to-negative: a non-empty `negative` on a wildcard option, fixed value or
|
|
# combine, or a derivation action with mode `negative`
|
|
NEGATIVES_SCHEMA_VERSION = 8
|
|
# wildcard fallback option: an option flagged `fallback: true`
|
|
FALLBACK_SCHEMA_VERSION = 9
|
|
# derivation NOT toggle: a test using `not_contains` or `not_matches`
|
|
NEGATED_TESTS_SCHEMA_VERSION = 10
|
|
|
|
_CHAIN = {
|
|
0: migrate_v0_to_v1,
|
|
1: migrate_v1_to_v2,
|
|
}
|
|
|
|
|
|
def migrate_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
"""Walk the migration chain from `payload.schema_version` up to current.
|
|
|
|
Returns one of:
|
|
- {"ok": True, "migrated": <dict>, "migrated_entity_count": int}
|
|
where migrated_entity_count is the cumulative entity count across
|
|
all migration steps (total bundles+wildcards+fixed_values+combines+
|
|
derivations+constraints+categories that passed through any step).
|
|
Matches TS `migratedEntityCount`.
|
|
- {"ok": False, "reason": str}
|
|
"""
|
|
version = payload.get("schema_version")
|
|
if not isinstance(version, int):
|
|
return {"ok": False, "reason": "payload missing schema_version field"}
|
|
# Reject threshold reads MAX_KNOWN (highest natively-supported version),
|
|
# NOT CURRENT (the migration-chain head). A payload at
|
|
# CURRENT < v <= MAX_KNOWN is shape-compatible and handled natively, so it
|
|
# passes through AS-IS — the while-loop below is bound by CURRENT, so there
|
|
# is nothing to migrate and schema_version is preserved. Only > MAX_KNOWN
|
|
# rejects. Mirror of TS `migrateImportEnvelope`.
|
|
if version > MAX_KNOWN_SCHEMA_VERSION:
|
|
return {
|
|
"ok": False,
|
|
"reason": f"future schema version {version} (max known: {MAX_KNOWN_SCHEMA_VERSION})",
|
|
}
|
|
current = {
|
|
"schema_version": version,
|
|
"bundles": payload.get("bundles", []),
|
|
"wildcards": payload.get("wildcards", []),
|
|
"fixed_values": payload.get("fixed_values", []),
|
|
"combines": payload.get("combines", []),
|
|
"derivations": payload.get("derivations", []),
|
|
"constraints": payload.get("constraints", []),
|
|
"categories": payload.get("categories", []),
|
|
}
|
|
migrated_entity_count = 0
|
|
while current["schema_version"] < CURRENT_SCHEMA_VERSION:
|
|
fn = _CHAIN.get(current["schema_version"])
|
|
if fn is None:
|
|
ver = current["schema_version"]
|
|
return {"ok": False, "reason": f"no migration registered for v{ver}"}
|
|
before = sum(
|
|
len(current[k])
|
|
for k in ("bundles", "wildcards", "fixed_values", "combines",
|
|
"derivations", "constraints", "categories")
|
|
)
|
|
current = fn(current)
|
|
migrated_entity_count += before
|
|
return {"ok": True, "migrated": current, "migrated_entity_count": migrated_entity_count}
|