feat(planning): start_planning hands back the active plan that already covers the work (#4079)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 1m10s
CI & Build / TypeScript typecheck (push) Successful in 1m15s
CI & Build / Python tests (push) Failing after 1m22s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 1m10s
CI & Build / TypeScript typecheck (push) Successful in 1m15s
CI & Build / Python tests (push) Failing after 1m22s
CI & Build / Build & push image (push) Skipped
Step 4 of milestone 415 "An existing plan is found before a new one is made". A session that could not see an existing plan made a second one beside it. start_planning and create_milestone now ask first: an ACTIVE milestone in the project with the same title, or one that reads as the same plan (title, design and steps against milestone embeddings), is returned with its progress and a pointer to create_records(milestone_id=...). Nothing is created; force=true bypasses. - dedup.find_matching_plan / plan_gate / plan_match_response; access-checked before either arm (rule 78), fail-open like the other gates. - Done milestones never block; the semantic arm needs 200+ chars of candidate. - kb_plan_match_threshold (default 0.90) is a setting, in the Settings view, and pinned against the Python default by test_settings_defaults_agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Real-Postgres tests for finding a plan by meaning (milestone 415, step 3).
|
||||
"""Real-Postgres tests for finding a plan by meaning (milestone 415, steps 3 and 4).
|
||||
|
||||
A project's roadmap written as milestones was invisible to recall: `search`
|
||||
covered notes, tasks and rules, so "is there already a plan for this?" had no
|
||||
@@ -16,6 +16,7 @@ from scribe.models import async_session
|
||||
from scribe.models.embedding import EMBEDDING_DIM, MilestoneEmbedding
|
||||
from scribe.models.milestone import Milestone
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import dedup as dedup_svc
|
||||
from scribe.services.embeddings import CHUNKER_VERSION, semantic_search_milestones
|
||||
from tests.helpers import ensure_user
|
||||
|
||||
@@ -80,3 +81,46 @@ async def test_without_a_project_it_searches_the_callers_own(roadmap):
|
||||
async def test_a_project_the_caller_cannot_read_returns_nothing(roadmap):
|
||||
assert await _found(roadmap["stranger"], project_id=roadmap["mine"]) == []
|
||||
assert await _found(roadmap["stranger"]) == []
|
||||
|
||||
|
||||
# ── the plan gate (step 4): start_planning finds the plan before making another ──
|
||||
|
||||
LONG = "Resolve works and editions against the metadata providers. " * 5
|
||||
|
||||
|
||||
async def _gate(roadmap, title, text="", project=None):
|
||||
with patch("scribe.services.embeddings.get_embedding", AsyncMock(return_value=NEAR)):
|
||||
return await dedup_svc.plan_gate(
|
||||
roadmap["owner"], project or roadmap["mine"], title, text,
|
||||
)
|
||||
|
||||
|
||||
async def test_the_gate_returns_an_active_plan_with_the_same_title(roadmap):
|
||||
out = await _gate(roadmap, " m3 — METADATA ")
|
||||
assert out["duplicate"] is True and out["match"] == "title"
|
||||
assert out["existing_milestone"]["id"] == roadmap["m3"]
|
||||
assert f"create_records(milestone_id={roadmap['m3']}" in out["message"]
|
||||
|
||||
|
||||
async def test_the_gate_finds_a_plan_by_meaning_and_never_a_done_one(roadmap):
|
||||
"""NEAR matches m3 (active), `done` (done) and `foreign` (another project).
|
||||
Only m3 is a plan this project is still working through."""
|
||||
out = await _gate(roadmap, "Book metadata", LONG)
|
||||
assert out["match"] == "semantic"
|
||||
assert out["existing_id"] == roadmap["m3"]
|
||||
|
||||
|
||||
async def test_a_done_plan_and_another_projects_plan_do_not_block(roadmap):
|
||||
assert await _gate(roadmap, "Covers") is None
|
||||
assert await _gate(roadmap, "Metadata elsewhere") is None
|
||||
async with async_session() as s:
|
||||
m3 = await s.get(Milestone, roadmap["m3"])
|
||||
m3.status = "done"
|
||||
await s.commit()
|
||||
assert await _gate(roadmap, "Book metadata", LONG) is None
|
||||
|
||||
|
||||
async def test_a_short_candidate_is_judged_by_title_alone(roadmap):
|
||||
"""A title-only embedding sits in a tight neighbourhood and matches
|
||||
anything nearby, so a bare title never takes the semantic arm."""
|
||||
assert await _gate(roadmap, "Book metadata", "just a title") is None
|
||||
|
||||
Reference in New Issue
Block a user