docs(search): say that the activities query filters as well as ranks
`query` on `openproject_list_work_package_activities` returns `rankRecords(...)`, which drops everything below `DEFAULT_MIN_SCORE`, so it filters as well as ranks. The schema description, README, and ADR-019 all said "rank". A model told "rank" passes a query to a list tool expecting a reordered full history and then reasons over a silently truncated timeline. Behaviour is unchanged — filtering is the more useful behaviour and the tests pin it. Only the wording is corrected, plus tests that keep the wording from regressing. Also records the response-shape and pagination changes from the preceding commits in ADR-019 and the README. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a8d3c23096
commit
e76cfa750b
@@ -351,9 +351,11 @@ This launches a local web UI (typically at `http://localhost:5173`) where you ca
|
|||||||
`openproject_search_meetings`, `openproject_search_wiki_pages`, and `openproject_search_work_packages` rank results by relevance instead of returning a flat list. They accept `matchMode: "fuzzy" | "exact"` (default `"fuzzy"`):
|
`openproject_search_meetings`, `openproject_search_wiki_pages`, and `openproject_search_work_packages` rank results by relevance instead of returning a flat list. They accept `matchMode: "fuzzy" | "exact"` (default `"fuzzy"`):
|
||||||
|
|
||||||
- **`fuzzy`** (default) tolerates typos, reordered words, and partial/natural-language phrasing (e.g. "budget aproval", "what did we decide about hiring"). Results are ranked and each one carries a `score` (0..1 relevance) and `matchedFields` (which fields matched, highest-scoring first); work package and wiki page results also carry a `snippet` excerpt.
|
- **`fuzzy`** (default) tolerates typos, reordered words, and partial/natural-language phrasing (e.g. "budget aproval", "what did we decide about hiring"). Results are ranked and each one carries a `score` (0..1 relevance) and `matchedFields` (which fields matched, highest-scoring first); work package and wiki page results also carry a `snippet` excerpt.
|
||||||
- **`exact`** restores literal case-insensitive substring matching, byte for byte equivalent to this project's pre-fuzzy search behavior.
|
- **`exact`** restores literal case-insensitive substring matching, byte for byte equivalent to this project's pre-fuzzy search behavior. Exact mode still searches descriptions, comments, and agenda content — it changes *how* text is matched, not *what* is searched.
|
||||||
|
|
||||||
`openproject_list_work_package_activities` similarly accepts an optional `query`/`matchMode` pair to rank the activities it already fetched, at no extra API cost. `openproject_list_work_packages` is unchanged — it remains a plain filter tool (`subject` substring match only, no ranking), kept deliberately separate from the ranked `openproject_search_work_packages` tool.
|
On every search tool, `offset` is a **1-based page number**, not an item index: with `pageSize: 20`, `offset: 2` returns results 21-40. Ranked results are returned under `elements`.
|
||||||
|
|
||||||
|
`openproject_list_work_package_activities` similarly accepts an optional `query`/`matchMode` pair, applied to the activities it already fetched at no extra API cost. Note that `query` **filters as well as ranks**: activities scoring below the relevance threshold are dropped, so the response is a relevant subset of the timeline rather than the full history reordered. Omit `query` to retrieve the complete activity list. `openproject_list_work_packages` is unchanged — it remains a plain filter tool (`subject` substring match only, no ranking), kept deliberately separate from the ranked `openproject_search_work_packages` tool.
|
||||||
|
|
||||||
### Projects
|
### Projects
|
||||||
- `openproject_list_projects`: List projects with pagination (`offset`, `pageSize`), sorting (`sortBy`), and filtering.
|
- `openproject_list_projects`: List projects with pagination (`offset`, `pageSize`), sorting (`sortBy`), and filtering.
|
||||||
@@ -362,7 +364,7 @@ This launches a local web UI (typically at `http://localhost:5173`) where you ca
|
|||||||
### Work Packages & Activities
|
### Work Packages & Activities
|
||||||
- `openproject_list_work_packages`: Query work packages with high-level filters (`projectId`, `status`, `typeId`, `assigneeId`, `priorityId`, `subject`, `pageSize`, `offset`) or custom JSON filter expressions.
|
- `openproject_list_work_packages`: Query work packages with high-level filters (`projectId`, `status`, `typeId`, `assigneeId`, `priorityId`, `subject`, `pageSize`, `offset`) or custom JSON filter expressions.
|
||||||
- `openproject_get_work_package`: Retrieve detailed information for a specific work package by ID (`workPackageId: 38`), including description, type, status, priority, author, dates, parent, and children.
|
- `openproject_get_work_package`: Retrieve detailed information for a specific work package by ID (`workPackageId: 38`), including description, type, status, priority, author, dates, parent, and children.
|
||||||
- `openproject_list_work_package_activities`: Retrieve timeline history, field change logs, and discussions for a work package (`workPackageId: 38`), with optional `onlyComments` filtering and optional `query`/`matchMode` relevance ranking.
|
- `openproject_list_work_package_activities`: Retrieve timeline history, field change logs, and discussions for a work package (`workPackageId: 38`), with optional `onlyComments` filtering and an optional `query`/`matchMode` pair that filters the timeline to relevant entries and ranks them (non-matching activities are omitted).
|
||||||
- `openproject_search_work_packages`: Ranked fuzzy search across work package subjects, descriptions, and comments (`query: "budget aproval"`, optional `projectId`, `status`, `matchMode`, `offset`, `pageSize`). Returns `score`, `matchedFields`, and a `snippet` per result, plus `degraded`/`enrichmentFailures` if some deep content couldn't be fetched.
|
- `openproject_search_work_packages`: Ranked fuzzy search across work package subjects, descriptions, and comments (`query: "budget aproval"`, optional `projectId`, `status`, `matchMode`, `offset`, `pageSize`). Returns `score`, `matchedFields`, and a `snippet` per result, plus `degraded`/`enrichmentFailures` if some deep content couldn't be fetched.
|
||||||
|
|
||||||
### Meetings
|
### Meetings
|
||||||
|
|||||||
+20
-2
@@ -525,8 +525,11 @@ only, and work package search matched subjects only.
|
|||||||
6. **No standalone activities search tool**, because OpenProject exposes
|
6. **No standalone activities search tool**, because OpenProject exposes
|
||||||
activities only per work package. Comment text enters the work package deep
|
activities only per work package. Comment text enters the work package deep
|
||||||
phase instead. `openproject_list_work_package_activities` gained an optional
|
phase instead. `openproject_list_work_package_activities` gained an optional
|
||||||
`query`/`matchMode` pair that ranks the activities it already fetched
|
`query`/`matchMode` pair that filters and ranks the activities it already
|
||||||
client-side, at no extra API cost.
|
fetched client-side, at no extra API cost. It applies the same
|
||||||
|
`DEFAULT_MIN_SCORE` gate as every other ranked surface, so a query narrows
|
||||||
|
the returned timeline to relevant entries rather than reordering the full
|
||||||
|
history; omitting `query` returns the complete list.
|
||||||
|
|
||||||
### Consequences
|
### Consequences
|
||||||
|
|
||||||
@@ -547,6 +550,21 @@ only, and work package search matched subjects only.
|
|||||||
section 6.2).
|
section 6.2).
|
||||||
- Scoring constants are tuning parameters. `tests/search/recall.test.ts` is
|
- Scoring constants are tuning parameters. `tests/search/recall.test.ts` is
|
||||||
the acceptance test that governs them.
|
the acceptance test that governs them.
|
||||||
|
- Search response envelopes carry ranked results under `elements` only. The
|
||||||
|
duplicate `items` alias that non-search list functions still return is
|
||||||
|
omitted from `MeetingSearchPage` and `WorkPackageSearchPage`, because the
|
||||||
|
tool layer JSON-stringifies the whole envelope and shipping the same array
|
||||||
|
twice doubled the token cost of every search response.
|
||||||
|
- `offset` on both search pages is a 1-based page number, matching the tool
|
||||||
|
schemas and the OpenProject API. This corrected `openproject_search_meetings`,
|
||||||
|
which previously sliced from item index `offset - 1` and returned a page
|
||||||
|
overlapping the previous one by all but a single result.
|
||||||
|
- The candidate union for work package search runs both legs in every match
|
||||||
|
mode. Restricting exact mode to the server-side `subject ~` filter made
|
||||||
|
description- and comment-only matches unreachable precisely for the literal
|
||||||
|
strings (error codes, ticket refs, commit SHAs) that exact mode exists for.
|
||||||
|
Both legs page the candidate window in batches of 100 up to the 250 cap,
|
||||||
|
since OpenProject enforces a server-side maximum page size.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -185,8 +185,11 @@ export const listWorkPackageActivitiesShape = {
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Optional search text to rank activities by relevance. Matched against " +
|
"Optional search text. FILTERS the activity list down to relevant " +
|
||||||
"comment text and change details; tolerates typos."
|
"entries and ranks them by relevance — activities that do not match " +
|
||||||
|
"are omitted, so the returned timeline is not the complete history. " +
|
||||||
|
"Omit this parameter to get the full, unfiltered list. Matched " +
|
||||||
|
"against comment text and change details; tolerates typos."
|
||||||
),
|
),
|
||||||
matchMode: z
|
matchMode: z
|
||||||
.enum(["fuzzy", "exact"])
|
.enum(["fuzzy", "exact"])
|
||||||
@@ -227,7 +230,7 @@ export const listWorkPackageActivitiesTool: ToolDefinition<
|
|||||||
> = {
|
> = {
|
||||||
name: "openproject_list_work_package_activities",
|
name: "openproject_list_work_package_activities",
|
||||||
description:
|
description:
|
||||||
"Retrieve timeline activities and comments for a work package. Can optionally filter to only include user comments.",
|
"Retrieve timeline activities and comments for a work package. Can optionally filter to only include user comments. Supplying 'query' filters the timeline to relevant entries and ranks them, rather than reordering the full history.",
|
||||||
parameters: listWorkPackageActivitiesShape,
|
parameters: listWorkPackageActivitiesShape,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
execute: handleListWorkPackageActivities,
|
execute: handleListWorkPackageActivities,
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import { OpenProjectClient } from "../src/client/api-client.ts";
|
import { OpenProjectClient } from "../src/client/api-client.ts";
|
||||||
import { runWithContext, type RequestContext } from "../src/context.ts";
|
import { runWithContext, type RequestContext } from "../src/context.ts";
|
||||||
import { listWorkPackageActivities } from "../src/services/work-packages.ts";
|
import { listWorkPackageActivities } from "../src/services/work-packages.ts";
|
||||||
|
import {
|
||||||
|
listWorkPackageActivitiesShape,
|
||||||
|
listWorkPackageActivitiesTool,
|
||||||
|
} from "../src/tools/work-packages.ts";
|
||||||
|
|
||||||
describe("Work Package Activities Service", () => {
|
describe("Work Package Activities Service", () => {
|
||||||
test("parses activities and separates comments from field changes", async () => {
|
test("parses activities and separates comments from field changes", async () => {
|
||||||
@@ -204,3 +208,55 @@ describe("Activity Ranking", () => {
|
|||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Work Package Activities query documentation", () => {
|
||||||
|
/**
|
||||||
|
* `query` does not merely reorder the timeline — rankRecords drops
|
||||||
|
* everything below DEFAULT_MIN_SCORE. A model told the parameter "ranks"
|
||||||
|
* will reason over a silently truncated history, so the schema and the tool
|
||||||
|
* description must both say that it filters.
|
||||||
|
*/
|
||||||
|
test("the query parameter is documented as filtering, not only ranking", () => {
|
||||||
|
const description = listWorkPackageActivitiesShape.query.description ?? "";
|
||||||
|
expect(description.toLowerCase()).toContain("filter");
|
||||||
|
expect(description.toLowerCase()).toMatch(/not the complete history|are omitted/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the tool description warns that a query narrows the timeline", () => {
|
||||||
|
expect(listWorkPackageActivitiesTool.description.toLowerCase()).toContain("filters");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a query really does drop non-matching activities", async () => {
|
||||||
|
const client = {
|
||||||
|
get: async () => ({
|
||||||
|
_type: "Collection",
|
||||||
|
_embedded: {
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2026-09-10T00:00:00Z",
|
||||||
|
comment: { raw: "Renewed the expired TLS certificate" },
|
||||||
|
_links: { user: { href: "/api/v3/users/4", title: "Admin" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
version: 2,
|
||||||
|
createdAt: "2026-09-11T00:00:00Z",
|
||||||
|
comment: { raw: "Merged the dependency bump" },
|
||||||
|
_links: { user: { href: "/api/v3/users/5", title: "Dev" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
} as unknown as OpenProjectClient;
|
||||||
|
|
||||||
|
const all = await listWorkPackageActivities({ workPackageId: 1 }, client);
|
||||||
|
const filtered = await listWorkPackageActivities(
|
||||||
|
{ workPackageId: 1, query: "TLS certificate" },
|
||||||
|
client
|
||||||
|
);
|
||||||
|
expect(all).toHaveLength(2);
|
||||||
|
expect(filtered.map((a) => a.id)).toEqual([1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user