mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
2df6edeaab
Extends the convention + playbook library HTTP layer to support the shape
the upcoming `pad_library` MCP tool and the updated `pad library` CLI need:
- `GET /api/v1/convention-library?category=X` — server-side filter,
case-sensitive exact match. Unknown categories return an empty slice,
not 404.
- `GET /api/v1/playbook-library?category=X&summary=true` — same filter
plus a new summary mode that strips Content and injects Summary
(first non-heading paragraph, ~240 char cap). Web UI and existing
consumers omit the flag and see the legacy full-body shape. Summary
mode deep-copies category slices so a request never mutates the
package-level library data — TestPlaybookLibrary_SummaryDoesNotMutate
Global pins this.
- `GET /api/v1/library/entry?title=X` — NEW. Returns one matched entry
in a `{type, convention|playbook}` envelope. Conventions-first
precedence mirrors the dispatcher's `library activate` so a title
resolves to the same kind in both surfaces. 400 on missing title,
404 on no match.
Hoisted `playbookSummary` to `collections.PlaybookSummary` so the
bootstrap handler and the new library endpoints share one algorithm.
Bootstrap continues to call it for every playbook entry it returns.
Adds 12 handler tests + the existing bootstrap-summary test stays
green after the move. Lint clean on touched packages; `make check`
gate is blocked by a pre-existing gofmt issue in
internal/store/workspace_members.go captured as BUG-1565.
Parent: PLAN-1560. Unblocks TASK-1562 (CLI) and TASK-1563 (MCP catalog).
57 lines
1.5 KiB
Go
57 lines
1.5 KiB
Go
package collections
|
|
|
|
// PlaybookSummary extracts a short prose hint from a playbook body. Picks the
|
|
// first non-heading non-empty paragraph and caps at ~240 chars so summary
|
|
// payloads stay compact.
|
|
//
|
|
// Lives here (rather than in internal/server/) so the bootstrap handler, the
|
|
// library HTTP endpoints, and any future surface that wants a playbook
|
|
// summary all share one algorithm. The bootstrap handler previously owned
|
|
// this helper (handlers_bootstrap.go) — TASK-1561 hoisted it to the
|
|
// collections package alongside the library data it summarizes.
|
|
func PlaybookSummary(body string) string {
|
|
const maxLen = 240
|
|
const ellipsis = "…"
|
|
for _, line := range splitLines(body) {
|
|
trimmed := trimLeadingSpaces(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
// Skip markdown headings — they're labels, not summaries.
|
|
if len(trimmed) > 0 && trimmed[0] == '#' {
|
|
continue
|
|
}
|
|
if len(trimmed) > maxLen {
|
|
return trimmed[:maxLen-len(ellipsis)] + ellipsis
|
|
}
|
|
return trimmed
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// splitLines is a small dependency-free helper. We avoid bufio.Scanner here
|
|
// because the typical body is small (under 50KB) and allocating a scanner
|
|
// per playbook is wasteful at this scale.
|
|
func splitLines(s string) []string {
|
|
out := []string{}
|
|
start := 0
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\n' {
|
|
out = append(out, s[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
if start < len(s) {
|
|
out = append(out, s[start:])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func trimLeadingSpaces(s string) string {
|
|
i := 0
|
|
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
|
|
i++
|
|
}
|
|
return s[i:]
|
|
}
|