Files
pad/internal/server/app_attribution.go
T
xarmian 41be1bf113 feat(apps): human-facing JSON names the app that wrote an item, comment or version (TASK-3413, SPEC-6 U9c) (#1787)
* feat(apps): human-facing JSON names the app that wrote an item, comment or version (TASK-3413, SPEC-6 U9c)

Items (the creating app), comments and version rows carry via_app (the
install id) and via_app_name (the bot's name, else the origin; kept after
uninstall) on the web API, the MCP get / list summary / history summary
projections, and the TS types, with WorkspaceApp naming the members apps
array. ToolSurface 0.65, additive.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): attribution on search, /changes, playbook show and restore; chunked lookup (codex r1 on U9c)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): attribution on the role board, a comment edit's response and the account export (codex r2 on U9c)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* docs: README tool catalog at v0.65 (TASK-3413)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 04:49:25 -04:00

84 lines
2.1 KiB
Go

package server
import (
"log/slog"
"github.com/PerpetualSoftware/pad/internal/models"
)
// App attribution on human-facing JSON (SPEC-6 U9c, TASK-3413): via_app and
// via_app_name on items (the creating app), comments and version rows, read
// in one batch per response. A lookup failure leaves the fields absent and
// is logged: the label is a decoration, and the response is not refused
// for it.
func (s *Server) hydrateItemsAppAttribution(items []models.Item) {
if len(items) == 0 {
return
}
ids := make([]string, len(items))
for i := range items {
ids[i] = items[i].ID
}
attr, err := s.store.ItemsCreatedViaApp(ids)
if err != nil {
slog.Error("app attribution: items", "error", err)
return
}
for i := range items {
if a, ok := attr[items[i].ID]; ok {
items[i].ViaApp, items[i].ViaAppName = a.InstallID, a.Name
}
}
}
// hydrateItemAppAttribution is hydrateItemsAppAttribution for one item.
func (s *Server) hydrateItemAppAttribution(item *models.Item) {
if item == nil {
return
}
one := []models.Item{*item}
s.hydrateItemsAppAttribution(one)
item.ViaApp, item.ViaAppName = one[0].ViaApp, one[0].ViaAppName
}
func (s *Server) hydrateCommentsAppAttribution(comments []models.Comment) {
if len(comments) == 0 {
return
}
ids := make([]string, len(comments))
for i := range comments {
ids[i] = comments[i].ID
}
attr, err := s.store.CommentsViaApp(ids)
if err != nil {
slog.Error("app attribution: comments", "error", err)
return
}
for i := range comments {
if a, ok := attr[comments[i].ID]; ok {
comments[i].ViaApp, comments[i].ViaAppName = a.InstallID, a.Name
}
}
}
func (s *Server) hydrateVersionsAppAttribution(versions []models.Version) {
if len(versions) == 0 {
return
}
ids := make([]string, len(versions))
for i := range versions {
ids[i] = versions[i].ID
}
attr, err := s.store.VersionsViaApp(ids)
if err != nil {
slog.Error("app attribution: versions", "error", err)
return
}
for i := range versions {
if a, ok := attr[versions[i].ID]; ok {
versions[i].ViaApp, versions[i].ViaAppName = a.InstallID, a.Name
}
}
}