Skip to content

feat(journeys,core): buildInput, runtime.goBack, state hooks, typed step/simulator output#38

Merged
diogomiguel merged 11 commits into
mainfrom
feat/journey-runtime-additions
May 12, 2026
Merged

feat(journeys,core): buildInput, runtime.goBack, state hooks, typed step/simulator output#38
diogomiguel merged 11 commits into
mainfrom
feat/journey-runtime-additions

Conversation

@diogomiguel
Copy link
Copy Markdown
Collaborator

@diogomiguel diogomiguel commented May 12, 2026

Summary

Six API additions surfaced by a consumer project first journey-adoption flow that previously had to be hand-rolled at the consumer.

  • ModuleEntryPoint.buildInput?: (state) => TInput — runtime re-derives a step's input from journey state at every entry (initial, push, pop, resume), so back-navigated forms see accumulated state instead of the snapshot frozen at first push. Opt-in; entries without it keep cache-on-push behaviour.
  • JourneyRuntime.goBack(id) on the public runtime so a shell wiring browser-Back / popstate / breadcrumb navigation doesn't have to capture the active step's goBack callback through a React context.
  • useJourneyState<T>(id) and useActiveLeafJourneyState<T>(rootId)useSyncExternalStore-backed hooks. The leaf variant follows activeChildId and re-subscribes as the call chain grows / shrinks.
  • JourneyStepFor<TModules> — discriminated-union step shape so simulator step / currentStep / history reads narrow input by entry without Record<string, unknown> casts. JourneyStep itself becomes JourneyStep<TInput = unknown> (backwards-compatible).
  • simulateJourney fourth generic TOutput — journeys with a typed terminal payload are assignable without as unknown as Parameters<typeof simulateJourney>[0]. SimulateJourneyOptions.modules added so headless tests can bind descriptors (required for buildInput to fire).

Internal cleanup: the leaf-chain bookkeeping that lived in outlet.tsx (useInstanceSnapshot, useCallChain, useLeafId) moved to instance-hooks.ts and is shared with the new hooks — no duplication of the depth bound, subscription teardown, or stable-snapshot caching.

Commits

  • feat(journeys,core): the six additions, 10 new unit / type tests.
  • docs(journeys): README sections covering each new export — buildInput pattern with worked example, runtime / hooks / simulator API tables, TS inference note for JourneyStepFor.

Test plan

  • pnpm --filter @modular-react/journeys test — 393/393 passing.
  • pnpm --filter @modular-react/core test — 167/167 passing.
  • pnpm typecheck — 92/92 packages.
  • Consume from autopilot's EXP-1848 branch (workspace: link) and delete the local ProjectCreationStateContext, useJourneyInstance, and JourneyBackHandlerContext stand-ins to confirm the new APIs cover the same ground.
  • Smoke-test back navigation on a buildInput-enabled step in the autopilot project-creation flow to confirm re-entered forms see the accumulated draftName / draftEmail instead of the first-push snapshot.

Backwards compatibility

All additions are opt-in:

  • Entries without buildInput keep cache-on-push semantics.
  • The goBack prop on ModuleEntryProps keeps working — runtime.goBack(id) is a parallel access point.
  • JourneyStep<TInput = unknown> defaults to the existing wide form; existing usages compile unchanged.
  • simulateJourney's new fourth generic defaults to unknown, matching pre-PR behaviour.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Optional buildInput for module entries to recompute step input on every entry/re-entry (start, forward, back, resume)
    • Public runtime goBack(id) to trigger id-based back navigation
    • New hooks to observe instances and active-leaf state: useJourneyInstance, useJourneyState, useActiveLeafJourneyInstance, useActiveLeafJourneyState
    • Typing improvements: JourneyStepFor, JourneyStep<TInput = unknown>, simulator TOutput generic and modules option
  • Tests

    • Added tests covering buildInput, goBack, journey state hooks, and simulator typings
  • Documentation

    • README expanded with buildInput guidance, hook usage, and testing/typing notes

Review Change Stack

diogomiguel and others added 2 commits May 12, 2026 12:22
…ed step/simulator output

Six API additions surfaced by autopilot's first journey-adoption flow
(EXP-1848) that previously had to be hand-rolled at the consumer:

- `ModuleEntryPoint.buildInput?: (state) => TInput` — runtime re-derives
  a step's input from journey state at every entry (initial, push, pop,
  resume), so back-navigated forms see accumulated state instead of the
  snapshot frozen at first push.
- `JourneyRuntime.goBack(id)` on the public runtime so a shell wiring
  the browser back button doesn't have to capture the active step's
  closure through a React context.
- `useJourneyState<T>(id)` and `useActiveLeafJourneyState<T>(rootId)`
  hooks built on `useSyncExternalStore`; the leaf variant follows
  `activeChildId` and re-subscribes as the chain shifts.
- `JourneyStepFor<TModules>` — discriminated-union step shape so
  simulator step / history reads narrow input by entry without casts.
- `simulateJourney` fourth generic `TOutput` so journeys with a typed
  terminal payload are assignable without `as unknown as Parameters<…>`.

Internal: the leaf-chain bookkeeping that lived in `outlet.tsx`
(`useInstanceSnapshot`, `useCallChain`, `useLeafId`) moves to
`instance-hooks.ts` and is shared with the new hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rneyStepFor

Follow-up to the runtime additions: README sections covering the new
authoring + runtime + testing surface so consumers can find them
without reading the changelog.

- Entry-points table + new "Pattern - `buildInput` for re-entered forms"
  walking through state-derived input and the headless-test wiring
  (`SimulateJourneyOptions.modules`).
- Runtime methods table adds `goBack(id)` with the same no-op guards
  the `goBack` prop already documents.
- Rendering + context table adds `useJourneyState` and
  `useActiveLeafJourneyState`.
- `simulateJourney` API entry calls out the new `TOutput` generic and
  the `JourneyStepFor`-typed `step` / `currentStep` / `history`.
- TS inference notes mention `JourneyStepFor` alongside `StepSpec` so
  readers know which to reach for when asserting on a step snapshot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Caution

Review failed

An error occurred during the review process. Please try again later.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-entry optional buildInput (recomputes step.input on entry/re-entry and same-step state changes), a JourneyRuntime.goBack(id) API, instance-chain snapshot hooks and public state hooks, typed JourneyStepFor/JourneyStep, simulator typing/options, tests, and docs updates.

Changes

Journey Runtime Enhancements: Step Input, Navigation, State Hooks, and Typed Simulation

Layer / File(s) Summary
Core Type Contracts and Exports
packages/core/src/journey-contracts.ts, packages/core/src/types.ts, packages/core/src/index.ts
JourneyStep becomes JourneyStep<TInput = unknown>; JourneyStepFor<TModules> added; ModuleEntryPointBase gains optional buildInput?(state): TInput; new system abort reason "build-input-threw" added; JourneyStepFor re-exported.
Runtime Implementation: withBuiltInput, applyTransition, startFresh, goBack
packages/journeys/src/runtime.ts
Introduces withBuiltInput(step, state) to recompute step.input when an entry declares buildInput; applied in initial start, forward next transitions, same-step { state } results, and goBack flows; runtime aborts on thrown buildInput and exposes goBack(id).
Instance Observation Hooks & Outlet Refactor
packages/journeys/src/instance-hooks.ts, packages/journeys/src/outlet.tsx
Adds useInstanceSnapshot, useCallChain, useLeafId for stable instance subscriptions and chain traversal with depth/cycle guards; outlet delegates to these hooks and removes in-file traversal logic.
Public React Hooks: instance & leaf state readers
packages/journeys/src/use-journey-state.ts, packages/journeys/src/index.ts
Adds useJourneyInstance(instanceId), useJourneyState<TState>(instanceId), useActiveLeafJourneyInstance(rootId), and useActiveLeafJourneyState<TState>(rootId) implemented using instance-hooks and journey context.
Simulator Typing & Headless Module Binding
packages/journeys/src/simulate-journey.ts, packages/journeys/src/types.ts
JourneySimulator parameterized by TModules; public step/currentStep/history use JourneyStepFor<TModules> for entry-specific input narrowing. simulateJourney adds TOutput = unknown generic and SimulateJourneyOptions.modules to forward module descriptors into runtime for headless binding; simulator getters cast results into typed shapes.
Tests: buildInput, goBack, state hooks, simulate typing
packages/journeys/src/build-input.test.ts, packages/journeys/src/runtime-go-back.test.ts, packages/journeys/src/use-journey-state.test.tsx, packages/journeys/src/simulate-journey.test-d.ts
Adds tests verifying buildInput-derived inputs (initial, back, resume), runtime.goBack behavior and no-op cases, state hooks following active-child chains, and simulateJourney typing/type-level assertions.
Documentation & changelog updates; package exports
CHANGELOG.md, packages/journeys/README.md, packages/journeys/src/index.ts, packages/core/src/index.ts
CHANGELOG and README document buildInput semantics, goBack API/no-op conditions, new hooks, JourneyStepFor typing notes, and simulateJourney typing/options; package indexes re-export added hooks and types.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • kibertoad/modular-react#20: Main PR is a follow-up that extends the journeys feature introduced in PR #20—adding buildInput re-computation, JourneyRuntime.goBack, new journey hooks, JourneyStepFor typing, and simulator options.
  • kibertoad/modular-react#24: Overlaps journey contracts and runtime surface (JourneySystemAbortReason/JourneyRuntime changes).
  • kibertoad/modular-react#35: Related runtime and abort-reason changes affecting journey behavior.

Suggested labels

minor

Suggested reviewers

  • casamitjana

Poem

"🐰 I hop through steps and stitch the thread,
buildInput wakes where once it slept,
goBack rewinds the path ahead,
hooks find the leaf where states are kept,
typed steps hum tidy songs for tests."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: buildInput factory, runtime.goBack method, new state hooks, and typed step/simulator features.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/journey-runtime-additions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@diogomiguel diogomiguel self-assigned this May 12, 2026
@diogomiguel diogomiguel marked this pull request as ready for review May 12, 2026 11:33
@diogomiguel
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/journeys/src/types.ts (1)

7-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the unused JourneyStepFor type import.

Line 13 imports JourneyStepFor, but it isn’t used locally (it’s re-exported directly at Line 45), which triggers lint noise.

Suggested diff
 import type {
   AbandonCtx,
   CatalogMeta,
   JourneyHandleRef,
   JourneyPersistence,
   JourneyStep,
-  JourneyStepFor,
   ModuleTypeMap,
   ResumeMap,
   SerializedJourney,
   StepSpec,
   TerminalCtx,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/journeys/src/types.ts` around lines 7 - 23, Remove the unused
JourneyStepFor import from the import list in types.ts: currently JourneyStepFor
is imported along with other types but not used locally (it's re-exported
later), so delete JourneyStepFor from the import statement to silence the linter
while leaving the re-export (export type { JourneyStepFor }) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/journeys/README.md`:
- Around line 2303-2314: The docs table lists a goBack(id) API but the
JourneyRuntime TypeScript interface omitted it; update the JourneyRuntime
interface to include a goBack method with the correct signature (matching the
docs table) so the interface and docs stay in sync—locate the JourneyRuntime
declaration (around where methods like start, hydrate, getInstance, subscribe,
end, forget are declared) and add a goBack(id: InstanceId): void (or the
project-appropriate return type) entry.

---

Outside diff comments:
In `@packages/journeys/src/types.ts`:
- Around line 7-23: Remove the unused JourneyStepFor import from the import list
in types.ts: currently JourneyStepFor is imported along with other types but not
used locally (it's re-exported later), so delete JourneyStepFor from the import
statement to silence the linter while leaving the re-export (export type {
JourneyStepFor }) intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9df6ea4-8b7c-4b80-bfa0-882a52391a77

📥 Commits

Reviewing files that changed from the base of the PR and between 32c7ec4 and 3f3321e.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • packages/core/src/index.ts
  • packages/core/src/journey-contracts.ts
  • packages/core/src/types.ts
  • packages/journeys/README.md
  • packages/journeys/src/build-input.test.ts
  • packages/journeys/src/index.ts
  • packages/journeys/src/instance-hooks.ts
  • packages/journeys/src/outlet.tsx
  • packages/journeys/src/runtime-go-back.test.ts
  • packages/journeys/src/runtime.ts
  • packages/journeys/src/simulate-journey.test-d.ts
  • packages/journeys/src/simulate-journey.ts
  • packages/journeys/src/types.ts
  • packages/journeys/src/use-journey-state.test.tsx
  • packages/journeys/src/use-journey-state.ts

Comment thread packages/journeys/README.md
diogomiguel and others added 2 commits May 12, 2026 12:43
- Drop unused `JourneyStepFor` import in journeys/src/types.ts (it was
  re-exported in the export block but never referenced locally — lint noise).
- Add `goBack(id: InstanceId): void` to the `JourneyRuntime` interface
  snippet in the README so the inline interface matches the methods
  table further down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…afJourneyInstance, better discoverability

Three follow-ups from autopilot's EXP-1848 adoption work:

- `SimulateJourneyOptions.modules` typed as
  `Record<string, ModuleDescriptor<any, any, any, any>>` (matching
  `JourneyRuntimeOptions.modules`) so apps with a custom `TNavItem`
  don't need `as unknown as AnyModuleDescriptor` casts at the call
  site. The bivariant `any` on every generic absorbs the difference.
- New `useActiveLeafJourneyInstance(rootId)` hook returning the full
  leaf `JourneyInstance` so hosts that need `step` / `status` /
  `terminalPayload` from the leaf don't have to pair
  `useActiveLeafJourneyState` with `useJourneyCallStack` +
  `runtime.getInstance(leafId)`. `useActiveLeafJourneyState` now
  delegates to it.
- README "Pattern - `buildInput` for re-entered forms" gains a callout
  reminding test authors to always pass `options.modules` to
  `simulateJourney`; without it `buildInput` silently falls back to the
  cached handler input and contract-validation warnings surface in
  test output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner

@kibertoad kibertoad left a comment

Choose a reason for hiding this comment

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

Reviewed for architecture fit, design, perf, and correctness. Net read: this is a well-scoped, additive PR — six APIs that replace three hand-rolled consumer patterns, with reasonable tests and docs. Most comments below are about polish; the substantive items are (1) a possible behaviour gap with the documented "buildInput fires on every entry" promise on the invoke-resume path, (2) the state: unknown type-safety leak in buildInput, (3) unhandled buildInput throws, and (4) the silent override of the handler-supplied input.

Non-inline note — the test plan still has the autopilot EXP-1848 consumer-side validation unchecked. Six new public APIs are hard to retract once published; worth confirming the actual replacement of ProjectCreationStateContext / useJourneyInstance / JourneyBackHandlerContext lands end-to-end before merging, in case the API shape needs another pass.

Comment thread packages/journeys/src/runtime.ts Outdated
* first push. Returns the original `step` reference when no factory
* is declared — caller-facing identity matters for snapshot bail-out.
*/
function withBuiltInput(step: JourneyStep, state: unknown): JourneyStep {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Three concerns clustered on this helper, all worth thinking about together:

1. Possible gap with the "every entry" promise on invoke-resume. PR body and README claim buildInput runs on "initial start, forward push, goBack pop, and re-entry after a child journey returns." I see three callsites — here at line 1171 (applyTransitionnext), line 1628 (dispatchGoBack), and line 1773 (startInstance). The invoke-resume path only fires buildInput if the parent's resume handler returns { next: … }. If resume just bumps state (as use-journey-state.test.tsx's fixture does: back: ({state}) => ({ state: { counter: state.counter + 10 } })), the parent's step stays put and step.input is the cached snapshot. That's a real-feeling case for a parent step that wants to refresh after a child returns. Either close the gap (re-run withBuiltInput against the parent's current step on resume even when step doesn't change) or tighten the docs to "every step-changing entry".

2. Throws are unhandled. entry.buildInput(state) is called synchronously and any throw bubbles through applyTransition / dispatchGoBack / startInstance into the original caller (popstate handler, component click, runtime.start). Instance may end up half-transitioned. The runtime already wraps transition handlers carefully; buildInput deserves the same treatment — try/catch into an abort with a typed reason, or at minimum a debug-mode log so the failure mode is observable.

3. Step identity. When buildInput is declared, this always allocates a fresh step object (and a fresh input) on every entry. That defeats reference equality for consumers diffing instance.step in onTransition / memoization. Probably fine — the doc-comment already calls out that identity is preserved when no factory exists, which is the important case — but worth a one-liner noting this in the buildInput docs.

Comment thread packages/journeys/src/runtime.ts Outdated

if ("next" in result) {
const nextStep = stepFromSpec(result.next);
const nextStep = withBuiltInput(stepFromSpec(result.next), record.state);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

buildInput's silent override of the handler-supplied next.input is action-at-a-distance: an author who adds buildInput later won't get any signal that the input they're carefully constructing in transitions just became dead code. The README documents the override, but consider either:

  • a dev-only warning when both are set and produce different values, or
  • making the override explicit at the type level (e.g. next.input becomes optional / nullable when the target entry declares buildInput).

The "stamp null / a placeholder" guidance in the README is a smell that the contract leaks.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Worth noting I took the dev-only warning route. Test in build-input.test.ts.

Holding off on making next.input conditionally optional at the type level. It would require StepSpec to map over each entry's buildInput field to flip input between required and optional. Doable but invasive enough that I'd rather want it on its own PR with the type-test additions to cover it.

Comment thread packages/journeys/src/runtime.ts
Comment thread packages/core/src/types.ts
Comment thread packages/core/src/journey-contracts.ts
Comment thread packages/journeys/src/simulate-journey.ts Outdated
Comment thread packages/journeys/src/use-journey-state.ts
Comment thread packages/journeys/src/use-journey-state.ts
Comment thread packages/journeys/README.md
Comment thread packages/journeys/README.md Outdated
Twelve clustered review comments addressed in one pass.

Runtime
- buildInput now re-fires on same-step state changes too (an `{ invoke }`
  arm carrying `state`, or a resume that bumps state without advancing
  the parent step). Closes the gap between the docs' "every entry"
  promise and the previous "every step-changing entry" reality. Covered
  by a new test that drives a state-bumping resume and asserts the
  parent's `step.input` reflects the post-resume state.
- buildInput throws now abort with a new typed
  `JourneySystemAbortReason` code `"build-input-threw"` (carries
  `moduleId`, `entry`, `error`) instead of bubbling through
  applyTransition / dispatchGoBack / startFresh and leaving the
  instance half-transitioned. Mirrors how transition-handler throws are
  already treated. Test asserts the aborted terminalPayload shape.
- Dev-mode warning when a transition handler stamps a `next.input` that
  buildInput would override with a different value — the action-at-a-
  distance footgun the README's "stamp a placeholder" guidance hinted
  at. Stays out of production builds.

React hooks
- New `useJourneyInstance(id)` for symmetry with the leaf variant —
  returns the full `JourneyInstance` for callers with a known id who
  need more than `state`. `useJourneyState` becomes sugar over it.
- `useActiveLeafJourneyState` likewise becomes sugar over
  `useActiveLeafJourneyInstance(rootId)?.state`. The instance variant
  is now documented as the recommended primitive when the leaf may be
  any of several journeys — `inst.journeyId` is the natural
  discriminator vs typing `<ParentState | ChildState>` and asserting
  manually.

Types / API hygiene
- `SimulateJourneyOptions.modules` switched from inline
  `ModuleDescriptor<any, any, any, any>` back to
  `Readonly<Record<string, AnyModuleDescriptor<any>>>` so the `any`s
  are localized to one definition site in core.
- `JourneyStep<TInput>` JSDoc gains `@see JourneyStepFor` so IDE hovers
  point at the typed counterpart.
- `MAX_CHAIN_DEPTH` in instance-hooks.ts no longer exported (it was
  private in outlet.tsx before the extraction; the move accidentally
  promoted it).
- `runtime.goBack(id)` comment reworded — the previous comment claimed
  "stale-closure protection still applies" but there's no captured
  token on the id-based path, so the runtime's `stepToken` check is a
  no-op here. New comment is honest about which guards actually fire.

README
- Hooks table reordered: instance variants listed first; state
  variants documented as sugar over them. Discrimination pattern
  called out explicitly so callers don't reach for `<A | B>` casts.
- `buildInput` pattern section gains a NOTES block:
  - State-typing tradeoff: `(state: unknown)` at the module surface
    pushes type safety to the author; `tsconfig` with
    `strictFunctionTypes` may flag the narrowed annotation, and a
    wrong annotation passes silently. A `buildInputFor<TState>` helper
    on the journey side is called out as a candidate follow-up.
  - Step-identity churn: every entry allocates a fresh `{ moduleId,
    entry, input }` when buildInput is declared. Consumers diffing
    `instance.step` reference equality should compare structurally.
- Throw semantics documented inline (typed
  `JourneySystemAbortReason`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/journeys/src/build-input.test.ts`:
- Around line 127-140: The test currently asserts no warning but its description
says it should test the warning path; update the test that uses
createJourneyRuntime, runtime.start and createTestHarness(...).fireExit so it
performs a second push that produces a divergent next.input vs the email
module's buildInput (e.g., fireExit a second time with a payload that makes
handler-returned next.input differ from buildInput), then assert the
console.warn spy (vi.spyOn(console, "warn")) was called (use
expect(warn).toHaveBeenCalled()) — optionally clear or reset the spy between
pushes if needed to avoid false positives.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37b287b0-7efc-4621-b973-d20f58c38678

📥 Commits

Reviewing files that changed from the base of the PR and between 4447781 and 0ed07a4.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • packages/core/src/journey-contracts.ts
  • packages/journeys/README.md
  • packages/journeys/src/build-input.test.ts
  • packages/journeys/src/index.ts
  • packages/journeys/src/instance-hooks.ts
  • packages/journeys/src/runtime.ts
  • packages/journeys/src/simulate-journey.ts
  • packages/journeys/src/use-journey-state.ts
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/journeys/src/index.ts
  • packages/core/src/journey-contracts.ts
  • packages/journeys/src/runtime.ts
  • packages/journeys/src/simulate-journey.ts

Comment thread packages/journeys/src/build-input.test.ts Outdated
diogomiguel and others added 2 commits May 12, 2026 14:58
Addresses concerns surfaced reviewing 0ed07a4:

Correctness
- Skip the same-step `buildInput` rebuild for the `{ invoke }` arm.
  Previously a `buildInput` throw on the rebuild block would abort the
  parent AFTER `beginInvoke` had already minted the child, orphaning
  the in-flight child with a now-terminal parent. The rebuild is also
  pointless during invoke (parent is paused, hidden form doesn't render);
  `buildInput` re-fires naturally when the resume's `{ next }` lands.

DRY / readability
- Extract `abortFromBuildInputThrow` — collapses the 4 near-identical
  try/abort/fireOnError blocks at the `withBuiltInput` callsites
  (initial start, next-arm push, goBack pop, same-step rebuild) into a
  shared helper. Each callsite is now 4 lines, not 20.
- Rename `deepEqual` → `shallowEqual`. The implementation is one-level
  strict-equality per top-level field; the old name was misleading.
- Comment on the same-step rebuild block explicitly calls out that
  `record.step === previousStep` is reference equality (not just
  `!== null`) — the arm-replacement paths already ran `withBuiltInput`
  and replaced `record.step`, so the equality check is what excludes
  them from re-running here.

Test correctness
- Split the dev-mode drift-warning test into two cases:
  1. The original (renamed) covers the no-drift path (handler-stamped
     input shallow-equals buildInput output).
  2. A new case actually drives drift (handler stamps `{ value: 0 }`,
     buildInput returns `{ value: 99 }`) and asserts the warning fires
     with the expected message. The earlier single test asserted
     `not.toHaveBeenCalled()` despite its name promising drift.

Docs
- `JourneySystemAbortReasonCode` JSDoc calls out that the string codes
  are reserved — author abort payloads using one of them get
  misidentified by `isJourneySystemAbort`. Suggests app-slug
  namespacing.
- `shallowEqual` JSDoc spells out that nested-object drift may slip
  through (dev-only warning, observability log not load-bearing).
- `useJourneyInstance` / `useActiveLeafJourneyInstance` JSDoc now lead
  with WHY they're the primitives, not just WHAT they do. State
  variants are documented as sugar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aa21229 narrowed the same-step rebuild to skip the `{ invoke }` arm
(orphan-child correctness fix), but the README still claimed buildInput
fires on an `{ invoke }` carrying state. Aligned the doc with the
implementation; called out why the invoke case is excluded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
diogomiguel and others added 3 commits May 12, 2026 15:16
Follow-up from PR review on `buildInput`'s state typing.

The inline annotation pattern — `buildInput: (state: ProjectState) => …`
— works in most configs but trips `strictFunctionTypes`: TS can refuse
to assign a narrower-parameter function to the entry's declared
`(state: unknown) => TInput` shape. `buildInputFor<TState>()` does the
unknown→TState cast inside its own body, so the *outer* signature
always matches the entry's declared shape regardless of consumer
strictness. Curried, mirroring `defineJourney`'s shape: spell `TState`
explicitly, infer `TInput` from the body.

What this doesn't do: verify that the spelled `TState` matches the host
journey's actual state type. Modules stay journey-agnostic — the
cross-cut isn't expressible at the entry-declaration site. README spells
this out for both annotation patterns.

- `buildInputFor` in `@modular-react/core` next to `defineEntry`
- Type tests covering `state` narrowing, defineEntry composition, and
  TInput-mismatch rejection via the contextually-expected signature
- Runtime sanity test against the simulator
- README NOTES block updated to show both annotation patterns side by
  side, and the API-reference table picks up the new export

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Concerns surfaced reviewing 1427c8e:

- Drop the parallel `wrappedModule`/`wrappedJourney` test fixture and
  convert `emailModule`'s `buildInput` to the `buildInputFor<FormState>()`
  wrapper instead. `nameModule` keeps the inline annotation; the
  "rebuilds input from state when navigating back" test now exercises
  both patterns side by side, pinning their runtime equivalence
  without a 35-line duplicate fixture.
- Cross-link the curry rationale: `buildInputFor`'s JSDoc calls out that
  `defineJourney` uses the same visual two-call shape for a different
  reason (partial-inference workaround vs. clean TInput inference), and
  `defineJourney` gains an `@see` line pointing at `buildInputFor`.
- Trim "Zero runtime cost beyond the wrapping closure" from
  `buildInputFor`'s JSDoc — the closure is two lines below in the body;
  the comment was narrating WHAT instead of WHY.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dev-mode `buildInput` drift warning was comparing `step.input`
(the value stored on the step at the moment `withBuiltInput` ran)
against the freshly-derived value. That worked at the next-arm and
startFresh callsites — `step.input` there is the handler's literal
stamp from `result.next.input` / `def.start(...).input`. But the same
helper also runs on `dispatchGoBack` (where `step.input` is what was
stored in the history frame at first push — itself a prior
`buildInput` derivation) and on the same-step rebuild after a
state-changing resume (where `step.input` is the prior step's derived
value). Anytime state changed between visits, those callsites
false-positive: the comparison reads "a previous derivation differs
from the current one," which is exactly what `buildInput` is supposed
to do.

Fix:

- `withBuiltInput` becomes a pure derivation helper — no drift check
  at all. Its JSDoc spells out why: a popped frame's `input` is a
  prior derivation, not a handler stamp, and comparing it would
  false-positive whenever state changed between visits.
- New `warnIfHandlerInputDiffers(handlerInput, derivedInput, step)`
  helper does the structural comparison. Called ONLY at the next-arm
  and startFresh sites, where the handler's literal `next.input` /
  `def.start(...).input` is still in scope. Same shallow-equality,
  same warning text.

Regression test: drive forward to bump state, then goBack — assert
the warning did not fire (it would have, on every back-nav, before
this fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@diogomiguel diogomiguel requested a review from kibertoad May 12, 2026 14:51
@diogomiguel
Copy link
Copy Markdown
Collaborator Author

Six new public APIs are hard to retract once published; worth confirming the actual replacement of ProjectCreationStateContext / useJourneyInstance / JourneyBackHandlerContext lands end-to-end before merging, in case the API shape needs another pass.

The cutover-ap branch was created specifically to validate this concern. Three checks:

Local stand-ins are deleted, not wrapped

  • ProjectCreationStateContext.tsx ‚ deleted
  • JourneyBackHandlerContext.tsx ‚ deleted
  • useJourneyInstance.ts ‚ deleted (140 lines: chain walk + subscription reconciliation)

Plus the previous* / default* plumbing on every step's input declaration and every transition handler's input stamp ‚is gone, replaced by buildInput factories declared on the entries themselves.

Each new API has a real consumer in the adoption commit

The API shape went through two rounds of adoption-driven revision

Exactly what the concern called for. Both rounds were prompted by running the adoption end-to-end and hitting rough edges:

Caveat worth naming

Single first consumer (autopilot's project-creation journey). A second consumer ‚ portal request creation, future integration-setup parent journey ‚ may surface shapes we didn't hit. That's the inherent limit of first-consumer validation; not a blocker, but worth tracking the second adoption when it happens.

@diogomiguel diogomiguel merged commit 1156d2f into main May 12, 2026
12 checks passed
@diogomiguel diogomiguel deleted the feat/journey-runtime-additions branch May 12, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants