refactor(systems): one seam — the Systems question rides every read and write of a record
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 12s
CI & Build / Python tests (push) Failing after 29s
CI & Build / Build & push image (push) Skipped

Scribe issue #2570, from the operator's challenge: the invariant is
"always be asking whether what you're touching is a System's territory
and whether the work is filed there" — not a nudge in one corner. The
prior shape failed it twice: the hint fired only on creates (with a
special-cased zero-Systems branch), and get_task/get_note returned
records WITHOUT their Systems, so the read-side reflex had nothing to
fire on (same per-kind asymmetry as #2481).

- attach_systems(): single helper used by get/create/update for tasks,
  notes, and snippets, plus add_task_log. Tagged records always show
  `systems`; an untagged project record carries the `systems_hint`
  question instead. Neither field attaches empty (#2483). Hint is
  owner-only; everything fail-open (#2109).
- untagged_systems_hint unified to ONE question — the vocabulary
  listing varies, the question doesn't; the zero-Systems branch stops
  being special text.
- Docstrings state the uniform contract; floor prose now names the
  read-side reflex (systems visible -> list_system_records the pile).
- Plugin 0.1.27 -> 0.1.28.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 15:48:41 -04:00
co-authored by Claude Fable 5
parent 56b1952de5
commit 3455f9cb9a
7 changed files with 169 additions and 86 deletions
+51 -8
View File
@@ -132,36 +132,79 @@ async def test_create_system_distinct_name_passes_the_gate():
@pytest.mark.asyncio
async def test_create_task_untagged_in_project_with_systems_carries_hint():
async def test_create_task_untagged_in_project_carries_the_question():
note = MagicMock(); note.id = 60; note.to_dict.return_value = {"id": 60}
sys_a = MagicMock(); sys_a.id = 4; sys_a.name = "plugin-hooks"
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
patch("scribe.mcp.tools.tasks.dedup_svc") as dedup_svc, \
patch("scribe.mcp.tools.systems.systems_svc") as hint_svc:
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
notes_svc.create_note = AsyncMock(return_value=note)
dedup_svc.find_duplicate_note = AsyncMock(return_value=None)
hint_svc.list_systems = AsyncMock(return_value=[sys_a])
seam_svc.list_record_systems = AsyncMock(return_value=[])
seam_svc.list_systems = AsyncMock(return_value=[sys_a])
from scribe.mcp.tools.tasks import create_task
result = await create_task(title="untagged", project_id=5)
assert "plugin-hooks" in result["systems_hint"]
@pytest.mark.asyncio
async def test_create_task_tagged_or_projectless_carries_no_hint():
async def test_create_task_tagged_shows_systems_and_projectless_gets_neither():
note = MagicMock(); note.id = 61; note.to_dict.return_value = {"id": 61}
tagged_sys = MagicMock(); tagged_sys.to_dict.return_value = {"id": 4, "name": "plugin-hooks"}
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
patch("scribe.mcp.tools.tasks.dedup_svc") as dedup_svc, \
patch("scribe.mcp.tools.tasks.systems_svc") as systems_svc, \
patch("scribe.mcp.tools.systems.systems_svc") as hint_svc:
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
notes_svc.create_note = AsyncMock(return_value=note)
dedup_svc.find_duplicate_note = AsyncMock(return_value=None)
systems_svc.set_record_systems = AsyncMock()
systems_svc.list_record_systems = AsyncMock(return_value=[])
hint_svc.list_systems = AsyncMock(return_value=[MagicMock()])
seam_svc.list_record_systems = AsyncMock(return_value=[tagged_sys])
from scribe.mcp.tools.tasks import create_task
tagged = await create_task(title="tagged", project_id=5, system_ids=[4])
seam_svc.list_record_systems = AsyncMock(return_value=[])
orphan = await create_task(title="orphan", project_id=0)
assert tagged["systems"] == [{"id": 4, "name": "plugin-hooks"}]
assert "systems_hint" not in tagged
assert "systems_hint" not in orphan
assert "systems" not in orphan and "systems_hint" not in orphan
@pytest.mark.asyncio
async def test_get_task_shows_the_records_systems_on_read():
"""The touching-a-System reflex needs the affiliation visible on READ —
a session opening the task it is about to work must see its areas."""
note = MagicMock()
note.id = 70; note.user_id = 1; note.project_id = 5
note.deleted_at = None; note.parent_id = None
note.to_dict.return_value = {"id": 70, "task_kind": "work"}
tagged_sys = MagicMock(); tagged_sys.to_dict.return_value = {"id": 2, "name": "exporter"}
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
patch("scribe.mcp.tools.tasks.access_svc") as access_svc, \
patch("scribe.mcp.tools.tasks.record_pulled"), \
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
notes_svc.get_note_for_user = AsyncMock(return_value=(note, "owner"))
access_svc.describe_provenance = AsyncMock(return_value={})
seam_svc.list_record_systems = AsyncMock(return_value=[tagged_sys])
from scribe.mcp.tools.tasks import get_task
result = await get_task(task_id=70)
assert result["systems"] == [{"id": 2, "name": "exporter"}]
@pytest.mark.asyncio
async def test_add_task_log_on_untagged_project_task_asks_the_question():
"""Logging work IS working in some area — the strongest moment to ask."""
log = MagicMock(); log.to_dict.return_value = {"id": 9, "task_id": 70}
task = MagicMock(); task.user_id = 1; task.project_id = 5
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
patch("scribe.mcp.tools.tasks.task_logs_svc") as logs_svc, \
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
logs_svc.create_log = AsyncMock(return_value=log)
notes_svc.get_note_for_user = AsyncMock(return_value=(task, "owner"))
seam_svc.list_record_systems = AsyncMock(return_value=[])
seam_svc.list_systems = AsyncMock(return_value=[])
from scribe.mcp.tools.tasks import add_task_log
result = await add_task_log(task_id=70, content="progress")
assert "no Systems yet" in result["systems_hint"]