Merge specifier commit 45882dc4aa into swarmforge-coder

By coder.
This commit is contained in:
Chris Troutner
2026-09-03 06:55:43 -07:00
5 changed files with 62 additions and 180 deletions
@@ -56,3 +56,7 @@ class NotificationsPage {
NotificationsPage.NOTIFICATIONS_PATH = NOTIFICATIONS_PATH
module.exports = NotificationsPage
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-02T19:56:07.544Z","module_hash":"647bc4fca66811725670506e32f4db2a94b387b4d318a658a925490607b2caa7","functions":[{"id":"func/NotificationsPage.constructor","name":"NotificationsPage.constructor","line":13,"end_line":19,"hash":"52eca9540d0f0c524cd8b2c6c2a80c37cc1769c2a109bae7c97f080bede56130"},{"id":"func/NotificationsPage.getMyAddress","name":"NotificationsPage.getMyAddress","line":21,"end_line":23,"hash":"3e5d4ac4df379300933a772020528b4ecf4ed83c7386a066f5c270df81adcddd"},{"id":"func/NotificationsPage.load","name":"NotificationsPage.load","line":25,"end_line":45,"hash":"99ce6b9176f964e8794325b59c7e67318514b64388117df930e5addb8ece7ea5"},{"id":"func/NotificationsPage.canLoadMore","name":"NotificationsPage.canLoadMore","line":47,"end_line":49,"hash":"634983bcc6bbe560daad8326db0dd4bf31d5cb9e45c40112565351dceaf8e5d5"},{"id":"func/NotificationsPage.getNotification","name":"NotificationsPage.getNotification","line":51,"end_line":53,"hash":"d01238c7fddb85e9ca7427d512c828ec6524657feb1156a820516641ed27105c"}]}
// mutate4javascript-manifest-end
@@ -52,6 +52,7 @@ class NotificationsQuery {
this._collectFollowNotifications = this._collectFollowNotifications.bind(this)
this._collectLikeNotifications = this._collectLikeNotifications.bind(this)
this._collectReplyNotifications = this._collectReplyNotifications.bind(this)
this._replyNotificationChild = this._replyNotificationChild.bind(this)
this._sortNotifications = this._sortNotifications.bind(this)
}
@@ -120,11 +121,8 @@ class NotificationsQuery {
const childTxid = child?.childTxid
if (!parentTxid || !childTxid) continue
const parent = await this._getPostOrNull(parentTxid)
if (!parent || parent.addr !== addr) continue
const childPost = await this._getPostOrNull(childTxid)
if (!childPost || childPost.addr === addr) continue
const childPost = await this._replyNotificationChild(child, addr)
if (!childPost) continue
notifications.push({
type: 'reply',
@@ -140,6 +138,21 @@ class NotificationsQuery {
return notifications
}
// Return the child post when a child record is a valid reply notification
// for addr: it links a parent authored by addr to a child authored by someone
// else. Returns null when the parent is missing or not authored by addr, or
// the child is missing or authored by addr. Callers must have already
// verified the record carries parentTxid and childTxid.
async _replyNotificationChild (child, addr) {
const parent = await this._getPostOrNull(child.parentTxid)
if (!parent || parent.addr !== addr) return null
const childPost = await this._getPostOrNull(child.childTxid)
if (!childPost || childPost.addr === addr) return null
return childPost
}
_sortNotifications (notifications) {
return notifications.sort((a, b) => {
if (b.blockHeight !== a.blockHeight) return b.blockHeight - a.blockHeight
@@ -162,3 +175,7 @@ class NotificationsQuery {
}
export default NotificationsQuery
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-02T20:02:58.120Z","module_hash":"1a298fb4ce5a783162b6e34e8840073f97e0f74c1dc29c675e4d8954ea6c7ae6","functions":[{"id":"func/NotificationsQuery.constructor","name":"NotificationsQuery.constructor","line":12,"end_line":57,"hash":"962099cdc7a50291b0998e09081f53d6d1ee88cdabda016b9318476ce6129507"},{"id":"func/NotificationsQuery._getPostOrNull","name":"NotificationsQuery._getPostOrNull","line":59,"end_line":66,"hash":"5457ca98de35d7ad6569365f4061c56fdb1495db9d2429f267c5c9fee3f2bb00"},{"id":"func/NotificationsQuery._collectFollowNotifications","name":"NotificationsQuery._collectFollowNotifications","line":69,"end_line":90,"hash":"702150793e1b80639ff125864d0841f8037a83701b1773e5e41451cc087d2995"},{"id":"func/NotificationsQuery._collectLikeNotifications","name":"NotificationsQuery._collectLikeNotifications","line":93,"end_line":113,"hash":"1ec11eb62288a1353a74dfd21f8e01635aca72764b01afe033b57c06b713d5d0"},{"id":"func/NotificationsQuery._collectReplyNotifications","name":"NotificationsQuery._collectReplyNotifications","line":116,"end_line":139,"hash":"ab0acd3f37231a52065caee1728913abaa8c9d154d1a7068f743588521a94d4b"},{"id":"func/NotificationsQuery._replyNotificationChild","name":"NotificationsQuery._replyNotificationChild","line":146,"end_line":154,"hash":"b45fbb877dc7fabd01da40362fe5ab92e2729d4f86cf2a27ac129a5f37bb0e5e"},{"id":"func/NotificationsQuery._sortNotifications","name":"NotificationsQuery._sortNotifications","line":156,"end_line":161,"hash":"61b4ae0b6d450e172db3e59240a3c2ccd5fdf0e6ea1e31023df7bb3d52fdbd5c"},{"id":"func/NotificationsQuery.listNotifications","name":"NotificationsQuery.listNotifications","line":164,"end_line":174,"hash":"02d410897747d5e7a7c7ab1df393dfef13e61569ea53442922786ffd7f172554"}]}
// mutate4javascript-manifest-end
@@ -33,3 +33,7 @@ class ListNotifications extends ListUseCase {
}
export default ListNotifications
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-02T19:56:06.917Z","module_hash":"9cee2e299acbb1203a40425d6d429eec8b1e3978f67fb0f3e76f5c32644d956c","functions":[{"id":"func/ListNotifications.constructor","name":"ListNotifications.constructor","line":12,"end_line":14,"hash":"84b86af8804c5aa8b25ec55f1190863b4652d4460d3d25f0cde0fdad2efda18f"},{"id":"func/ListNotifications.execute","name":"ListNotifications.execute","line":16,"end_line":32,"hash":"59384e928f105a7995ff014b410e0832f812a306c4598e19cfe18e9be570d0f0"}]}
// mutate4javascript-manifest-end
+13 -21
View File
@@ -147,7 +147,16 @@ needed.
## 5. Goal & feature backlog
The full backlog lives at `specs/feature-backlog.md` and is refreshed below.
Goal: reach feature parity with [memo.cash](https://memo.cash).
### Current direction (2026-09-03)
Core functionality is implemented and shipped. All previously listed roadmap
features (P0P6) have been removed from the backlog. For the foreseeable future
the focus is **front-end improvements** to `psf-memo-client` (the React SPA):
UI/UX polish, accessibility, performance, responsiveness, state handling, error
surfacing, and other client-side improvements. A single user-facing feature may
still touch more than one component; call out all affected components in the
task description and in the handoff.
### Research (2026-08-27)
@@ -162,24 +171,6 @@ Goal: reach feature parity with [memo.cash](https://memo.cash).
`follow`/`unfollow`, `topicMessage`, `topicFollow`/`topicUnfollow`). The main
gaps are client UI and high-level REST read APIs.
### Prioritized roadmap
| Tier | Features | Status |
|------|----------|--------|
| **P0** | Post, Set name, Reply, Efficient pagination | ✅ shipped |
| **P1** | Like counts (read side) ✅; client display ✅; Set profile text ✅; Set profile picture ✅; Follow/Unfollow ✅ | ✅ shipped |
| **P2** | Topics (list ✅, feed ✅, post/follow/unfollow) | next |
| **P3** | Polls (create, add option, vote) | ✅ shipped (task `poll-actions`) |
| **P4** | Mute / unmute user | later |
| **P5** | Send money memo action, MIP-0009 token exchange | later |
| **P6** | Repost, ranked feed, notifications, search, tags, following feed | later |
**Suggested next spec:** P2.3/P2.4/P2.5 — add a topic post composer and topic
follow/unfollow buttons on the topic feed page that broadcast `0x6d0c`/
`0x6d0d`/`0x6d0e`, plus the DB read side (topic follow state, topic followers).
The indexer already stores topic messages and follows in `roomsDb`; the missing
pieces are the client write path and the DB read side for follows.
---
## 6. Memo protocol reference (action bytes)
@@ -368,5 +359,6 @@ At the end of each session, update this file:
- Note the current `master` HEAD commit.
- State the next feature to work on.
Current `master` HEAD: `4422372` (merge of the architect's second-pass hardening of the Following feed — P6.6 — verified: client build/test/lint and DB test/lint all pass).
Next action: **P6.3 Notifications** — the Gherkin spec is drafted at `psf-memo-client/specs/notifications.feature` (8 scenarios, parsed clean, DRY-checked) and is awaiting user approval before handoff to the coder. After approval, commit it (`By specifier.`), invent the stable task name `notifications`, and send the `git_handoff` to the coder. Send money (P5.1) was skipped by user decision (no clear use case). Remaining candidates after notifications: P5.25.5 (MIP-0009 token exchange), P6.1 Repost, P6.2 Ranked feed, P6.5 Tags/hashtags.
Current `master` HEAD: `d3903fa` (backlog cleared of all previously listed
features; direction set to front-end improvements to `psf-memo-client`).
Next action: **ask the user for the next front-end improvement to spec**.
+19 -154
View File
@@ -1,8 +1,8 @@
# psf-memo — Prioritized Feature Backlog
# psf-memo — Feature Backlog
**Status**: DRAFT — research refresh 2026-08-27.
**Status**: DRAFT — refreshed 2026-09-03.
**Owner**: specifier.
**Last updated**: 2026-08-29
**Last updated**: 2026-09-03
---
@@ -16,6 +16,21 @@ that is broadcast from the client to the chain and later indexed by
---
## Current direction
Core functionality is implemented and shipped. For the foreseeable future the
focus is **front-end improvements** to `psf-memo-client` (the React SPA).
- All previously listed roadmap features (P0P6) have been removed from this
backlog.
- New work should target the client: UI/UX polish, accessibility, performance,
responsiveness, state handling, error surfacing, and any other front-end
improvements.
- A single user-facing feature may still touch more than one component; call
out all affected components in the task description and in the handoff.
---
## Research notes
- **Protocol reference**: `https://memo.sv/protocol` (Wayback Machine snapshot
@@ -24,19 +39,8 @@ that is broadcast from the client to the chain and later indexed by
`memo.cash` implementation.
- **memo.cash access**: the live site is behind Cloudflare. Direct `curl` and
headless Firefox login attempts from this environment were blocked, so the
roadmap is derived from the protocol spec plus an audit of the existing
roadmap was derived from the protocol spec plus an audit of the existing
mono-repo code.
- **Implementation audit** (2026-08-27):
- Indexer already has handlers for: `setName`, `post`, `reply`, `like`,
`setProfile`, `follow`/`unfollow`, `setProfilePic`, `topicMessage`,
`topicFollow`/`topicUnfollow`.
- `psf-memo-db` already has LevelDB stores for all of those actions and raw
`/level/*` CRUD routes.
- Client already supports: posting, replying, setting name, and the like/tip
broadcast UI.
- **Main gaps**: client UI for setting profile text/picture and
follow/unfollow; high-level read APIs for like counts, follow state, topic
feeds, polls, and mutes.
---
@@ -100,137 +104,6 @@ Reference: https://memo.sv/protocol (Wayback snapshot 2025-12-15)
---
## Tier P0 — Already shipped to `master`
| # | Feature | Memo action | Components | Status |
|---|---------|-------------|------------|--------|
| 0.1 | Post a Memo | `0x6d02` | C, I, D | ✅ |
| 0.2 | Set display name | `0x6d01` | C, I, D | ✅ |
| 0.3 | Reply to a Memo | `0x6d03` | C, I, D | ✅ |
| 0.4 | Efficient post pagination | read | D, I | ✅ |
---
## Tier P1 — Core social verbs (close the read loop)
These features already have indexer storage and (for like/tip) client
broadcast. The remaining work is exposing the read side in `psf-memo-db` and
adding/editing the client UI.
| # | Feature | Memo action | Components | Status | Next work |
|---|---------|-------------|------------|--------|-----------|
| 1.1 | Like / tip a Memo — read side | `0x6d04` | D | ✅ | `likeCount` returned on `/posts/*` and `/posts/:txid/thread` |
| 1.2 | Like / tip a Memo — client display | `0x6d04` | C | ✅ | Feed/Profile/Thread read `likeCount` from API instead of defaulting to 0 |
| 1.3 | Set profile text (bio) | `0x6d05` | C | ✅ | C: "Set Bio" UI on Account page broadcasts `0x6d05` (217-byte limit) |
| 1.4 | Set profile picture | `0x6d0a` | C, I, D | ✅ | C: "Set Avatar URL" UI broadcasts `0x6d0a` (217-byte limit); I/D already read |
| 1.5 | Follow a user | `0x6d06` | C, I, D | ✅ | C: follow button on profile; D: follow state + following/followers lists |
| 1.6 | Unfollow a user | `0x6d07` | C, I, D | ✅ | C: unfollow button; D: follow state |
### Priority order within P1
1. **Like / tip a Memo — read side** ✅ DONE.
2. **Like / tip a Memo — client display** ✅ DONE. The feed, profile, and
thread views now read `likeCount` from the API (profile shows a read-only
like button).
3. **Set profile text** ✅ DONE. Account page "Set Bio" UI broadcasts `0x6d05` with a 217-byte limit and byte counter (task `set-bio`).
4. **Set profile picture** ✅ DONE. Account page "Set Avatar URL" UI broadcasts `0x6d0a` with a 217-byte limit and byte counter (task `set-avatar-url`).
5. **Follow / Unfollow user** ✅ DONE. Profile page Follow/Unfollow button broadcasts `0x6d06`/`0x6d07`; DB exposes follow state and following/followers lists (task `follow-user`).
### Like / tip details
- The like action carries the liked post txid (32 bytes). A pure like has no
BCH output to the author; a tip adds a P2PKH output paying the author.
- `psf-memo-indexer` stores each like in `likesDb` and records `tip` (sats)
when the like tx pays the author.
- `psf-memo-db` aggregates `likesDb` into per-post `likeCount` in
`/posts/recent`, `/posts/by/:addr`, and `/posts/:txid/thread` responses.
- The client already has `MemoLike`, `LikeTipPage`, `LikeButton`, and
`LikeTipModal`; the feed/profile/thread views now read `likeCount` from the
API (profile renders a read-only `LikeButton`).
---
## Tier P2 — Topics
Topics add a second feed axis. The indexer already stores topic messages and
follows in `roomsDb`; the DB and client need query/render support.
| # | Feature | Memo action | Components | Status |
|---|---------|-------------|------------|--------|
| 2.1 | Topic list / discovery | read | D, C | ✅ |
| 2.2 | Topic feed page | read | D, C | ✅ |
| 2.3 | Post a topic message | `0x6d0c` | C, I, D | ✅ shipped (task `topic-actions`) |
| 2.4 | Follow a topic | `0x6d0d` | C, I, D | ✅ shipped (task `topic-actions`) |
| 2.5 | Unfollow a topic | `0x6d0e` | C, I, D | ✅ shipped (task `topic-actions`) |
---
## Tier P3 — Polls
Polls require a new data model and rendering. The indexer has no handler yet.
| # | Feature | Memo action | Components | Status |
|---|---------|-------------|------------|--------|
| 3.1 | Create a poll | `0x6d10` | C, I, D | ✅ shipped (task `poll-actions`) |
| 3.2 | Add a poll option | `0x6d13` | C, I, D | ✅ shipped (task `poll-actions`) |
| 3.3 | Vote in a poll | `0x6d14` | C, I, D | ✅ shipped (task `poll-actions`) |
---
## Tier P4 — Moderation
| # | Feature | Memo action | Components | Status |
|---|---------|-------------|------------|--------|
| 4.1 | Mute a user | `0x6d16` | C, D, I | ✅ shipped (task `mute-user`) |
| 4.2 | Unmute a user | `0x6d17` | C, D, I | ✅ shipped (task `mute-user`) |
---
## Tier P5 — Money & token exchange
| # | Feature | Memo action | Components | Status |
|---|---------|-------------|------------|--------|
| 5.1 | Send money with memo | `0x6d24` | C | 🔴 missing |
| 5.2 | Token sell offer | `0x6d30` (MIP-0009) | C, I, D | 🔴 missing |
| 5.3 | Token buy offer | `0x6d31` (MIP-0009) | C, I, D | 🔴 missing |
| 5.4 | Attach token sale signature | `0x6d32` (MIP-0009) | C, I, D | 🔴 missing |
| 5.5 | Pin token post | `0x6d35` (MIP-0009) | C, I, D | 🔴 missing |
---
## Tier P6 — Advanced social & discovery (later)
| # | Feature | Components | Notes |
|---|---------|------------|-------|
| 6.1 | Repost a memo | C, I, D | `0x6d0b` is marked *planned* in the protocol |
| 6.2 | Ranked feed | C, D | memo.cash "ranked" post ordering |
| 6.3 | Notifications | C, D | replies / likes / follows to my posts |
| 6.4 | Search | C, D | posts / profiles / topics — ✅ shipped (task `search`) |
| 6.5 | Tags / hashtags | C, D | link + filter by tag |
| 6.6 | Following feed | C, D | feed filtered to followed users — ✅ shipped (task `following-feed`) |
---
## Suggested next spec
**Following feed (P6.6)** — ✅ shipped (task `following-feed`), merged from the pipeline and verified (client build/test/lint, DB test/lint). A second-pass hardening merge from the architect (2026-09-02) extracted `parseRequiredString` into `pagination.js` (shared by the addr/room-scoped list use cases) and added client + DB composition-root hardening tests; merged to `master` at `4422372` and re-verified. Following Feed is a read-only aggregation: the viewer sees top-level posts (replies excluded) authored only by profiles they follow, newest first, never their own posts.
- `psf-memo-db`: `GET /posts/following/:addr?limit=&offset=` returning `{ posts, pagination }`, joining the follows index (`FollowQuery.listFollowing`) with the posts index (`PostQuery.scanFollowingFeedTxidsAndCount`). Empty following or no posts → empty result set.
- `psf-memo-client`: a "Following" nav page at `/posts/following` that reads the viewer's wallet address, calls `MemoDb.getFollowingFeed`, and renders posts like the recent feed; shows "You are not following anyone." when following nobody and "No posts from profiles you follow." when followed profiles have no posts.
- Spec: `psf-memo-client/specs/following-feed.feature`.
**Search (P6.4)** — ✅ shipped (task `search`), merged from the pipeline and verified (client build/test/lint, DB test/lint):
- `psf-memo-db`: `GET /search?q=&limit=&offset=` returning `{ posts, profiles, pagination }`. A `SearchQuery` adapter scans `postsDb` (top-level posts only, replies excluded via `postParentsDb`), `namesDb` (profile name), and `profilesDb` (profile bio) with case-insensitive substring matching; a `SearchAll` use case; a `/search` router + controller.
- `psf-memo-client`: a `/search` page with a search box; `MemoDb.search(q)`; a `SearchPage` service; render matching posts like the feed and matching profiles like recent-profiles.
- Empty queries and no-match queries both return an empty result set (no error).
- Spec: `psf-memo-client/specs/search.feature`.
### Deferred / skipped
- **Send money with memo (P5.1)** — `0x6d24` — skipped by user decision
(no clear use case yet).
---
## Notes for future cycles
- Broadcast result (txid) is returned immediately; the action appears in the
@@ -239,11 +112,3 @@ Polls require a new data model and rendering. The indexer has no handler yet.
- Mutations/specs are Gherkin feature files under per-component `specs/` in
the format defined by github.com/unclebob/Acceptance-Pipeline-Specification.
- Root `specs/` contains this backlog and cross-component architecture notes.
## Completed fixes (not roadmap features)
- **ZMQ-mode DB backups** (2026-08-28, task `zmq-db-backups`): the block
indexer only created zip backups during IBD; the ZMQ live loop never called
`backupDb()`. Fixed by centralizing the `height % epoch === 0` decision in a
`BackupDb.maybeBackupDb` use case called from both the IBD and ZMQ paths.
Spec: `psf-memo-indexer/specs/zmq-mode-db-backups.feature`.