feat(moments): actions map onto moments, with in-session corrections (milestone 458 step 2, #4920)
CI & Build / Plugin hooks (push) Successful in 18s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m23s
CI & Build / Python tests (push) Successful in 2m0s
CI & Build / Build & push image (push) Successful in 36s
CI & Build / Plugin hooks (push) Successful in 18s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m23s
CI & Build / Python tests (push) Successful in 2m0s
CI & Build / Build & push image (push) Successful in 36s
moment_actions.resolve(tool, input) names every moment a call reaches and the action that reached it. One call can reach several: kubectl apply is a run, a deliver and a reach outside the workspace. Command tools match by how each segment of the line starts, with a word boundary; other tools by field=value arguments. The MCP server prefix and case are ignored. 56 shipped defaults cover the harness tools, Scribe tools and common command shapes. moment_mappings (migration 0116) holds what an install adds and the defaults it switches off. A removal is a stored row, so an upgrade does not switch the default back on. Per the operator ruling, corrections happen in the session: map_action and unmap_action (write tools) return now_reaches so the fix can be confirmed in the same reply. list_moments now shows each moment's actions on this install. REST mirrors both doors, recorded as human. Backup v21 carries the mappings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ from scribe.models.design_system import DesignSystem, DesignToken
|
||||
from scribe.models.note_usage import NoteUsageEvent
|
||||
from scribe.models.rule_usage import RuleUsageEvent
|
||||
from scribe.models.system_usage import SystemUsageEvent
|
||||
from scribe.models.moment_mapping import MomentMapping
|
||||
from scribe.models.retrieval_tuning import RetrievalTuningEvent
|
||||
from scribe.models.canonical_system import CanonicalSystem
|
||||
from scribe.models.rulebook import RuleRelation, rule_systems as rule_systems_t
|
||||
@@ -93,8 +94,12 @@ logger = logging.getLogger(__name__)
|
||||
# 444): the files that are each area, and whether an area's rulings were read
|
||||
# once shown. The usage rows restore through the SYSTEM map, for the reason
|
||||
# the rule twin restores through the rule map.
|
||||
# v21 (2026-10) added moment_mappings (milestone 458): which of this install's
|
||||
# actions reach which moment, and the shipped defaults it switched off. Each
|
||||
# row is a correction an operator made in-session; a restore that dropped them
|
||||
# would silently put every misfire back.
|
||||
# Bump when the serialized schema changes.
|
||||
BACKUP_VERSION = 20
|
||||
BACKUP_VERSION = 21
|
||||
|
||||
# Every table this backup carries, by its REAL name. Paired with _NOT_INCLUDED
|
||||
# below, these two lists must together account for the entire schema — which is
|
||||
@@ -139,6 +144,8 @@ _BACKED_UP = [
|
||||
# v20 (2026-10): System usage telemetry (milestone 444), for the reason
|
||||
# its note and rule twins travel.
|
||||
"system_usage_events",
|
||||
# v21 (2026-10): an install's action → moment corrections (milestone 458).
|
||||
"moment_mappings",
|
||||
]
|
||||
|
||||
# Tables intentionally NOT in the backup, surfaced in the payload so the gap is
|
||||
@@ -247,6 +254,8 @@ _COLUMN_EXCLUSIONS: dict[str, set[str]] = {
|
||||
# Same again — and everything else travels, because each remaining column
|
||||
# is part of the argument: what moved, from what, to what, by whom, why.
|
||||
"retrieval_tuning_events": {"id"},
|
||||
# Every other column is the correction itself, or who made it and why.
|
||||
"moment_mappings": {"id"},
|
||||
"design_systems": {"deleted_at", "deleted_batch_id", "created_at", "updated_at"},
|
||||
"design_tokens": {"deleted_at", "deleted_batch_id", "created_at", "updated_at"},
|
||||
"repo_bindings": {"id", "created_at", "updated_at"},
|
||||
@@ -334,6 +343,7 @@ _IMPORT_COLUMN_EXCLUSIONS: dict[str, set[str]] = {
|
||||
"rule_usage_events": {"id"},
|
||||
"system_usage_events": {"id"},
|
||||
"retrieval_tuning_events": {"id"},
|
||||
"moment_mappings": {"id"},
|
||||
"design_systems": {
|
||||
"id", "deleted_at", "deleted_batch_id", "created_at", "updated_at",
|
||||
},
|
||||
@@ -486,6 +496,21 @@ def _rule_usage_event_rows(rows) -> list[dict]:
|
||||
]
|
||||
|
||||
|
||||
def _moment_mapping_rows(rows) -> list[dict]:
|
||||
"""An install's action → moment corrections (milestone 458). Not
|
||||
`to_dict()`, for _retrieval_tuning_event_rows' reason: the restore needs
|
||||
`user_id` to remap, and the MCP reader omits it."""
|
||||
return [
|
||||
{
|
||||
"user_id": r.user_id, "tool": r.tool, "match": r.match,
|
||||
"moment": r.moment, "effect": r.effect, "reason": r.reason,
|
||||
"actor": r.actor,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def _retrieval_tuning_event_rows(rows) -> list[dict]:
|
||||
"""The record of why a retrieval dial is where it is (#4102).
|
||||
|
||||
@@ -836,6 +861,9 @@ async def export_full_backup() -> dict:
|
||||
retrieval_tuning_events = (await session.execute(
|
||||
select(RetrievalTuningEvent).order_by(RetrievalTuningEvent.id)
|
||||
)).scalars().all()
|
||||
moment_mappings = (await session.execute(
|
||||
select(MomentMapping).order_by(MomentMapping.id)
|
||||
)).scalars().all()
|
||||
repo_bindings = (await session.execute(select(RepoBinding))).scalars().all()
|
||||
code_shapes = (await session.execute(select(CodeShape))).scalars().all()
|
||||
code_shape_events = (await session.execute(
|
||||
@@ -886,6 +914,7 @@ async def export_full_backup() -> dict:
|
||||
"retrieval_tuning_events": _retrieval_tuning_event_rows(
|
||||
retrieval_tuning_events
|
||||
),
|
||||
"moment_mappings": _moment_mapping_rows(moment_mappings),
|
||||
"repo_bindings": _repo_binding_rows(repo_bindings),
|
||||
"note_supersessions": _note_supersession_rows(supersessions),
|
||||
"code_shapes": _code_shape_rows(code_shapes),
|
||||
@@ -1035,6 +1064,13 @@ async def export_user_backup(user_id: int) -> dict:
|
||||
.where(RetrievalTuningEvent.user_id == user_id)
|
||||
.order_by(RetrievalTuningEvent.id)
|
||||
)).scalars().all()
|
||||
# The user's own configuration: user_id is the owner, nothing to
|
||||
# route around.
|
||||
moment_mappings = (await session.execute(
|
||||
select(MomentMapping)
|
||||
.where(MomentMapping.user_id == user_id)
|
||||
.order_by(MomentMapping.id)
|
||||
)).scalars().all()
|
||||
rule_relations = (await session.execute(
|
||||
select(RuleRelation).where(
|
||||
RuleRelation.from_rule_id.in_(_rule_ids),
|
||||
@@ -1093,6 +1129,7 @@ async def export_user_backup(user_id: int) -> dict:
|
||||
"retrieval_tuning_events": _retrieval_tuning_event_rows(
|
||||
retrieval_tuning_events
|
||||
),
|
||||
"moment_mappings": _moment_mapping_rows(moment_mappings),
|
||||
"repo_bindings": _repo_binding_rows(repo_bindings),
|
||||
"note_supersessions": _note_supersession_rows(supersessions),
|
||||
"code_shapes": _code_shape_rows(code_shapes),
|
||||
@@ -1301,6 +1338,23 @@ def _build_setting(row: dict, maps: _Maps) -> Setting | None:
|
||||
return Setting(user_id=uid, key=row["key"], value=row.get("value", ""))
|
||||
|
||||
|
||||
def _build_moment_mapping(row: dict, maps: _Maps) -> MomentMapping | None:
|
||||
"""No remapping beyond the user: `tool` and `moment` are names, not keys."""
|
||||
uid = maps.users.get(row.get("user_id") or 0)
|
||||
if uid is None or not row.get("tool") or not row.get("moment"):
|
||||
return None
|
||||
return MomentMapping(
|
||||
user_id=uid,
|
||||
tool=row["tool"],
|
||||
match=row.get("match") or "",
|
||||
moment=row["moment"],
|
||||
effect=row.get("effect") or "add",
|
||||
reason=row.get("reason") or "",
|
||||
actor=row.get("actor") or "model",
|
||||
created_at=_dt(row.get("created_at")),
|
||||
)
|
||||
|
||||
|
||||
def _build_retrieval_tuning_event(row: dict, maps: _Maps) -> RetrievalTuningEvent | None:
|
||||
"""No id remapping beyond the user: `surface` is a registry NAME, not a
|
||||
foreign key, which is what lets this history survive a restore into an
|
||||
@@ -1874,7 +1928,7 @@ async def _restore_v2(data: dict) -> dict:
|
||||
"code_shape_uses": 0, "canonical_systems": 0,
|
||||
"rule_systems": 0, "rule_relations": 0, "rule_versions": 0,
|
||||
"retrieval_tuning_events": 0, "lesson_rule_links": 0,
|
||||
"lesson_no_rule": 0,
|
||||
"lesson_no_rule": 0, "moment_mappings": 0,
|
||||
}
|
||||
|
||||
async with async_session() as session:
|
||||
@@ -1984,6 +2038,15 @@ async def _restore_v2(data: dict) -> dict:
|
||||
session.add(event)
|
||||
stats["retrieval_tuning_events"] += 1
|
||||
|
||||
# 8c. Moment mappings (v21) — the install's corrections to which
|
||||
# actions reach which moment. Names only, so only the user remaps.
|
||||
for mm_data in data.get("moment_mappings", []):
|
||||
mapping = _build_moment_mapping(mm_data, maps)
|
||||
if mapping is None:
|
||||
continue
|
||||
session.add(mapping)
|
||||
stats["moment_mappings"] += 1
|
||||
|
||||
# 9. Rulebooks (v3)
|
||||
for rb_data in data.get("rulebooks", []):
|
||||
rb = _build_rulebook(rb_data, maps)
|
||||
|
||||
Reference in New Issue
Block a user