mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-22 10:33:27 +00:00
5320f988ee
* feat(mcp): expand HTTPHandlerDispatcher route table — 10 new commands + declarative routeSpec framework (TASK-966)
TASK-965 shipped HTTPHandlerDispatcher with `item create` as the
proof-of-concept route. This expansion lays a small declarative
framework (`routeSpec` → `RouteMapper`) and wires the high-value
read + write surface, so an OAuth-authenticated agent connecting via
the future /mcp endpoint (TASK-950) gets a useful tool surface
out-of-the-box rather than 11/12 tools returning "not yet implemented
over HTTP transport."
## Framework
`routeSpec` (in dispatch_http_routes.go) is the declarative shape
shared across simple commands:
routeSpec{
method: http.MethodGet,
pathTemplate: "/api/v1/workspaces/{workspace}/items/{ref}",
queryParams: map[string]string{"q": "query", ...}, // dst→src
bodyKeys: []string{"title", "content", ...},
}
`{placeholder}` segments substitute from the input map (snake_case
keys per TASK-964); `collection` and `target_collection` placeholders
are auto-normalized via `collections.NormalizeSlug`. queryParams maps
URL-query names to input keys and handles type coercion for ints
(json.Number / float64) and bool presence-only treatment. bodyKeys
emits a flat JSON body with empty values omitted.
Commands that don't fit the shape (item.create's fields-rolling,
item.move's nested overrides, item.list's path-varies-on-arg,
item.search's renamed q param, item.comment's message→body rename)
stay as standalone RouteMapper functions. The escape hatch is
deliberate — the simple cases get one-line entries; the weird cases
get full functions with their own test coverage.
## Commands wired (10 new + item.create)
| Cmd | Method | Path / Notes |
| --- | --- | --- |
| item create *(prior)* | POST | /workspaces/{ws}/collections/{coll}/items, fields-rolling |
| item show | GET | /workspaces/{ws}/items/{ref} |
| item delete | DELETE | /workspaces/{ws}/items/{ref} |
| item list | GET | path varies on collection arg; filters → query |
| item move | POST | /items/{ref}/move with target_collection + field_overrides body |
| item search | GET | /search?q=...&workspace=... (cross-workspace) |
| item comment | POST | /items/{ref}/comments, message→body, reply_to→parent_id |
| item comments | GET | /items/{ref}/comments |
| project dashboard | GET | /workspaces/{ws}/dashboard |
| collection list | GET | /workspaces/{ws}/collections |
| role list | GET | /workspaces/{ws}/agent-roles |
## Out of scope
`item update` requires read-modify-write semantics (the CLI fetches
the existing fields JSON, merges in --status / --priority / --field
entries, then PATCHes the merged result; the handler treats Fields
as a complete replacement). Implementing that here would mean making
two HTTP calls per dispatch and adding a new "prefetch" hook to the
framework — out of scope for this PR. Captured as the next follow-up.
`project next` / `project standup` / `project changelog` are CLI-side
compositions (multiple API calls + presentation logic) with no
single backing endpoint. Their HTTP equivalent for an agent is "call
project dashboard and read the suggested_next field." Documented in
the follow-up task.
The remaining ~40 commands (attachments, webhooks, library, github,
workspace audit-log, role create / delete, ...) are tracked in the
follow-up.
## Tests
- Framework unit tests: expandPath (substitution, normalization,
escaping, error paths), buildQuery (rename, type coercion,
json.Number support, empty-skip), flatJSONBody (omission rules).
- Per-command unit tests: every wired command has a happy-path
assertion + at least one error path. Custom mappers
(item.list / move / search / comment) get table-driven coverage of
their renames + path-variation behaviour.
- Lock test: TestRouteTable_ContainsExpectedCommands fails loudly if
an entry gets accidentally deleted.
- Integration smoke (TestHTTPHandlerDispatcher_Integration_ReadPaths)
drives item create → list → show → project dashboard → collection
list end-to-end against a real *server.Server, asserting the
full chain stays wired together after the refactor.
Parent: PLAN-943.
* fix(mcp): item.list parity with CLI per Codex review (round 1)
Codex caught three CLI-parity bugs in mapItemList:
1. Default active-status filter missing. `pad item list` ships a
broad inclusion list of active statuses unless --status or --all
is set; the HTTP mapper returned no status filter, so MCP would
leak done/completed/archived items by default.
2. `--parent <ref>` mapped to query param `parent_id`, which the
server treats as a literal ID. The CLI uses `parent`, which
parseItemListParams' unknown-key path routes to resolveParentFilter
for ref → UUID resolution. Without this, `?parent=PLAN-3` would
silently match nothing.
3. `--assign <name>` passed straight through as `assigned_user_id`.
The CLI resolves names → user IDs via a workspace-members lookup
first; passing the raw name to the store filter (which compares
against `i.assigned_user_id` UUID) returns nothing.
Fixes:
1. Added `defaultActiveStatusFilter` constant mirroring the CLI's
hardcoded list at cmd/pad/main.go itemListCmd. Applied when
neither --status nor --all is set; --all drops it (so done items
show); explicit --status wins (so the user can pin to any tier).
2. Renamed the query-param target from `parent_id` to `parent` so
the handler's resolveParentFilter sees it as a field filter and
does ref→UUID resolution.
3. Reject `--assign` with a clear error pointing agents at
`--field assigned_user_id=<uuid>` for explicit-ID filtering. Same
pattern as the existing rejection on item.create. Full name → ID
prefetch belongs in the same follow-up that handles `--assign` on
item create / update.
Tests:
- TestRoute_ItemList_AllItemsPath_AppliesDefaultActiveStatusFilter
asserts the broad inclusion list is on the wire and verifies a
spot-check of well-known active + terminal statuses.
- TestRoute_ItemList_AllFlagDropsDefaultStatus pins --all behaviour.
- TestRoute_ItemList_ExplicitStatusOverridesDefault pins explicit
--status precedence.
- TestRoute_ItemList_FiltersAsQuery now asserts `parent` (not
`parent_id`) is what reaches the wire.
- TestRoute_ItemList_RejectsAssignByName covers the rejection.
- TestRoute_ItemList_NumericLimitFromJSONNumber covers the
json.Number path through the new numericInput helper.
Parent: PLAN-943.
* fix(mcp): normalize collection alias on item.search per Codex review (round 2)
Codex caught: `pad item search foo --collection task` was passing
"task" through verbatim to /api/v1/search?collection=task, but the
search store filters via `c.slug = ?` (exact match) and 0-matches
shorthand. The CLI normalizes to "tasks" first; mapper now does the
same.
Lifted the mutation into a tiny cloneStringMap helper so the input
map the caller hands us isn't accidentally rewritten — the registry
attaches the original via WithDispatchInput, and downstream code
shouldn't see a mapper's normalization leak back.
Tests:
- TestRoute_ItemSearch_NormalizesCollectionAlias asserts task → tasks
on the wire.
- TestRoute_ItemSearch_DoesNotMutateInput pins the no-mutation
contract so future refactors of the helper don't regress.
Parent: PLAN-943.