Milestone 415: an existing plan is found before a new one is made #159

Merged
bvandeusen merged 8 commits from dev into main 2026-09-15 14:23:32 -04:00
3 changed files with 13 additions and 6 deletions
Showing only changes of commit 2811fc9025 - Show all commits
+6 -1
View File
@@ -152,6 +152,9 @@ async def create_task(
priority: One of: low, medium, high, or 'none'. Omit (empty string) to leave unset.
project_id: Associate with a project (0 = no project).
milestone_id: Place within a project milestone (0 = no milestone).
When the work belongs to an active plan — a milestone in
enter_project's lists or found by search(content_type=
"milestone") — pass its id, so the plan shows all of its work.
parent_id: Make this a sub-task of another task (0 = top-level).
tags: List of plain-string tags without # prefix.
kind: 'work' (default), 'issue', or 'spike'.
@@ -462,6 +465,8 @@ async def create_records(
project_id: The project every record belongs to (0 = none, or taken
from milestone_id).
milestone_id: File every record under this existing milestone (0 = none).
This is how steps are added to a plan that already exists,
including one start_planning handed back as `existing_milestone`.
force: Bypass the near-duplicate gate for the whole batch. By default
the first record that near-duplicates an existing one BLOCKS the
batch, and its existing id comes back so you can update it instead.
@@ -553,7 +558,7 @@ async def start_planning(
match = await dedup_svc.plan_gate(
uid, project_id, title,
dedup_svc.plan_candidate_text(
body=body, step_texts=[f"{i.title}\n{i.body or ''}" for i in items],
body=body, steps=[(i.title, i.body) for i in items],
),
)
if match is not None:
+4 -3
View File
@@ -767,16 +767,17 @@ async def get_plan_match_threshold(user_id: int) -> float:
def plan_candidate_text(
description: str | None = None,
body: str | None = None,
step_texts: list[str] | None = None,
steps: list[tuple[str | None, str | None]] | None = None,
) -> str:
"""What a plan that doesn't exist yet says about itself, for the gate.
The steps belong in it: a plan passed with steps and no design is still
recognisable by them, and what its steps say is most of what makes two
plans the same plan.
plans the same plan. Each step is (title, body), joined by embedding_text
like every other record that becomes embedded text (#2486).
"""
parts = [(description or "").strip(), (body or "").strip()]
parts += [t.strip() for t in (step_texts or [])]
parts += [embeddings_svc.embedding_text(t, b) for t, b in (steps or [])]
return "\n\n".join(p for p in parts if p)
+3 -2
View File
@@ -408,8 +408,9 @@ async def test_a_bad_threshold_setting_falls_back_to_the_default():
def test_plan_candidate_text_carries_the_steps():
text = plan_candidate_text(description=None, body=" design ", step_texts=["Step one\n", ""])
assert text == "design\n\nStep one"
text = plan_candidate_text(description=None, body=" design ",
steps=[("Step one", None), ("Step two", "with a body"), (None, None)])
assert text == "design\n\nStep one\n\nStep two\nwith a body"
def test_plan_match_response_points_at_adding_steps_not_a_second_plan():