Files
pad/internal/collections/summary.go
T
xarmian 2df6edeaab feat(server): library endpoints gain ?category, ?summary, /library/entry (TASK-1561) (#612)
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).
2026-05-21 13:08:02 -04:00

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:]
}