Skip to content

feat(site): decide guestbook moderation on our xstate fork (lifecycle layer 1) - #82

Merged
ryanleecode merged 6 commits into
mainfrom
feat/guestbook-lifecycle-decision
Oct 9, 2026
Merged

ryanleecode merged 6 commits into
mainfrom
feat/guestbook-lifecycle-decision

Conversation

@systemfsoftware-maker

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

Copy link
Copy Markdown
Collaborator

Layer 1 of the guestbook moderation lifecycle (area A), per docs/plans/2026-10-09-0313-feat-guestbook-moderation-lifecycle-plan.md (approved in conductor ruling A-3, recorded in the plan's first two commits). This layer adds the fork as a dependency (U2), the pure decision and its properties (U3), and the README's xstate removal steps. Nothing calls the decision yet; layer 2 wires it.

Judgment surfaces

flake.nix and flake.lock change (a new systemfsoftware-xstate input, locked at 84e602e, with inputs.nixpkgs.follows = "nixpkgs"). Both are on the origin's R24 list.

U1 spike (fork main 84e602e)

Tarball: nix build github:systemfsoftware/xstate/84e602e#workspace-tarballs → xstate-6.0.0-alpha.64.tgz, extracted to .cache/xstate-spike/ (gitignored). The runtime script ran under Bun 1.4.2 (the harness blocks spawning node); the property file below runs the same calls under Vitest on Node 24.

Item Result
1. resolveState({ value }) per name Visible, Flagged, Hidden each give that value, status active.
2. Six pairs, free transition and method machine.transition Both forms agree. The 3 legal pairs return a new snapshot with R1's target and 0 effects. The 3 illegal pairs return the same snapshot object and 0 effects. isUnhandled is true for exactly the illegal three.
3. type: 'final' Hidden Flagged/Flag returns status done and effects ["@xstate.terminate"] (both forms). KTD1 holds: Hidden is a plain state with no on.
4. resolveState on Banished Throws Error: State 'Banished' does not exist on 'guestbook-entry'. KTD2 holds.
5. Types (tsc 7.0.2) resolveState's snapshot value is exactly 'Visible' | 'Flagged' | 'Hidden'. The free transition returns that value type, with effects typed TransitionExecutableActionObject<never>. The method machine.transition called on a createMachine result returns any (hover: …): any). The same method on a binding typed as the plain StateMachine<…> returns the declared types. v6 also needs { target: '…' } objects for typed transitions; string targets fail to type-check.
6. Lint (pnpm lint) Free transition inside decide: dmmf-workflow(make-body-purity) reports the unsealed import (KTD3's reading holds). Method on the createMachine result: purity passes, but the any return trips typescript(no-unsafe-assignment) ×3 and no-unsafe-member-access ×2. Method on createMachine(…).provide({}): pnpm lint exits 0.
7. Observing R8's empty-action law EntryMoved carries actions: S.Int, the length of the transition's effect list, and ∀p_MoveActions_=Empty pins it at 0. No branch is added.

Raw runtime output:

Visible -> value: "Visible" status: active
Flagged -> value: "Flagged" status: active
Hidden -> value: "Hidden" status: active
{"pair":"Visible/Flag","table":"Flagged","freeValue":"Flagged","freeSame":false,"freeEffects":0,"methodValue":"Flagged","methodSame":false,"methodEffects":0,"isUnhandledFree":false,"isUnhandledMethod":false,"ok":true}
{"pair":"Visible/Vouch","table":"illegal","freeValue":"Visible","freeSame":true,"freeEffects":0,"methodValue":"Visible","methodSame":true,"methodEffects":0,"isUnhandledFree":true,"isUnhandledMethod":true,"ok":true}
{"pair":"Flagged/Flag","table":"Hidden","freeValue":"Hidden","freeSame":false,"freeEffects":0,"methodValue":"Hidden","methodSame":false,"methodEffects":0,"isUnhandledFree":false,"isUnhandledMethod":false,"ok":true}
{"pair":"Flagged/Vouch","table":"Visible","freeValue":"Visible","freeSame":false,"freeEffects":0,"methodValue":"Visible","methodSame":false,"methodEffects":0,"isUnhandledFree":false,"isUnhandledMethod":false,"ok":true}
{"pair":"Hidden/Flag","table":"illegal","freeValue":"Hidden","freeSame":true,"freeEffects":0,"methodValue":"Hidden","methodSame":true,"methodEffects":0,"isUnhandledFree":true,"isUnhandledMethod":true,"ok":true}
{"pair":"Hidden/Vouch","table":"illegal","freeValue":"Hidden","freeSame":true,"freeEffects":0,"methodValue":"Hidden","methodSame":true,"methodEffects":0,"isUnhandledFree":true,"isUnhandledMethod":true,"ok":true}
all six pairs as R3 needs: true
value: Hidden status: done effects: ["@xstate.terminate"]
method value: Hidden status: done effects: ["@xstate.terminate"]
throws: Error - State 'Banished' does not exist on 'guestbook-entry'

No stop condition hit: items 1, 2 and 4 support R3, and one form reaching the fork passes the full lint (item 6).

Declared deviations from the plan (CONST-W3)

  • The machine is createMachine(…).provide({}), not the bare createMachine(…). Fork gap: StateMachine.ts:701 types transition's effects as ExecutableActionObjectFromLogic<this>, and this is createMachine's intersection (createMachine.ts:221-224), so the whole result degrades to any. provide is declared to return the plain StateMachine<…> (StateMachine.ts:590), which types it. Pick, Omit and AnyStateMachine annotations all still give any or lose the value type. Proposed fork fix, one line at StateMachine.ts:701: ExecutableActionObjectFromLogic<this> → ExecutableActionObjectFromLogic<StateMachine<TContext, TEvent, TChildren, TStateValue, TTag, TInput, TOutput, TEmitted, TMeta, TConfig, TActionMap, TActorMap, TGuardMap, TDelayMap, TInternalEvent, TTransitionMeta>>. Applied to a scratch copy of the fork's .d.ts, it makes the direct machine.transition fully typed. After it lands, .provide({}) is deleted.
  • [Workflow.InstrumentationBrand] is {}, not a (state, event) map. A mapped object literal can be mutated to {} and still compiles (checked with tsc), and stryker-ignorer-effect-schema-declarations has no rule for it. That mutant would survive and break R9's 100. The sign workflow uses the same empty map.
  • The two example tests became properties. The existential witnesses are now coverage classes on ∀e_WalkMoves_≡HandWrittenTableWalk: generated walks must take each legal pair, at a minimum share of 0.1 each. AE3's hand-written refusal is now ∀s_CommandStateDecode_≡ThreeNameMembership, drawn from EntryState ∪ String, with the three names written by hand as the oracle. The house property-file rule refuses it examples and hand-picked lists.

What changed

  • flake.nix, flake.lock: the systemfsoftware-xstate input, and its workspace-tarballs in the sfs-deps copy loop and jq -s add merge.
  • pnpm-workspace.yaml: the catalog: line file:.sfs-deps/xstate-6.0.0-alpha.64.tgz and its overrides: mirror. apps/site/package.json: "@systemfsoftware/xstate": "catalog:". pnpm-lock.yaml: from pnpm install.
  • guestbook.schema.ts: EntryState and LifecycleEvent literal schemas.
  • moderate-guestbook-entry.workflow.ts: moderateGuestbookEntry (Workflow.make, EntryMoved | IllegalTransition) over a module-private machine.
  • __tests__/moderate-guestbook-entry.workflow.property.test.ts: seven laws, covering R8's six and AE3's refusal.
  • README.md: removal steps 7-10 (the xstate lines, the flake input, pnpm install, and the README's own guestbook text, so R12's grep can reach exit 1).

Verification (run locally, in the devshell)

  • U2: nix build .#sfs-deps contains xstate-6.0.0-alpha.64.tgz; nix build .#pnpm-store and nix develop -c true succeed; pnpm install --frozen-lockfile passes; the lockfile key is @systemfsoftware/xstate@file:.sfs-deps/xstate-6.0.0-alpha.64.tgz; pnpm check:sfs-sources reports 26 packages, all from file:.
  • U3: pnpm --filter @endgame/site test passes, with the constant-impostor gate and the coverage classes satisfied. The decision matches stryker.mutate (src/**/*.workflow.ts) unchanged. Mutation was not run locally; it runs at the release gate.
  • Sabotage (CONST-T10), each reverted:
    • Swapping the Flagged/Flag target to Visible turns ∀p_LegalPair_≡HandWrittenTable and ∀e_WalkMoves_≡HandWrittenTableWalk red.
    • Dropping the event from the refusal message turns ∀e_RefusedEvent_=UnchangedState red.
    • Inverting the refusal predicate turns three laws red.
  • pnpm check:ci: exit 0 on the committed tree (3612204), covering format, check:sfs-sources, lint, lint:suppressions, typecheck, test, dist and the sandbox proofs.
  • Main moved to 368b1d2 (docs(repo): take the unbuilt brainstorm off main until its code lands #83, which takes the brainstorm doc off main). It was merged in as 9392a3a with a plain merge, no rebase. The merge only deletes the brainstorm doc; layer 2 (feat(site): flag and vouch for guestbook entries (lifecycle layer 2) #84) brings it back.

Notes for the owner of the instruments (not changed here)

  • scripts/check-sfs-sources.ts still says "our flakes (systemfsoftware, stryker-js-effect)". Its check is scope-based, so it covers the fork; only the wording is stale. Ruling A-6 fixes it in layer 2 (feat(site): flag and vouch for guestbook entries (lifecycle layer 2) #84).
  • The effecttsgo oxlint plugin exists only after pnpm run prepare (effect-tsgo patch --oxlint). ignoreScripts: true skips it on install, so pnpm lint fails with Unknown plugin: 'effecttsgo' in a fresh worktree.

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.
@systemfsoftware-maker systemfsoftware-maker changed the title feat/guestbook lifecycle decision feat(site): decide guestbook moderation on our xstate fork (lifecycle layer 1) Oct 9, 2026
…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).

@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 9392a3a, behind 0, hunt grep clean, whole-stack review findings ruled (A-7). Landing with #84.

@ryanleecode
ryanleecode removed this pull request from stack #85 October 9, 2026 09:28
@ryanleecode
ryanleecode merged commit 64cfc96 into main Oct 9, 2026
12 checks passed
ryanleecode added a commit that referenced this pull request Oct 9, 2026
main's tree equals layer 1's head 9392a3a, which this branch already contains, so the merge keeps this branch's tree unchanged.
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