Skip to content

feat(site): flag and vouch for guestbook entries (lifecycle layer 2) - #84

Merged
ryanleecode merged 12 commits into
mainfrom
feat/guestbook-lifecycle-wiring
Oct 9, 2026
Merged

ryanleecode merged 12 commits into
mainfrom
feat/guestbook-lifecycle-wiring

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Layer 2 of the guestbook moderation lifecycle (area A, plan docs/plans/2026-10-09-0313-feat-guestbook-moderation-lifecycle-plan.md, units U4-U6), stacked on #82. Any visitor can now flag or vouch for an entry from the page. Two flags with no vouch between them hide it, and list never returns a hidden entry. The two PRs land together (ruling A-6).

What changes for someone using the starter

  • moderate RPC. It takes { id, event } and answers with the entry's new state, or with one of four typed refusals: IllegalTransition, TransitionConflict, EntryNotFound, StoredStateInvalid. A Sandwich cell runs it: it reads the stored state, runs layer 1's decision, then writes. The write is UPDATE … WHERE id = ? AND state = ?from, so a move commits only if the row still holds the state that was read. Zero changed rows is a TransitionConflict (R5). No identity, role or token is checked (R11).
  • Stored state is decoded, never cast (R6). The command's state field is now StoredEntryState: text that decodes to Visible | Flagged | Hidden. A stored Banished becomes the cell's CommandRejected and then StoredStateInvalid, and it never reaches the machine. Layer 1's AE3 property draws this same schema.
  • Migration 0002_add_guestbook_entry_state.sql adds state TEXT NOT NULL DEFAULT 'Visible'. pnpm dev applies it on start: the journeys sign and read rows through the new column.
  • Page. Each entry has Flag <name>'s entry and Vouch for <name>'s entry buttons and shows Flagged when flagged. While a moderation request is in flight, the Flag, Vouch and Sign buttons are all disabled, so a double-click can't send two Flags. Signing itself is unchanged and doesn't disable anything. Each refusal's message goes into the role=alert notice.
  • README. The guestbook paragraph describes the lifecycle. The removal steps now cover migration 0002. Step 8 runs pnpm install --no-frozen-lockfile before the flake edit, and it names pnpm-lock.yaml as the file it rewrites (see U6).
  • scripts/check-sfs-sources.ts (feat(site): decide guestbook moderation on our xstate fork (lifecycle layer 1) #82 reviewer's P3). The message no longer lists flakes by name. It says the packages resolve "from the .sfs-deps tarballs our flake inputs build". A list of names went stale with xstate and would go stale again when an adopter removes the guestbook.
  • docs/brainstorms/2026-10-08-0340-feat-starter-state-of-the-art-plan.md returns at its 365f3e7 content and path (rulings A-5, A-6). git diff 365f3e7 HEAD -- docs/brainstorms/ is empty.

Evidence

U4: the cell fits the RPC. moderateEntry.run(request) typechecks as the moderate handler against the four-refusal error schema (pnpm typecheck exit 0). The store's two infrastructure failures become defects, the way sign and list already handle them.

U5: journeys against pnpm dev (pnpm journeys exit 0, 8 of 8):

  • Two flags with no vouch between them hide an entry; one flag leaves it listed as Flagged (R10, AE4). Sign A, B and C. A goes Flag (Flagged), Vouch (unlabelled), Flag, Flag. B gets one Flag. A fresh browser context then lists B as Flagged and C as signed, and A not at all. Every step is its own page load and real RPC.
  • A vouch for an entry nobody flagged is refused and changes nothing (AE1). The notice reads An entry that is Visible can't take Vouch., and the entry's listed line is identical before and after.
  • A vouch and a flag sent at the same moment end as one of them happening first (AE2). Six fresh Flagged entries each get a concurrent Vouch and Flag from two browser contexts. Every pair must end as ok/conflict/Visible, conflict/ok/Hidden, illegal/ok/Hidden or ok/ok/Flagged.

AE2 admission probe (plan U5; the scenario is admitted only if the probe shows the race test can fail). The AND state = ?3 guard was removed from move, then 8 concurrent Vouch+Flag pairs ran on fresh Flagged entries, 3 times against one pnpm dev. The harness ran each scenario twice per run, so each run printed two result rows (16 pairs per run). Every run had pairs where both moves succeeded and the final state wasn't Flagged, which is a lost update. Tallies per run, as vouch/flag/final:

  • run 1: ok/ok/Visible 3, ok/ok/Hidden 6, ok/ok/Flagged 6, illegal/ok/Hidden 1;
  • run 2: ok/ok/Visible 10, ok/ok/Hidden 3, ok/ok/Flagged 2, illegal/ok/Hidden 1;
  • run 3: ok/ok/Visible 6, ok/ok/Hidden 5, ok/ok/Flagged 5.

The criterion held in 3 of 3 runs, so the race scenario is admitted. The probe file is deleted.

Sabotage (CONST-T10). Each break got a full pnpm journeys run and was reverted (each file byte-identical afterwards):

Break Red scenarios
latest drops WHERE state IN ('Visible', 'Flagged') R10 journey (and the race)
IllegalTransition handler Effect.fail → Effect.die AE1 (and the race)
move drops AND state = ?3 AE2 race

U6: removal procedure in a scratch clone of f7a9a8a (ruling A-7, F1). The full transcript is .cache/u6-removal.log in the worktree; it isn't committed. Before any edit, I opened the clone's devshell to stand in for "the shell you already have open". Then I followed steps 1-10 literally. Decisive lines:

$ pnpm install --no-frozen-lockfile          # step 8, in the open shell
Done in 25.9s using pnpm v12.9.0
[exit 0]
$ git grep -c xstate -- pnpm-lock.yaml; echo grep-lock-exit=$?
grep-lock-exit=1
$ nix flake lock                             # step 9
• Removed input 'systemfsoftware-xstate'
[exit 0]
$ nix develop --command bash -c 'ls .sfs-deps | grep -c xstate; …; pnpm install --frozen-lockfile'   # fresh devshell
0
Lockfile is up to date, resolution step is skipped
[exit 0]
$ git grep -nI xstate -- . ':!*.lock' ':!docs/'; echo r12-grep-exit=$?
r12-grep-exit=1

R12's ':!*.lock' excludes only flake.lock. pnpm-lock.yaml stops matching because step 8 rewrites it, and the README now says so (F3). Step 8 passes --no-frozen-lockfile because pnpm freezes the lockfile by default when CI is set, which would refuse that rewrite. An earlier run, on 85e3d27 with the install after the flake edit, failed the devshell build with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. That run's output wasn't kept, so the step order rests on the passing run above, not on that failure.

Gates. pnpm check:ci passed on the committed tree (exit 0 on f7a9a8a; site tests 13/13): format, check:sfs-sources, lint, lint:suppressions, typecheck, test, dist and the sandbox proofs. There are no inline suppressions and no any or cast. No Stryker run locally; the release gate scores the moderation decision after merge.

Judgment surfaces

scripts/check-sfs-sources.ts is a check script. Only its two messages changed. What it accepts and refuses is untouched, and the change was ruled in A-6.

Stack upkeep

Main moved to 368b1d2 (#83) under the stack. It was merged into feat/guestbook-lifecycle-decision (9392a3a), and that branch was merged into this one. There was no rebase and no force-push.

Units U1-U6 for the visitor-only Visible/Flagged/Hidden lifecycle on the
systemfsoftware/xstate fork. U1 is the fork spike that R3 depends on; if it
fails, A stops with no fallback to upstream.
Adds the systemfsoftware-xstate input (locked at 84e602e) to sfs-deps and
the catalog, so the package resolves as a file: tarball and never from npm.
The README's removal steps now take the dependency and the flake input out
too. flake.nix and flake.lock are judgment surfaces.
moderateGuestbookEntry resolves the entry's decoded state on a module-private
machine, applies its transition, and refuses an unhandled event with
IllegalTransition. Seven property laws carry R8 and AE3's refusal.
…line infer alias

createMachine's result intersects StateMachine with identity members, and
the fork types transition's effects as ExecutableActionObjectFromLogic<this>
(StateMachine.ts:701), so the method returns any on it. provide({}) is
declared to return the plain StateMachine, which restores the types. Delete
it once the fork types that return from its own parameters (conductor
ruling A-4).
Migration 0002 adds the state column. The store reads an entry's state and
writes a move only if the row still holds the state it read. The moderate
RPC runs the lifecycle decision in a Sandwich cell and types all four
refusals. list omits Hidden entries. The page gets Flag and Vouch buttons.
The journeys cover R10, AE1 and the AE2 race. The README removal steps
cover migration 0002, and the check-sfs-sources message stops naming flakes.
…code lands

The doc returns at its 365f3e7 content and path, in the layer that makes
area A true (ruling A-5).
The devshell builds its pnpm store with a frozen install, so entering it
after the flake edit fails until the lockfile no longer lists xstate.
@systemfsoftware-maker
systemfsoftware-maker added this pull request to stack #85 October 9, 2026 05:25
@systemfsoftware-maker systemfsoftware-maker changed the title feat/guestbook lifecycle wiring feat(site): flag and vouch for guestbook entries (lifecycle layer 2) Oct 9, 2026
Step 8 now says the install rewrites pnpm-lock.yaml without its xstate
entries and uses --no-frozen-lockfile, because a frozen install (pnpm's
default under CI) refuses that rewrite. R12's ':!*.lock' covers only
flake.lock, so this step is what clears the lockfile (ruling A-7, F3).
@ryanleecode
ryanleecode removed this pull request from stack #85 October 9, 2026 09:28
@ryanleecode
ryanleecode changed the base branch from feat/guestbook-lifecycle-decision to main October 9, 2026 09:28
main's tree equals layer 1's head 9392a3a, which this branch already contains, so the merge keeps this branch's tree unchanged.

@ryanleecode ryanleecode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified: 10/10 required checks green on 5deab75, up to date with main 64cfc96, diff = layer 2's 13 files, hunt grep clean, whole-stack findings ruled (A-7).

@ryanleecode
ryanleecode merged commit d3e2f1b into main Oct 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants