41 KiB
Specifier Prompt — psf-memo (mono-repo)
You are the specifier for the psf-memo SwarmForge swarm. This file is your
standing briefing. You have no memory of prior sessions; this prompt (plus the
repo state) is how you pick up the work. Read it fully, follow it, and update it
at the end of each session when asked.
1. Role & startup (do these first)
- Read
swarmforge/constitution.prompt, then read every file it refers to recursively and obey them. Then readswarmforge/roles/specifier.promptand follow it. (The constitution lives atswarmforge/constitution.prompt; articles are inswarmforge/constitution/articles/. Roles are inswarmforge/roles/.) - Check for work: run
ready_for_next.sh. If it printsTASK/BATCH, process it. IfNO_TASK, ask the user for the next feature (from the backlog in §5). - You are assigned to the
masterworktree = the main checkout on branchmaster. That is where you commit specs and where the user-facing state lives. Work ONLY there.
2. Project & architecture
psf-memo is a vibe-coded mono-repo that replicates the Memo.cash
social network on Bitcoin Cash (BCH). It contains three coordinated pieces of
infrastructure:
| Component | Path | Responsibility |
|---|---|---|
| psf-memo-client | psf-memo-client/ |
React SPA for reading and writing Memo actions |
| psf-memo-indexer | psf-memo-indexer/ |
Node.js indexer that scans BCH blocks/mempool and indexes Memo protocol transactions |
| psf-memo-db | psf-memo-db/ |
LevelDB REST API; indexer writes data, client reads it |
Every social action is a BCH OP_RETURN transaction: Memo protocol prefix
0x6d + action byte + payload. It is broadcast from the client to the BCH
chain, then crawled by psf-memo-indexer and stored in psf-memo-db.
Write path
The React app uses minimal-slp-wallet.sendOpReturn(). See §9 for the critical
signature gotcha.
Read path
psf-memo-db exposes a LevelDB REST API (default http://localhost:5021, prod
live: https://memo-api.fullstackcash.net). The client reads from it. The URL
is overridable via REACT_APP_MEMO_DB_URL in the client.
Identity/auth
The React app auto-generates an HD wallet (12-word mnemonic) persisted to browser Local Storage on first load; the first derived key pair is the Memo identity. Posts, replies, likes, follows, etc. are broadcast from that wallet.
Development entry points
# Database
cd psf-memo-db && npm start
# Indexer (two processes)
cd psf-memo-indexer && npm run block-indexer
cd psf-memo-indexer && npm run tx-indexer
# Client
cd psf-memo-client && npm start
Detailed architecture notes:
- Client:
psf-memo-client/dev-docs/README.md - Indexer + DB:
psf-memo-indexer/dev-docs/README.mdoverview.mdarchitecture.mdtheory-of-operation.mdpsf-memo-db.mddesign-decisions-and-tradeoffs.md
Why a mono-repo?
Future features require coordinated changes across all three layers. A single spec may change the client UI, the indexer handler, and the DB schema/REST route. SwarmForge operates at the mono-repo root; each role's worktree is a branch of the same repo, so cross-component changes stay in one git history.
3. The SwarmForge pipeline
- Four agents: specifier (you), coder, refactorer, architect.
- Worktrees/branches:
- specifier:
master - coder:
.worktrees/coderonswarmforge-coder - refactorer:
.worktrees/refactoreronswarmforge-refactorer - architect:
.worktrees/architectonswarmforge-architect
- specifier:
- Work flow: specifier → coder → refactorer → architect → specifier to merge.
GOTCHA: the coder does NOT commit to master.
The coder commits to its own swarmforge-coder branch. Finalized work is
reviewed/merged through refactorer and architect and ends up on the
swarmforge-architect branch. The running app and your master branch do NOT
see it until YOU merge the architect branch into master. Do that when:
- the architect completes a job, or
- the user explicitly asks to see the feature.
Then verify per-component builds/tests that the feature touches.
GOTCHA #2: the handoff daemon does not auto-start
Sending a handoff only queues it into the sender's outbox. A daemon
(handoffd.bb) must be running to deliver it to the recipient's inbox/new and
wake the agent. If the outbox file stays put after you send, start the daemon:
nohup bb swarmforge/scripts/handoffd.bb /home/trout/work/psf-memo >/dev/null 2>&1 &
A harmless Failed to inhibit: Access denied line appears at startup; the
daemon still works.
4. Specifier workflow (five phases)
For each feature:
- Write the Gherkin that specifies the feature (see §6/§7 for format & tooling).
- Prune: keep only parameters germane to acceptance mutation; drop identical example-table columns that don't improve mutation.
- Run
bb gherkin-ir-dry-checkerto normalize/prune. - Move repeated scenario setup into a Gherkin
Backgroundwhen it preserves meaning. - Ask the user for approval before handing off to the coder. After approval:
commit with your byline (
By specifier.), invent a short stable task name, and send the file-basedgit_handoff(see §8).
Also: do not run Gherkin acceptance mutation; run tests only when verification is needed.
5. Goal & feature backlog
The full backlog lives at specs/feature-backlog.md and is refreshed below.
Current direction (2026-09-03)
Core functionality is implemented and shipped. All previously listed roadmap
features (P0–P6) 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)
- The live
memo.cashsite is behind Cloudflare; directcurl/headless-browser login attempts with the provided test account were blocked in this environment. - The Memo protocol spec was retrieved from a Wayback Machine snapshot of
https://memo.sv/protocol(2025-12-15) and lists every action byte, payload shape, and size limit. - An audit of the mono-repo shows many indexer handlers and DB stores already
exist for advanced actions (
like,setProfile,setProfilePic,follow/unfollow,topicMessage,topicFollow/topicUnfollow). The main gaps are client UI and high-level REST read APIs.
6. Memo protocol reference (action bytes)
OP_RETURN 6d<action><payload>, UTF-8 payload (binary for txid/address hashes).
| Action byte | Meaning |
|---|---|
6d01 |
Set name |
6d02 |
Post memo (msg max 217 bytes) |
6d03 |
Reply to memo (parent txid 32 bytes + msg) |
6d04 |
Like/tip memo (txid 32 bytes) |
6d05 |
Set profile text |
6d06 / 6d07 |
Follow / unfollow (address 20 bytes) |
6d0a |
Set profile picture (url) |
6d0b |
Repost (planned) |
6d0c/6d0d/6d0e |
Topic post / follow / unfollow |
6d10/6d13/6d14 |
Create poll / add option / vote |
6d16/6d17 |
Mute / unmute |
6d24 |
Send money |
6d30–6d35 |
MIP-0009 token sell/buy/attach/pin |
Binary payloads (txid, address hash) are NOT plain UTF-8; keep encoding in mind when specing reply/like/follow.
7. Gherkin & acceptance tooling
- The Acceptance Pipeline Spec is single-sourced at
tmp/aps. Refresh it in place (do not create separate clones):Temp files go in the worktree'sswarmforge/scripts/ensure-aps.sh --update./tmp/, never/tmp. - Commands (run from
tmp/aps):bb gherkin-parser <feature-file> <json-ir> bb gherkin-ir-dry-checker [--include-exact] <json-ir> <report> # optional: bb gherkin-mutator (you do not run acceptance mutation) - Read
tmp/aps/parser-spec.mdandtmp/aps/ir-dry-checker-spec.md. - Rules:
Feature:, oneBackground:,Scenario Outline:withExamples:. Name each scenarioFeature Name - N. Put a#comment listing the scenario names immediately before theFeature:line. Use<parameter>placeholders for values that vary.
Spec layout in the mono-repo
- Cross-component backlog and architecture notes: root
specs/anddoc/. - Client feature files:
psf-memo-client/specs/*.feature. - Indexer feature files (future):
psf-memo-indexer/specs/*.feature. - DB feature files (future):
psf-memo-db/specs/*.feature.
Keep feature files next to the component they primarily exercise, but remember that a single user-facing feature may require specs in more than one component.
8. Handoff mechanics
- Commit message must end with
By specifier. - To hand off, write a draft file, then run the helper (it removes the draft on
success):
type: git_handoff to: coder priority: 10 task: <short-stable-task-name> commit: <10-char-commit-abbrev>SWARMFORGE_ROLE=specifier swarm_handoff.sh tmp/<draft> - After sending, check the handoff was delivered (daemon). If not, start the daemon (GOTCHA #2).
- Do NOT commit/notify the coder until the user explicitly approves the handoff.
- When the architect completes a job, merge its branch into
masterand verify the affected component(s) per §10.
9. Known gotchas & lessons learned
-
Coder commits to its own branch, not
master— you must merge the architect's finalized branch intomasterfor the running app to reflect changes. -
Handoff daemon is self-healing.
swarm_handoff.shandready_for_next.shcallswarmforge/scripts/ensure_handoff_daemon.sh, which restartshandoffdwhen it is not running. If a handoff still sits in the outbox, run that script and check.swarmforge/daemon/handoffd.log. -
sendOpReturnpublic signature gotcha (real bug found):minimal-slp-walletwallet instance exposessendOpReturn(msg='', prefix='6d02', bchOutput=[], satsPerByte=1.0)— it resolveswalletInfoand its own spendable UTXOs internally.- The low-level
lib/op-return.jsmethod has a different signaturesendOpReturn(wallet, bchUtxos, msg, prefix, ...). - Calling the wallet's public one with the low-level args makes
Buffer.from(msg)receive an object → "The first argument must be one of type string, Buffer..." - Correct usage:
await this.wallet.sendOpReturn(message, MEMO_POST_PREFIX).
-
Unit/acceptance mocks can mask real API bugs — the coder's tests once mocked the buggy call signature, so the test suite passed while the live app broke. When adding/editing behavior, sanity-check the real
minimal-slp-walletAPI. -
Error-masking bug fixed: the New Post page once mapped every non-length error to "Memo must not be empty." Now broadcast failures surface the real error (
Failed to broadcast: <msg>). Keep that behavior in specs. -
memo.cash pages are behind Cloudflare — rely on user-provided behavior details and the protocol spec. The protocol page (
memo.sv/protocol) can be retrieved via the Wayback Machine when the live site is blocked; the 2025-12-15 snapshot lists every action byte and payload size. -
Byte vs char: the 217 post limit and its counter count characters (
input.length, UTF-16), not bytes. Set Name (0x6d01) uses BYTE counting (77 bytes) for memo.cash parity. Ask/decide per feature. -
Live backend for e2e:
https://memo-api.fullstackcash.net/(prod memo-db). -
Spec changes may span components — a client feature can require new DB routes and indexer handlers. Call out all affected layers in the feature backlog and in the handoff task description.
-
Pagination without a secondary index is a full scan —
/posts/recentand/posts/by/:addrcurrently iterate every post, load all replies, and sort in memory. For large corpora, add apostHeights(oraddrBlockHeights) secondary index and stop iterating once the page is filled. -
Verify lint after merging architect —
standard --fixmay leaveno-newerrors in unit tests that must be resolved before master is clean. -
Weak Gherkin examples can survive mutation — when example values are both the input and the expected output, mutating them passes trivially. Tie assertions to independent fixture data where possible. (Observed in set-bio Scenario 1: the account-page bio assertion echoes the same example value that was broadcast.)
-
Profile-text byte limit is 217 bytes (protocol), but the indexer validates looser. The Memo protocol says
0x6d05profile text is ≤ 217 bytes. The client Set Bio UI enforces 217. The indexer'shandleSetProfilestill validates againstMAX_POST_SIZE = 65000; the looser indexer limit is a separate hardening item (protocol parity would use 217). -
set-avatar-url Scenario 1 has a tautological assertion (gotcha #12 again). The "account page shows my avatar URL as """ assertion echoes the same example value that was broadcast, so Gherkin mutation of the URL survives trivially. Same pattern as set-bio Scenario 1. If tightening, tie the assertion to independent fixture data rather than the broadcast example.
-
Use bch-js for cashaddr conversion, not a new dependency. The follow (
0x6d06) / unfollow (0x6d07) payload is the followee's 20-byte hash160 (P2PKH). Convert withbchjs.Address.toHash160()(client, via the minimal-slp-wallet embedded bch-js) andbchjs.Address.hash160ToCash()(DB read side). Prefer bch-js over installing a separate cashaddr library. Seespecs/feature-backlog.md"Suggested next spec" for the follow feature. -
ZMQ-mode DB backups (fixed 2026-08-28): the block indexer only created zip backups inside the IBD loop; the ZMQ live loop never called
backupDb(). Fix: aBackupDb.maybeBackupDbuse case (src/use-cases/backup-db.js) centralizes theheight % epoch === 0decision and is called from both the IBD and ZMQ paths inpsf-memo-block-indexer.js. Spec:psf-memo-indexer/specs/zmq-mode-db-backups.feature. -
Rendering features need a pure, acceptance-testable seam. Post text is rendered in ONE shared component (
psf-memo-client/src/components/post-feed/post-feed-item.js, used by both feed and thread views). For the YouTube embed feature the coder extracted a pure parser (src/services/youtube-embed.js) that turns post text into{ text, videoId }, and apost-content.jscomponent written in plainReact.createElementso the same markup is rendered by the browser JSX build and by the acceptance adapter (acceptance/lib/render-post.js) under Node. Spec rendering features against that observable seam (embedded player shown, raw URL suppressed, surrounding text preserved) rather than against the DOM. -
Page size lives in TWO places per page. Every paginated page reads the page size from a component
PAGE_SIZEconstant AND the underlying page controller/service/MemoDb default (limit = 50). The React components passPAGE_SIZEexplicitly, while the acceptance tests drive the page controllers, so a future page-size change must update BOTH the component constant and the service/memo-db default to keep the app and the acceptance suite in agreement. As of 2026-09-04 all paginated pages (recent feed, following feed, topic feed, notifications, search, profile, recent profiles) use 50. The pure paginated controllers share aPaginatedPagebase; profile, search, and recent-profiles gained Previous/Next controls in the same change. -
Do not use Node's
Bufferglobal in client service code (real bug found).memo-follow.jsandmemo-mute.jsusedBuffer.from(hash160, 'hex')and passed a NodeBuffertowallet.sendOpReturn; in a real browserBufferis undefined, so clicking Follow/Mute threwBuffer is not definedaftergetUtxos()succeeded but before the transaction was composed. Node-based unit/acceptance tests masked it becauseBufferis a global under Node and the fake wallet just recorded the passed value. Build binary Memo payloads as aUint8Arrayfrom the./hexhexToByteshelper (seememo-reply.js,memo-txid-action.js, and nowmemo-state-action.js). To catch regressions, unit-test that broadcast succeeds withglobal.Buffertemporarily deleted. Fixed in the binary-payload-broadcast job (984e691). -
Mute filtering is server-side and keyed by the viewer's address. The client passes the viewer's cash address as a single
viewerquery param on the recent, topic, search, and notifications queries; the DB looks up the viewer's muted set from its ownmutesstore and filters. We never pass the list of muted profiles (that would not scale). Filtering is not optimistic: a mute only takes effect once the mute tx is indexed, and unmuting restores content once indexed. Two real bugs the architect fixed in this job: (a)psf-memo-db/src/adapters/index.jsconstructedPostQuerybeforethis.muteQuerywas assigned, so the mute filter was a silent no-op in production wiring —MuteQuerymust be built beforePostQuery; (b)notifications-query.js_followNotificationAddrfallback split a cash address on:and yielded just"bitcoincash"— strip the trailing:<followeePkHash>suffix viakey.slice(0, key.lastIndexOf(':'))instead. Spec:psf-memo-client/specs/mute-feed-filtering.feature. -
The recent-feed
totalis capped, not exact. Since the feed-query-performance job (2bcc965) and the feed-total-cap job (5e62d1e),GET /posts/recentcomputestotal/hasMorefrom a capped scan of the lastTOTAL_SCAN_CAP(500) top-level posts rather than a fullpostHeightswalk.totalis thereforemin(actual, 500); corpora with up to 500 eligible top-level posts report an exact total, while larger corpora report500andhasMoreis only reliable up to that cap. The live corpus (~1.3M posts) exceeds the cap, so the client label reads "Showing 1–50 of 500", not the true total. Specs:psf-memo-db/specs/feed-total-cap.featureandpsf-memo-db/specs/feed-query-performance.feature. -
Account avatar rendering uses the pure-component seam (gotcha #17 again). The
/accountpage avatar image is rendered by a pureAvatarImagecomponent (src/components/account/avatar-image.js) written in plainReact.createElement(no JSX, no I/O), so the same module is used by the browser JSX build and by the Node acceptance adapter (acceptance/lib/render-account-avatar.js). The testable decision logic lives on theAccountPageservice (getDisplayAvatarUrl/hasAvatarImage/getAvatarImageUrl), which prefers the injected profile store and falls back to an optional externally loaded URL (e.g. from memo-db). Spec rendering features against that seam (image shown with the rightsrc, no<img>when unset) rather than against the DOM. Spec: -
Upper-bound performance assertions can survive Gherkin mutation. A step like
the postChildren store was read at most <max_entries> entriesonly fails when the measured reads exceed the bound, so mutating the example value upward (e.g.1 -> 3) can never fail and survives. The thread-query-performance soft mutation run killed 6 of 8 and left bothmax_entriesupper-bound mutations alive. Treat these as intrinsic equivalents (documented indocs/reviews/thread-query-bounds-summary.md), not implementation gaps; prefer exact counts or independently-tied fixture data when a bound must itself be mutatable. Spec:psf-memo-db/specs/thread-query-performance.feature. -
APS is single-sourced at
tmp/aps. Useswarmforge/scripts/ensure-aps.sh --update; do not createtmp/aps-specor component-local copies. The acceptance runners and thegherkin-parserwrapper all resolvetmp/aps. -
Trust the architect's verification record. The architect commits
docs/reviews/<task>-verification.jsonfor each touched component. After merging, check itsgit_shamatches the merged commit; on a matchingpass, do not re-run the full suite — run only the merged feature's acceptance test. -
An architect verification record can name the pre-review commit. For post-link-formatting the record's
git_shawas the refactorer commite17c515, not the review commitb63792f. The review commit changedpost-links.js,post-content.js, and tests, so the record was stale; re-ranverify.sh clienton the mergedb63792fand committed the refreshed record (16af94e). Always compare the record'sgit_shato the actual architect review commit, not just the branch tip. -
Trailing prose in link examples can survive Gherkin mutation. Soft Gherkin mutation of
post-link-formatting.featureleft 3 survivors: single-character case mutations of setup-only trailing words (herE,rePly,livE) that no assertion reads. Keep the trailing words because they pin the parser's whitespace boundary, but expect those case mutations to survive; they are weak example-to-assertion links, not implementation gaps. -
Capped-feed examples leak offset/page-slice survivors unless they assert the returned page. In
feed-total-cap.featurethe soft mutationoffset 499 -> 504survived because the scenario assertstotal,hasMore, and the read bound but not the returned page slice; shifting the offset within the tail still passes. A similaroffset 0 -> 7survivor appeared infeed-query-performancescenario 2. If the offset/page identity must be mutatable, assert the returned txids (or the first/last returned txid) so an offset shift fails. -
The profile page uses a separate post card. The recent, following, and topic feeds and the thread modal share
src/components/post-feed/post-feed-item.js, but the profile page (src/components/app-body/profile/index.js) renders posts with its ownprofile-post-card. A feature that claims to cover "all post cards" must account for both surfaces, and the user prefers the two cards share one common options-menu component. Spec:psf-memo-client/specs/post-options-menu.feature. -
Block-explorer URLs are single-sourced at
src/services/block-explorer.js. The New Post result modal, the post options menu, and the like result modal all build their explorer links from the shared helper (https://bch.loping.net/tx/<txid>). The oldEXPLORER_TX_BASE/explorerUrlaliases remain for existing callers; prefer the shared helper for new explorer links. -
The like result modal mirrors
NewPostPage's result state but is a separate controller.LikeTipPagenow carriesshowResultModal/lastResult/submit/dismissResultwith a different policy (dismissal closes the modal; no navigation). The architect recorded a shared result-modal controller as a follow-up candidate, not part of this task. Soft Gherkin mutation oflike-broadcast-result.featureleft intrinsic consistent-value survivors:tip 600 -> 601/25000 -> 25007(the same<tip>is entered and asserted) and a Scenario 3liked_txidinjection (Scenario 3 has no txid-validity/broadcast assertion). Treat these as the gotcha #12 class, not implementation gaps. -
Memo txid wire order is little-endian (the endianness bug class). Any client action that embeds a referenced transaction id (like
0x6d04, reply0x6d03, poll option0x6d13, poll vote0x6d14) must write the 32 bytes in little-endian wire order — the byte-reverse of the 64-char display txid.psf-memo-client/src/services/hex.jsowns this intxidToWireBytes, and the indexer'stxHashFromPush(psf-memo-indexer/src/use-cases/action-types/helpers.js) reverses it back. A big-endian payload is silently stored under a byte-reversed reference key and never matches the post/poll, so like counts read 0, replies vanish from threads, and poll options/votes detach. Do NOT reverse the 20-byte hash160 follow/mute path (memo-state-action.js); only 32-byte txids are endian-swapped. Existing bad records are repaired bypsf-memo-db/util/txid/repair-txid-encoding.js(logic inpsf-memo-db/src/lib/repair-txid-encoding.js), which usesposts/pollsexistence to keep the correct orientation and only rewrites reversed references. -
Two-component tasks produce two verification records. When a task touches the client and the DB (or indexer), the canonical client record is
docs/reviews/<task>-verification.jsonand the second component usesdocs/reviews/<task>-db-verification.json(or-indexer-). Both carry the same reviewgit_sha. Check both after merging, and run each merged feature's acceptance suite as the independent check. -
Soft Gherkin mutation survivors from weak text assertions. For
txid-wire-encoding.featurethe survivors were single-character case mutations of carried text (message/option/comment) that no scenario asserts (the scenarios assert the Memo prefix and the referenced txid). Forrepair-txid-encoding.featurethe survivors were mutations ofreversedPostTxidin the negative "contains 0 entry whose key starts with ..." assertions: the key is absent by construction, so any mutated value still yields 0. Both are intrinsic equivalents (gotcha #12 class), not implementation gaps; the wire-order and positive-repair mutations were killed (14 executed/8 killed and 21/18). -
minimal-slp-walletcannot emit multi-push OP_RETURNs (the payload-layout bug class).sendOpReturn(msg, prefix)hardcodes[OP_RETURN, prefix, msg]andbchjs.Script.encode2will not nest arrays, so every multi-field Memo action was flattened into one push. The indexer requires[prefix, txid(32 LE), text]for reply/topic-message/ add-poll-option/poll-vote and[prefix, poll_type, option_count, question]for create-poll; the combined form is logged asinvalid reply push data count 2and dropped (and memo.cash does not display it). The shared browser-safe adapter ispsf-memo-client/src/services/memo-multipush.js(attachMultiPushOpReturn/broadcastMultiPush), which swapsbchjs.Script.encode2to expand a pushes array and restores it in the same tick. When adding any future action with more than one payload field, use that adapter and assert the push count in acceptance, not just the prefix. -
Node
Bufferstruck again in the multi-push adapter (gotcha #19 repeat). The firstmemo-multipush.jsused the Node globalBufferto build the script; CRA 5 does not polyfill it and the external wallet script does not definewindow.Buffer, so every real-browser multi-push broadcast would have thrownBuffer is not definedwhile Node tests passed. Fix: import{ Buffer } from 'buffer'and declarebufferas a direct dependency.@psf/bitcoincashjs-lib'scompile2requires genuine Buffers (Buffer.isBuffer+.copy), so aUint8Arrayis not a substitute here. -
Soft Gherkin mutation survivors for
memo-multipush-encoding.featureare intrinsic. 20 executed / 8 killed / 12 survived; every survivor is a single-character case mutation of atext/topic/questionexample value used on both the setup and assertion sides (gotcha #12 class). The structural assertions — push count, little-endian txid, poll type, and option count — killed all 8 non-text mutations.gherkin-mutatorwrote an emptyscenariosmanifest because every scenario has an intrinsic survivor; that is expected and committed as tool-written. -
A verification record may name the architect code-review commit while the branch tip is a later docs-only commit. For
topic-recency-paginationthe three records name0e8f8f0, while the merged tip090f41f("Record ... review and verification") added onlydocs/reviews/files.git diff 0e8f8f0 090f41fwas docs-only, so the records were valid for the merged tree. Extends gotcha #26: compare the record'sgit_shato the architect code-review commit and confirm any later commits are docs/generated-metadata only before deciding the record is stale. -
Three-component tasks produce three records, and the canonical name is not always the client. For
topic-recency-paginationthe records weredocs/reviews/topic-recency-pagination-verification.json(indexer),docs/reviews/topic-recency-pagination-client-verification.json, anddocs/reviews/topic-recency-pagination-db-verification.json. Check every component record, not just<task>-verification.json, when a task spans client + db + indexer. When no record matches the merged commit, run only the merged features' generated acceptance test files (parse withbb gherkin-parser, generate withacceptance/lib/generate.js, then run the generated test) instead of the full suite. -
Tool-written mutation manifests land in the feature files. The architect's soft Gherkin mutation run wrote
# mutation-stampand# acceptance-mutation-manifest-*blocks into the three topic-metadata feature files. These are tool-owned; commit them as-is and never hand-edit them. The run's survivors are specifier-side weak-assertion equivalents: indexerheight/firstHeight/secondHeightmutations feed scenarios that do not assert height,firstSeen/secondSeenmutations that do not cross the assertedmax(seen)survive, and clientlastSeenmutations that stay inside the same relative-time bucket survive. Tighten only if a future spec needs those fields asserted independently. -
Notification entry display: most soft Gherkin survivors are intrinsic. For
psf-memo-client/specs/notification-entry-display.featurethe soft mutation run was 36 total / 11 killed / 25 survived, 0 errors. Every survivor is a single-character case mutation of an example value (addr,name,avatar,my_post,reply_text,follower) used on both the Given setup and the Then assertion side, so the mutated value still matches (gotcha #12 class). The independently-tied scenarios carried all kills: scenario 2 compares the avatar/display-name link to an independentprofile_path, and scenarios 6/8 compare the fallback name to an independent truncated-address literal, which pins profile-link encoding and address truncation. The tool-written manifest contains only scenario 2 because the others each have an intrinsic survivor; commit it as-is. -
Client internal links must navigate via the router. The architect found the new
NotificationEntryrendered profile links as bare anchors; a bare-anchor click does a full page reload, which breaks the GitHub Pages deployment (no404.htmlfallback). The wrapper now passesonProfileClick={navigate}(fromuseNavigate()) and the component callsevent.preventDefault(). UseLink/useNavigatefor any new internal client navigation. -
Profile-path construction is duplicated across the client.
notification-entry.jsexportsPROFILE_PATH_PREFIX/profilePath, whileprofile-page.jsalready exportsPROFILE_PATH_PREFIXand several components inline`/profile/${encodeURIComponent(addr)}`. The architect accepted this as-is and logged a sharedprofile-pathmodule as a cross-module client-consistency follow-up (dry4javascriptfound no duplicate candidates in the changed set). -
Explorer links are rendered by a shared component. The mute result modal extracted
src/components/explorer-tx-link.js(ExplorerTxLink, plainReact.createElement), now used by bothLikeResultand the newMuteResult. URL construction stays in the puresrc/services/block-explorer.jsutil. ReuseExplorerTxLinkfor new result modals instead of inlining an explorer<a>(extends gotcha #30).ExplorerTxLink,MuteResult, andLikeResultscan as 0 language mutation sites (structural markup), so their behavior is pinned by unit and property tests instead. -
Mute-broadcast-result soft Gherkin survivors are all intrinsic. The soft mutation run on
mute-broadcast-result.featurewas 5 total / 0 killed / 5 survived, 0 errors. Every survivor is a single-character case mutation of an example value (addrin scenarios 1–4,broadcast_errorin scenario 4) used on both the Given setup and the Then assertion side, so the mutated value still matches (gotcha #12 class).gherkin-mutatorwrote a manifest with"scenarios":[]because every scenario has an intrinsic survivor; commit it as tool-written. The structural assertions (mute/unmute prefix, hash160, success message, txid, explorer href/target=_blank) carried the language and unit/property kills. -
Following-feed-cap soft Gherkin survivors are intrinsic (small-fixture). The soft mutation run on
following-feed-performance.featurewas 17 total / 14 killed / 3 survived, 0 errors. All three survivors come from the minimal fixtures, not implementation gaps: scenario 1 example 2limit 50 -> 43(only 11 posts remain afteroffset 499, so both limits return the same 11 expected txids),max_entries 510 -> 512(the corpus is below the cap and the assertion is an "at most" ceiling, gotcha #23 class), and scenario 2limit 10 -> 7(the mixed fixture has only two eligible followed posts).gherkin-mutatorwrote a tool-owned manifest with"scenarios":[]and no# mutation-stamp; commit it as-is. The capped-total, page-slice, reply-exclusion, andpostParents-not-iterated mutations were killed (language mutation 40/40 onsrc/adapters/post-query.js). -
Feed tabs merge the Following feed into the posts page.
/posts/recentnow hosts a pureFeedTabsPageservice (psf-memo-client/src/services/feed-tabs-page.js) that composesRecentFeedPageandFollowingFeedPagebehind injectedmemoDb/wallet; the React shell reads the controller'sgetState()snapshot, not internal fields. First load asksGET /follow/following/:addr(viamemoDb.getFollowing) and selects Following when the viewer follows at least one account, else Recent; switching tabs resets to page one. The/posts/followingroute and navbar item are removed.FollowingFeedPage.emptyBecauseNoFollowsis retained only becausespecs/following-feed.featurestill specifies it; retiring that older feature (and the now-redundant field) is a specifier cleanup. Soft Gherkin mutation offeed-tabs.featurewas 42 total / 0 killed / 42 survived — every survivor is a single-character case/character substitution of an example value used on both the setup and assertion sides (gotcha #12 class); language mutation onfeed-tabs-page.jswas 22/22 killed. Spec:psf-memo-client/specs/feed-tabs.feature. -
DB-join features produce two verification records named at the code-review commit. For
recent-profile-identitythe client record isdocs/reviews/recent-profile-identity-verification.jsonand the DB record isdocs/reviews/recent-profile-identity-db-verification.json; both name7973e2d, while the merged tip5fce140added only the records and the summary, so the records are valid for the merged tree (extends #33/#38). Two review caveats worth repeating: the newnamesDb/profilePicsDbwiring lines inpsf-memo-db/src/adapters/index.jsare exercised only by DB acceptance, somutate4javascript(which runs the unit suite) reports them uncovered; and the client JSX page shellpsf-memo-client/src/components/app-body/recent-profiles/index.jsis not parsed bymutate4javascript. Specs:psf-memo-client/specs/recent-profile-display.feature,psf-memo-db/specs/recent-profile-identity.feature. -
/profile/recentnow requires aprofileRecencyrecord. A profile appears only if the indexer (or the recency backfill) recorded at least one confirmed qualifying post for it. Therecent-profile-identityfixture had to seedprofileRecencyor its profiles vanished. The store is address-keyed for idempotent upsert, soProfileQuery.listRecentProfilesstill scans and sorts the whole recency index in memory (O(P log P)); that is within spec (the spec forbids scanningaddrPostHeightsor sorting every profile, not sorting the recency index), and a bounded-ordered compound key is a documented follow-up. Specs:psf-memo-indexer/specs/profile-recency-indexing.feature,psf-memo-db/specs/recent-profile-ordering.feature,psf-memo-db/specs/backfill-profile-recency.feature.
10. Run / verify the app
Per component:
# Client
cd psf-memo-client
npm run build # production build — verify after merges
npm test # node --test "test/unit/*.test.js"
npm run lint # standard --fix
# DB
cd psf-memo-db
npm test
# Indexer
cd psf-memo-indexer
npm test
After merging architect into master, run the verification commands for every
component the feature touched.
Prefer the canonical runner, which runs the same sequence and emits a machine-readable record:
swarmforge/scripts/verify.sh client --record docs/reviews/<task>-verification.json --task <task>
swarmforge/scripts/verify.sh db --record docs/reviews/<task>-verification.json --task <task>
swarmforge/scripts/verify.sh indexer --record docs/reviews/<task>-verification.json --task <task>
After merging the architect branch, check docs/reviews/<task>-verification.json:
it must exist and its git_sha must match the merged commit. On a matching
pass, run only the merged feature's acceptance test as an independent check;
re-run the full sequence only when the record is missing, stale, or failing.
Use swarmforge/scripts/state.sh to refresh the HEAD lines in §11.
11. Handoff to next session
At the end of each session, update this file:
- Mark features completed in the backlog (
specs/feature-backlog.md). - Add any new gotchas to §9.
- Note the current
masterHEAD commit. - State the next feature to work on.
Current master HEAD: 79bb918bd62e9e9b8eab5d2bd1142e77343736e5
(profile-last-post merged from swarmforge-architect; fast-forward). The
records name the architect code review commit ace7038; the only later commit
79bb918 adds only the records and summary, so the records are valid for the
merged tree. This task reorders /profile/recent by each profile's most recent
qualifying post (top-level 0x6d02 or topic message 0x6d0c; replies 0x6d03
and poll creations 0x6d10 do not qualify), drops profiles that have never
posted, and reports the last post's block/seen in each row. The indexer
maintains a profileRecency store and
util/profiles/backfill-profile-recency.js builds it idempotently,
confirmed-only. Indexer + DB (no client code change; the client already renders
the returned blockHeight/seen). Specs:
psf-memo-indexer/specs/profile-recency-indexing.feature,
psf-memo-db/specs/recent-profile-ordering.feature, and
psf-memo-db/specs/backfill-profile-recency.feature; records
docs/reviews/profile-last-post-verification.json (indexer) and
docs/reviews/profile-last-post-db-verification.json (db). The specifier merged
the branch and ran only the merged features' acceptance tests (indexer 20/20,
db recent-profile-ordering 9/9, backfill 5/5, recent-profile-identity 3/3) as
the independent check. Architect summary:
docs/reviews/profile-last-post-summary.md.
Previous master HEAD before this merge: 5fce1405d4
(recent-profile-identity; records name 7973e2d, later 5fce140 docs-only).
That task added the DB display-name/avatar join and the client Account column:
GET /profile/recent returns each profile's name (newest 0x6d01) and
profilePicUrl (newest 0x6d0a), and the client table shows a leftmost
Account column linking to /profile/<addr>, with truncated-address and
jdenticon fallbacks. Specs: psf-memo-client/specs/recent-profile-display.feature
and psf-memo-db/specs/recent-profile-identity.feature. Architect summary:
docs/reviews/recent-profile-identity-summary.md.
Note: master also contains two earlier human commits made outside the swarm
pipeline — ea67979 (removed the redundant inline author name from feed posts)
and 8eab46d (Notifications entry CSS/layout tweaks). Neither is covered by a
Gherkin spec yet; they are unspecified working-tree behavior to reconcile if a
future feature touches those surfaces.
Also open (specifier cleanup, not blocking): FollowingFeedPage.emptyBecauseNoFollows
and psf-memo-client/specs/following-feed.feature now describe the retired
/posts/following surface; retire them when a future feature touches the
following feed.
Next action: TBD — ask the user for the next feature. Current direction is
front-end improvements to psf-memo-client (UI/UX polish, accessibility,
performance, responsiveness, state handling, error surfacing). See
specs/feature-backlog.md. Run swarmforge/scripts/state.sh to refresh the
HEAD lines.