Files
pad/internal/server
xarmian b165e5fe7a fix(server): summarise structured field changes in activity feed (BUG-748) (#236)
* fix(server): summarise structured field changes in activity feed (BUG-748)

The activity-feed `metadata.changes` string is built by `diffFields()` in
`handlers_documents.go`, which used `fmt.Sprintf("%v", val)` to stringify
each old/new value. For structured fields (implementation_notes,
decision_log, or any other slice/map value in item.fields) Go's default
formatting dumps the raw map repr — e.g.
  implementation_notes: → [map[created_at:2026-04-23T... details:Code audit
  on 2026-04-23 found Phases 1, 2, and most of Phase 3 already implemented:
  - **Phase 1a** ... created_by:user summary:Phases 1-3 verified shipped]]

Activity cards on the item detail page surfaced this verbatim, leaking
internal field shape into the UI.

Replace the bare `%v` with a `formatChangeValue` helper:

  - Primitives (string/number/bool): unchanged Go default formatting.
  - Slices: counted summary. Known fields get domain-specific phrasing
    (`(1 note)` / `(N notes)` for implementation_notes, `(1 entry)` /
    `(N entries)` for decision_log); unknown fields fall back to
    `(N items)`.
  - Maps/objects: `(object)` placeholder.
  - nil: empty string.

This is a backend-only change. The frontend `TimelineActivityCard.svelte`
keeps splitting on `→` exactly as before, so the contract is unchanged
beyond the value-formatting.

Companion to PR #235 (frontend `.prose` class fix on TimelineCommentCard).
Together they close BUG-748 — markdown content was unrenderable both
in plain timeline comments AND in activity-feed change pills that
referenced structured field updates.

Tests: 9 new cases in handlers_documents_test.go covering primitives,
added/removed fields, implementation_notes single + plural, decision_log
single + plural, generic slice fallback, object fallback, invalid JSON,
and nil safety. All green.

Verified: go build ./..., go vet ./..., go test ./..., web/npm run build
all clean.

* fix(server): compare values, not display strings, in diffFields (Codex round 1)

Codex flagged two MEDIUM regressions in PR #236 round 1:

1. Object-valued fields (e.g. `convention`, `github_pr`) all stringify to
   the same `(object)` label, so an in-place edit produced
   oldStr == newStr == "(object)" and `diffFields()` silently dropped the
   change from `metadata.changes` — the activity card stopped recording
   that the field had been edited.

2. Same problem for slice fields when length is unchanged: replacing one
   `implementation_note` with a different one (`{"summary":"original"}`
   → `{"summary":"revised"}`) collapsed both sides to `(1 note)` and
   the change vanished from the activity log.

Switch the equality check from string-on-display to `reflect.DeepEqual`
on the raw decoded values. The display strings still go through
`formatChangeValue()` so the activity card stays clean (`(1 note) → (1
note)` for a same-cardinality replacement is coarse but correct — the
user knows something changed and can drill in via the timeline). For
truly identical values the entry is omitted, so no false positives.

`reflect.DeepEqual` is correct for the types `json.Unmarshal` into
`map[string]any` produces: nil, bool, float64, string, []any,
map[string]any.

New tests:
- TestDiffFieldsSameCardinalityArrayChangeStillReported
- TestDiffFieldsObjectMutationStillReported
Each also asserts the no-op case (identical input on both sides emits
nothing).

Verified: go build ./..., go vet ./..., go test -count=1 ./... all green.
2026-04-24 19:13:03 -04:00
..
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00