docs(19.1-04): complete herald tracer plan

Record proceed, coverage, tea-create of sm-herald-plugin, and Self-Check.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-18 13:49:47 +02:00
parent 56b4a72a6a
commit 0d3ba6517b

View File

@@ -0,0 +1,227 @@
---
phase: 19.1-chat-satellites
plan: 04
subsystem: herald
tags: [herald, webpush, vapid, fetchguard, postgres, gorm, testcontainers, jwt, conga]
# Dependency graph
requires:
- phase: 19
provides: analog sibling-plugin layout (boot steps, IF NOT EXISTS baseline, jwt.auth generic API, test-only proof host)
- phase: 19.1
provides: plan 01 tea-create / proceed-and-push remote policy
- phase: 13
provides: fetchguard PublicOnlyMode and WithTransport
provides:
- "decision note .planning/notes/2026-10-18-webpush-go.md pinning github.com/SherClockHolmes/webpush-go v1.4.0"
- "new repository sm-herald-plugin (module git.golem15.com/golem15/sm-herald-plugin, package herald, plugin golem15.herald), pushed to git.golem15.com at 46af07a"
- "idempotent baseline 202610180001_create_golem15_herald_baseline (golem15_herald_push_subscriptions, unique (user_id, endpoint) herald_push_user_endpoint_unique, FK user_id cascade)"
- "PublicOnly subscribe/send, transactional Enqueue Kind golem15.herald.send, PresenceGate DenyAll, NeutralHeraldProfile, herald:vapid-generate, generic /_herald/api/v1 push API"
affects: [19.1-06]
actuals:
tokens: 29450
tasks: 4
commits: 3
plan_head_before: f3ca32e486abbd3d46421a83d0bea0e50627078a
plan_head_after: 56b4a72a6a7b27eb47a29d9e4bbd7140b2287874
tech-stack:
added: [github.com/SherClockHolmes/webpush-go v1.4.0]
patterns:
- "Options.HTTPClient = *fetchguard.Client Do directly; tests inject fetchguard.WithTransport; no adapter / extra Doer / EndpointGuard"
- "SendEvent and SendUnconditional Enqueue Kind golem15.herald.send on the caller tx; worker consults PresenceGate only when Gated; 404/410 prune"
- "Generic herald API: actor from JWT; POST/PATCH/DELETE /push/subscription; GET /push/vapid-public-key public key only"
key-files:
created:
- .planning/notes/2026-10-18-webpush-go.md
- ../sm-herald-plugin/plugin.go
- ../sm-herald-plugin/boot.go
- ../sm-herald-plugin/routes.go
- ../sm-herald-plugin/classes/sender.go
- ../sm-herald-plugin/classes/subscription_service.go
- ../sm-herald-plugin/classes/deny_all.go
- ../sm-herald-plugin/classes/neutral_profile.go
- ../sm-herald-plugin/classes/payload.go
- ../sm-herald-plugin/contracts/presence_gate.go
- ../sm-herald-plugin/contracts/herald_profile.go
- ../sm-herald-plugin/console/vapid_generate.go
- ../sm-herald-plugin/jobs/send.go
- ../sm-herald-plugin/api/push.go
- ../sm-herald-plugin/updates/202610180001_create_golem15_herald_baseline.go
- ../sm-herald-plugin/http_tracer_test.go
- ../sm-herald-plugin/README.md
modified: []
key-decisions:
- "Task 2 reply verbatim: proceed"
- "Locked D-02 baseline id 202610180001_create_golem15_herald_baseline and D-05 prefix /_herald/api/v1; remote policy follows plan 01 proceed-and-push"
- "tea repos create --owner golem15 --name sm-herald-plugin succeeded; main pushed at 46af07a (never force). summercms.go was not pushed."
patterns-established:
- "herald proof host: golem15.user + golem15.herald + a test-only acme fixture; TestFixtureAbsentFromProduction keeps the fixture id and retired tokens out of production sources"
requirements-completed: [SC-19.1-1, SC-19.1-2, D-04, D-05, D-06, D-09, D-10, D-11, D-12]
coverage:
- id: D1
description: "webpush-go v1.4.0 decision note before any herald import (D-09); MIT, jwt/v5 share, Options.HTTPClient = *fetchguard.Client Do; in-tree flare rejected"
requirement: D-09
verification:
- kind: other
ref: "test -f .planning/notes/2026-10-18-webpush-go.md && grep SherClockHolmes/webpush-go and v1.4.0"
status: pass
human_judgment: false
- id: D2
description: "Human lock of D-02 baseline id 202610180001_create_golem15_herald_baseline and D-05 prefix /_herald/api/v1 before the sibling repo existed"
requirement: D-05
verification: []
human_judgment: true
rationale: "Task 2 was checkpoint:decision gate=blocking-human; the user replied proceed and that lock cannot be asserted by a unit test"
- id: D3
description: "sm-herald-plugin with subscribe HTTPS PublicOnly, SendUnconditional after commit, 410 prune, rollback sends 0"
requirement: D-10
verification:
- kind: unit
ref: "go -C ../sm-herald-plugin test -count=1 -short ./... # TestSubscribeEndpoint"
status: pass
- kind: integration
ref: "../sm-herald-plugin/http_tracer_test.go#TestHTTPTracer"
status: pass
- kind: integration
ref: "../sm-herald-plugin/http_tracer_test.go#TestSendEventRollback"
status: pass
human_judgment: false
- id: D4
description: "PresenceGate DenyAll with Boot Warn, NeutralHeraldProfile without Warn, herald:vapid-generate env lines, PATCH/DELETE /push/subscription and GET /push/vapid-public-key"
requirement: D-12
verification:
- kind: unit
ref: "go -C ../sm-herald-plugin vet ./... && go -C ../sm-herald-plugin test -count=1 -short ./..."
status: pass
- kind: other
ref: "grep herald:vapid-generate, golem15.herald.vapid, ShouldSend; grep modules/flare|websockets:generate-vapid-keys is 0"
status: pass
human_judgment: false
- id: D5
description: "Gitea remote golem15/sm-herald-plugin created and main pushed (never force)"
requirement: SC-19.1-1
verification:
- kind: other
ref: "tea repos create --owner golem15 --name sm-herald-plugin; git push -u origin main"
status: pass
human_judgment: false
duration: 11min
completed: 2026-10-18
status: complete
---
# Phase 19.1 Plan 04: Herald repo, PublicOnly send, vapid-generate Summary
**Sibling `sm-herald-plugin` with the webpush-go v1.4.0 note, IF NOT EXISTS subscriptions baseline, fetchguard PublicOnly send on a caller-tx conga job, deny-all PresenceGate, `herald:vapid-generate`, and a JWT generic `/_herald/api/v1` push API.**
## Performance
- **Duration:** 11 min
- **Started:** 2026-10-18T11:37:31Z
- **Completed:** 2026-10-18T11:48:50Z
- **Tasks:** 4
- **Files modified:** 35 (1 note in summercms.go; 34 in the sibling plugin)
## Task 2 human reply (verbatim)
proceed
Meaning recorded at resume: lock D-02 baseline id `202610180001_create_golem15_herald_baseline` and D-05 prefix `/_herald/api/v1`; implement; remote policy follows plan 01 proceed-and-push. After Task 4 is green, tea-create the remote and push main (never force). summercms.go is not pushed. Task 2 was not re-asked.
## Accomplishments
- Decision note `.planning/notes/2026-10-18-webpush-go.md` (hashids shape) pins `github.com/SherClockHolmes/webpush-go` v1.4.0 (MIT, jwt/v5 share, `Options.HTTPClient` is the Do interface so `*fetchguard.Client` is passed directly). In-tree `modules/flare` is the rejected alternative (`push.*`, `websockets:generate-vapid-keys`). No keys generated in Task 1. `go get` ran only after the note was committed.
- New independent git repo `../sm-herald-plugin` (not a submodule): module `git.golem15.com/golem15/sm-herald-plugin`, package `herald`, PluginID `golem15.herald`, `Requires()` empty. Display name Herald / Herald. Never imports user models, `modules/flare`, or `websockets:generate-vapid-keys`.
- Idempotent baseline `202610180001_create_golem15_herald_baseline` for `golem15_herald_push_subscriptions` (`endpoint` varchar 500, `keys` text JSON p256dh/auth, unique `(user_id, endpoint)` `herald_push_user_endpoint_unique`, `user_id` FK cascade).
- `classes.SubscriptionService.Subscribe` rejects non-HTTPS and non-public hosts (D-10). No EndpointGuard type. Send uses `fetchguard.NewClient` `PublicOnlyMode`; `Options.HTTPClient = *fetchguard.Client`. TTL 2419200, urgency normal. 404/410 prune.
- `SendEvent` and `SendUnconditional` Enqueue `jobs.SendArgs` Kind `golem15.herald.send` on the caller tx (D-11). Rollback sends 0. Worker consults `PresenceGate.ShouldSend` only when `Gated`. OBS-01 process-local `sync.Map` 60s.
- PresenceGate default `classes.DenyAll` (`ShouldSend` always false). `SetPresenceGate(nil)` becomes DenyAll. Boot Warns while DenyAll remains. HeraldProfile default `classes.NeutralHeraldProfile` (app name only); no Warn.
- Config keys `golem15.herald.vapid.{subject,public_key,private_key}`. PHP env names overlay empty values at Boot. Boot Warns when keys are missing; never auto-generates. `summer herald:vapid-generate` prints `VAPID_PUBLIC_KEY=` then `VAPID_PRIVATE_KEY=`. Private key is never slog'd. `GET /push/vapid-public-key` returns `{"publicKey"}`.
- Generic API `herald.APIPrefix` (`/_herald/api/v1`) behind `jwt.auth` and `herald-api` 120/min (D-05, D-06): `POST/PATCH/DELETE /push/subscription`, `GET /push/vapid-public-key`. Actor from the JWT, never the body.
- `TestSubscribeEndpoint` (short): http and private-IP rejected. `TestHTTPTracer` (Docker): HTTPS subscribe, SendUnconditional after commit hits the fake, 410 deletes the row. `TestSendEventRollback` (Docker): enqueue in a rolled-back tx; fake send count stays 0. Tests inject `fetchguard.WithTransport`.
- Remote created: `https://git.golem15.com/golem15/sm-herald-plugin`. `git push -u origin main` succeeded (never force). summercms.go was not pushed.
## Task Commits
Each task was committed atomically:
1. **Task 1: webpush-go decision note before any herald import** - `56b4a72` (docs, summercms.go)
2. **Task 2: Confirm D-02 herald baseline id and D-05 prefix** - none (decision only; human: proceed)
3. **Task 3: End-to-end push — subscribe https, SendUnconditional after commit, 410 prune, rollback sends 0** - `f20baac` (feat, sm-herald-plugin)
4. **Task 4: PresenceGate, NeutralHeraldProfile, vapid-generate, remaining push routes** - `46af07a` (feat, sm-herald-plugin)
**Sibling HEAD:** `46af07a` on `origin/main`.
## Files Created/Modified
- `.planning/notes/2026-10-18-webpush-go.md` - D-09 pin of webpush-go v1.4.0 before import
- `../sm-herald-plugin/` - full plugin tree (go.mod Go 1.27.0, analog pins, replace summercms => ../summercms.go)
## Decisions Made
- Task 2 reply verbatim: proceed.
- Locked D-02 / D-05 as named in the continuation. Remote policy follows plan 01 proceed-and-push.
- After Task 4 verify was green, `tea repos create --owner golem15 --name sm-herald-plugin` then `git push -u origin main` (never force).
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] PublicOnlyMode lives in classes.NewSender**
- **Found during:** Task 3
- **Issue:** Acceptance `grep PublicOnlyMode ../sm-herald-plugin/classes/*.go` failed while `NewClient` lived in `boot.go`.
- **Fix:** `classes.NewSender` builds the PublicOnly `fetchguard.Client` when HTTP is nil.
- **Files modified:** `../sm-herald-plugin/classes/sender.go`, `../sm-herald-plugin/boot.go`
- **Verification:** grep PublicOnlyMode in classes/*.go prints at least one line; `go test -short` pass
- **Committed in:** `f20baac`
**2. [Rule 3 - Blocking] analog pins after go mod tidy**
- **Found during:** Task 3
- **Issue:** `go mod tidy` pinned summercms v0.1.9 / sm-user-plugin v0.0.0.
- **Fix:** `go get` summercms@v0.2.2, sm-user-plugin@v0.1.1, pgx@v5.10.0; replace summercms => ../summercms.go and user => ../fonoteka.go/plugins/golem15/user.
- **Files modified:** `../sm-herald-plugin/go.mod`, `../sm-herald-plugin/go.sum`
- **Verification:** `go vet ./...` and `go test -short ./...` pass
- **Committed in:** `f20baac`
---
**Total deviations:** 2 auto-fixed (Rule 3 blocking)
**Impact on plan:** Required so the PublicOnly grep and analog pins match the sibling layout. No scope creep.
## Issues Encountered
The environment injects a `Co-authored-by: Cursor` trailer on commits; it was not added in the commit message source. Task 2 was not re-asked. The sibling directory did not exist at Task 3 start.
## TDD Gate Compliance
N/A — TDD is not applicable for this plan. Tracer verify is automated-only (`HUMAN_VERIFY_MODE=end-of-phase`). After Task 3 automated verify passed: ⚡ Tracer verified end-to-end — expanding.
## Auth gates
Task 2 `checkpoint:decision` `blocking-human` — user replied `proceed`. No further auth gates.
## Known Stubs
None.
## User Setup Required
None - no external service configuration required. The Gitea repo was created in-session. Hosts still paste `herald:vapid-generate` env lines into config (never auto-generated).
## Next Phase Readiness
Ready for 19.1-05 / 19.1-06 on their own gates. Herald tracer is green; PHP method map and goldens stay plan 06.
---
*Phase: 19.1-chat-satellites*
*Completed: 2026-10-18*
## Self-Check: PASSED