@cat-factory/app 0.134.0 → 0.135.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ import { type MaybeRefOrGetter, toValue, watch } from 'vue'
2
+ import { useEnvironmentWizardStore } from '~/stores/environmentWizard'
3
+
4
+ /**
5
+ * Bridge a journey step's target frame (`ModuleEntryProps.input.frameId`) into
6
+ * the `environmentWizard` data store (slice 3 of the modular-vue adoption).
7
+ *
8
+ * The journey owns per-frame NAVIGATION (its state carries the resolved
9
+ * `frameId`), but the heavy per-step DATA lives in the singleton
10
+ * `environmentWizard` store, which holds exactly ONE frame at a time. Every step
11
+ * that READS that store therefore has to (re)assert the frame it was entered for
12
+ * — otherwise the store can be pointing at a DIFFERENT frame than the one the
13
+ * journey is on, and the step renders (or `save()` persists) the wrong frame's
14
+ * recipe. That desync is reachable within a single session: configure frame A up
15
+ * to the preflight step, open the wizard for frame B (its review step repoints
16
+ * the store to B), then reopen A and RESUME at preflight — a step that never
17
+ * re-bridged would show B's data under A's journey.
18
+ *
19
+ * `beginForFrame` is idempotent per frame (a no-op when the store already targets
20
+ * `frameId`), so calling this from every data step is free on the happy path and
21
+ * only re-seeds when the store actually drifted. `immediate` so the store is
22
+ * targeted before the step's first render reads it.
23
+ */
24
+ export function useEnvironmentWizardTarget(frameId: MaybeRefOrGetter<string | null>): void {
25
+ const store = useEnvironmentWizardStore()
26
+ watch(
27
+ () => toValue(frameId),
28
+ (id) => store.beginForFrame(id),
29
+ { immediate: true },
30
+ )
31
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ ENV_STEP_ORDER,
4
+ envInitialState,
5
+ envNextAfter,
6
+ envStartStep,
7
+ } from './environmentSetup.logic'
8
+
9
+ // Pure navigation graph for the environment-setup journey (slice 3). `envNextAfter`
10
+ // is the source of truth the journey's `advance` transitions derive their target
11
+ // entries from (see `environmentSetup.ts`), so pinning the order, start branch, and
12
+ // forward edges here means a wiring regression can't silently reorder or drop a step.
13
+ describe('environment-setup journey logic', () => {
14
+ it('orders the steps pick → review → preflight → save', () => {
15
+ expect(ENV_STEP_ORDER).toEqual(['pick', 'review', 'preflight', 'save'])
16
+ })
17
+
18
+ it('seeds state from the launch input', () => {
19
+ expect(envInitialState({ frameId: 'blk_1' })).toEqual({ frameId: 'blk_1' })
20
+ expect(envInitialState({ frameId: null })).toEqual({ frameId: null })
21
+ })
22
+
23
+ it('skips the picker when a frame was preselected, else starts at pick', () => {
24
+ expect(envStartStep({ frameId: 'blk_1' }, { frameId: 'blk_1' })).toBe('review')
25
+ expect(envStartStep({ frameId: null }, { frameId: null })).toBe('pick')
26
+ })
27
+
28
+ it('advances linearly and terminates after save', () => {
29
+ expect(envNextAfter('pick')).toBe('review')
30
+ expect(envNextAfter('review')).toBe('preflight')
31
+ expect(envNextAfter('preflight')).toBe('save')
32
+ expect(envNextAfter('save')).toBe('done')
33
+ })
34
+ })
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Pure navigation logic for the environment-setup journey (slice 3 of the
3
+ * modular-vue adoption — docs/initiatives/modular-vue-adoption.md).
4
+ *
5
+ * The journey owns only the WIZARD NAVIGATION — the ordered steps
6
+ * (pick → review → preflight → save), the forward transitions, and the
7
+ * back/rewind + resume that used to live as a hand-rolled `STEP_ORDER` + `step`
8
+ * ref + `goToStep` in `stores/environmentWizard.ts`. The heavy per-step DATA and
9
+ * async actions (detect / deep-analysis / recipe editing / preflight / save /
10
+ * trial) stay in that Pinia store, which the step components still drive; the
11
+ * journey just decides WHICH step shows and threads the target frame id.
12
+ *
13
+ * This file is deliberately free of Vue/`.vue`/`defineModule` imports so the
14
+ * transition graph is unit-tested directly (`environmentSetup.spec.ts`); the
15
+ * component wiring lives in `environmentSetup.ts`, which feeds these helpers into
16
+ * `defineJourney`'s `transitions` map.
17
+ */
18
+
19
+ /** The journey's ordered step entries (also the module's `entryPoints` keys). */
20
+ export const ENV_STEP_ORDER = ['pick', 'review', 'preflight', 'save'] as const
21
+ export type EnvStep = (typeof ENV_STEP_ORDER)[number]
22
+
23
+ /** The single module id every step entry lives under. */
24
+ export const ENV_MODULE_ID = 'cat-factory:environment-setup' as const
25
+
26
+ /** The picker's exit — carries the chosen frame into journey state. */
27
+ export const ENV_SELECT_EXIT = 'select'
28
+ /** Every other step's forward exit — void, just advances. */
29
+ export const ENV_ADVANCE_EXIT = 'advance'
30
+
31
+ /** Payload of the picker's `select` exit. */
32
+ export interface EnvSelectOutput {
33
+ frameId: string
34
+ }
35
+
36
+ /** The journey's serializable state — just the resolved target frame. Everything
37
+ * else is derived live in the `environmentWizard` store, so this stays tiny and
38
+ * cheap to persist/resume. */
39
+ export interface EnvSetupState {
40
+ frameId: string | null
41
+ }
42
+
43
+ /** The journey's start input — the frame the launcher preselected, or `null` to
44
+ * begin at the pick step. `keyFor` scopes resume to this frame. */
45
+ export interface EnvSetupInput {
46
+ frameId: string | null
47
+ }
48
+
49
+ /** Seed the journey state from the launch input. */
50
+ export function envInitialState(input: EnvSetupInput): EnvSetupState {
51
+ return { frameId: input.frameId }
52
+ }
53
+
54
+ /** First step: skip the picker when the launcher already chose a frame. */
55
+ export function envStartStep(_state: EnvSetupState, input: EnvSetupInput): EnvStep {
56
+ return input.frameId ? 'review' : 'pick'
57
+ }
58
+
59
+ /**
60
+ * The step reached by advancing from `from` via its forward exit, or `'done'`
61
+ * when the flow completes. The pick step advances via `select` (a distinct
62
+ * exit); the rest via `advance`. Pure and total over `EnvStep`.
63
+ *
64
+ * This is the SINGLE SOURCE OF TRUTH for the linear forward chain: the journey
65
+ * definition (`environmentSetup.ts`) derives its `advance` transitions' target
66
+ * entries from it, so pinning this function in `environmentSetup.logic.spec.ts`
67
+ * genuinely guards the wired graph against a silent reorder/drop. The overloads
68
+ * narrow the return to a concrete entry (or `'done'`) so a transition handler can
69
+ * feed the result straight into a `StepSpec` without a widened `EnvStep | 'done'`.
70
+ */
71
+ export function envNextAfter(from: 'pick'): 'review'
72
+ export function envNextAfter(from: 'review'): 'preflight'
73
+ export function envNextAfter(from: 'preflight'): 'save'
74
+ export function envNextAfter(from: 'save'): 'done'
75
+ export function envNextAfter(from: EnvStep): EnvStep | 'done' {
76
+ switch (from) {
77
+ case 'pick':
78
+ return 'review'
79
+ case 'review':
80
+ return 'preflight'
81
+ case 'preflight':
82
+ return 'save'
83
+ case 'save':
84
+ return 'done'
85
+ }
86
+ }
@@ -0,0 +1,143 @@
1
+ import { defineModule } from '@modular-vue/core'
2
+ import { defineExit } from '@modular-frontend/core'
3
+ import { defineJourney, defineJourneyHandle } from '@modular-vue/journeys'
4
+ import EnvPickStep from '~/components/environments/steps/EnvPickStep.vue'
5
+ import EnvReviewStep from '~/components/environments/steps/EnvReviewStep.vue'
6
+ import EnvPreflightStep from '~/components/environments/steps/EnvPreflightStep.vue'
7
+ import EnvSaveStep from '~/components/environments/steps/EnvSaveStep.vue'
8
+ import { catFactoryJourneyPersistence } from '~/modular/journeys/persistence'
9
+ import {
10
+ ENV_ADVANCE_EXIT,
11
+ ENV_MODULE_ID,
12
+ ENV_SELECT_EXIT,
13
+ type EnvSelectOutput,
14
+ type EnvSetupInput,
15
+ type EnvSetupState,
16
+ envInitialState,
17
+ envNextAfter,
18
+ envStartStep,
19
+ } from '~/modular/journeys/environmentSetup.logic'
20
+
21
+ /**
22
+ * The environment-setup journey — the slice-3 pilot of the modular-vue adoption
23
+ * (docs/initiatives/modular-vue-adoption.md). It replaces the wizard's hand-rolled
24
+ * `STEP_ORDER` + `step` ref + `goToStep` navigation (in `stores/environmentWizard.ts`)
25
+ * with a typed, back/rewind-capable, resumable journey; the per-step data + async
26
+ * actions stay in that Pinia store, driven by the step components below.
27
+ *
28
+ * Authoring notes (co-evolution): `defineModule` comes from the `@modular-vue/core`
29
+ * binding, but the exit helper `defineExit` is only exported from the neutral
30
+ * `@modular-frontend/core` engine (a direct dep) — the Vue binding doesn't re-export
31
+ * the entry/exit authoring helpers yet. That's a small binding-surface gap to close
32
+ * upstream (mirrors slice 2's remote-manifest re-export), not a blocker.
33
+ *
34
+ * Each step's `input` (the target frame) is threaded explicitly on the `next` step
35
+ * specs rather than via an entry `buildInput` factory: the frame is fixed once at the
36
+ * pick step and never changes mid-flow, so the snapshot input a transition places is
37
+ * always current — even on `preserve-state` back-navigation — which keeps the module
38
+ * type simple (no `buildInput`-optionality quirk) and the flow fully explicit.
39
+ *
40
+ * This file imports the step `.vue` components, so — like `result-views.ts` — it's
41
+ * registered from the client plugin, keeping the unit-tested `registry.ts` /
42
+ * `*.logic.ts` import graph free of SFCs.
43
+ */
44
+
45
+ /**
46
+ * The step module: one module, four `journey`-mounted entries (the wizard steps),
47
+ * and two exits — `select` (the picker's chosen frame) and `advance` (every other
48
+ * step's void forward). Each non-initial entry opts into `preserve-state` back so
49
+ * the host's Back control rewinds a step without discarding the operator's edits.
50
+ */
51
+ export const environmentSetupModule = defineModule({
52
+ id: ENV_MODULE_ID,
53
+ version: '1.0.0',
54
+ entryPoints: {
55
+ pick: { mountKinds: ['journey'] as const, component: EnvPickStep },
56
+ review: {
57
+ mountKinds: ['journey'] as const,
58
+ allowBack: 'preserve-state' as const,
59
+ component: EnvReviewStep,
60
+ },
61
+ preflight: {
62
+ mountKinds: ['journey'] as const,
63
+ allowBack: 'preserve-state' as const,
64
+ component: EnvPreflightStep,
65
+ },
66
+ save: {
67
+ mountKinds: ['journey'] as const,
68
+ allowBack: 'preserve-state' as const,
69
+ component: EnvSaveStep,
70
+ },
71
+ },
72
+ exitPoints: {
73
+ [ENV_SELECT_EXIT]: defineExit<EnvSelectOutput>(),
74
+ [ENV_ADVANCE_EXIT]: defineExit(),
75
+ },
76
+ })
77
+
78
+ type EnvModules = { [ENV_MODULE_ID]: typeof environmentSetupModule }
79
+
80
+ /**
81
+ * The journey definition. `start` skips the picker when a frame was preselected;
82
+ * transitions are the linear pick → review → preflight → save chain, terminating
83
+ * with `complete` off the save step's `advance`. Back-navigation is handled by the
84
+ * entries' `allowBack`, not explicit transitions.
85
+ */
86
+ export const environmentSetupJourney = defineJourney<EnvModules, EnvSetupState>()({
87
+ id: 'environment-setup',
88
+ version: '1.0.0',
89
+ initialState: (input: EnvSetupInput) => envInitialState(input),
90
+ start: (state, input: EnvSetupInput) =>
91
+ envStartStep(state, input) === 'review'
92
+ ? ({ module: ENV_MODULE_ID, entry: 'review', input: { frameId: state.frameId } } as const)
93
+ : ({ module: ENV_MODULE_ID, entry: 'pick', input: { frameId: state.frameId } } as const),
94
+ transitions: {
95
+ [ENV_MODULE_ID]: {
96
+ pick: {
97
+ [ENV_SELECT_EXIT]: (ctx) => ({
98
+ next: {
99
+ module: ENV_MODULE_ID,
100
+ entry: 'review',
101
+ input: { frameId: ctx.output.frameId },
102
+ } as const,
103
+ state: { frameId: ctx.output.frameId },
104
+ }),
105
+ },
106
+ review: {
107
+ [ENV_ADVANCE_EXIT]: (ctx) => ({
108
+ next: {
109
+ module: ENV_MODULE_ID,
110
+ entry: envNextAfter('review'),
111
+ input: { frameId: ctx.state.frameId },
112
+ } as const,
113
+ }),
114
+ },
115
+ preflight: {
116
+ [ENV_ADVANCE_EXIT]: (ctx) => ({
117
+ next: {
118
+ module: ENV_MODULE_ID,
119
+ entry: envNextAfter('preflight'),
120
+ input: { frameId: ctx.state.frameId },
121
+ } as const,
122
+ }),
123
+ },
124
+ // `save` advances to `envNextAfter('save') === 'done'`, i.e. journey completion.
125
+ save: {
126
+ [ENV_ADVANCE_EXIT]: () => ({ complete: undefined }),
127
+ },
128
+ },
129
+ },
130
+ })
131
+
132
+ /** Typed launch handle — callers `runtime.start(handle, { frameId })`. */
133
+ export const environmentSetupHandle = defineJourneyHandle(environmentSetupJourney)
134
+
135
+ /**
136
+ * Pinia-backed persistence keyed by the target frame, so reopening the wizard for
137
+ * the same frame RESUMES at the step it was left on (and a different frame is a
138
+ * fresh flow). A null frame (launched at the picker) is a single transient key.
139
+ */
140
+ export const environmentSetupPersistence = catFactoryJourneyPersistence<
141
+ EnvSetupInput,
142
+ EnvSetupState
143
+ >(({ journeyId, input }) => `${journeyId}:${input.frameId ?? 'new'}`)
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { SerializedJourney } from '@modular-vue/journeys'
3
+ import { catFactoryJourneyPersistence, useJourneyPersistenceStore } from './persistence'
4
+
5
+ // The Pinia-backed journey persistence adapter (slice 3). This is what makes a
6
+ // modal-hosted wizard RESUME on reopen: `keyFor` scopes the stored blob, `save`
7
+ // records it, `load` returns it (so `runtime.start()` rehydrates instead of
8
+ // minting fresh), and `remove` drops it on completion. A fresh Pinia is installed
9
+ // per test by `test/setup.ts`.
10
+
11
+ interface FrameInput {
12
+ frameId: string | null
13
+ }
14
+
15
+ type FrameState = { frameId: string | null }
16
+
17
+ function blob(step: string): SerializedJourney<FrameState> {
18
+ // Minimal plain-JSON blob — the adapter only stores/round-trips it (JSON clone),
19
+ // it never interprets the shape, so a representative object suffices.
20
+ return {
21
+ journeyId: 'environment-setup',
22
+ version: '1.0.0',
23
+ state: { frameId: 'blk_1' },
24
+ step,
25
+ } as unknown as SerializedJourney<FrameState>
26
+ }
27
+
28
+ describe('catFactoryJourneyPersistence', () => {
29
+ const persistence = catFactoryJourneyPersistence<FrameInput, { frameId: string | null }>(
30
+ ({ journeyId, input }) => `${journeyId}:${input.frameId ?? 'new'}`,
31
+ )
32
+
33
+ it('derives a deterministic, frame-scoped key', () => {
34
+ expect(
35
+ persistence.keyFor({ journeyId: 'environment-setup', input: { frameId: 'blk_1' } }),
36
+ ).toBe('environment-setup:blk_1')
37
+ expect(persistence.keyFor({ journeyId: 'environment-setup', input: { frameId: null } })).toBe(
38
+ 'environment-setup:new',
39
+ )
40
+ })
41
+
42
+ it('round-trips save → load and detaches the loaded blob from the store', () => {
43
+ const key = 'environment-setup:blk_1'
44
+ persistence.save(key, blob('review'))
45
+
46
+ // Persisted under that key in the Pinia store.
47
+ expect(useJourneyPersistenceStore().journeys[key]).toMatchObject({ step: 'review' })
48
+
49
+ const loaded = persistence.load(key)
50
+ expect(loaded).toMatchObject({ step: 'review', state: { frameId: 'blk_1' } })
51
+ // Cloned, not the live store reference (mutating the load can't corrupt state).
52
+ expect(loaded).not.toBe(useJourneyPersistenceStore().journeys[key])
53
+ })
54
+
55
+ it('returns null for an unknown key (a fresh start)', () => {
56
+ expect(persistence.load('environment-setup:never-saved')).toBeNull()
57
+ })
58
+
59
+ it('removes a blob on completion so the next start is fresh', () => {
60
+ const key = 'environment-setup:blk_9'
61
+ persistence.save(key, blob('save'))
62
+ expect(persistence.load(key)).not.toBeNull()
63
+
64
+ persistence.remove(key)
65
+ expect(persistence.load(key)).toBeNull()
66
+ expect(useJourneyPersistenceStore().journeys[key]).toBeUndefined()
67
+ })
68
+ })
@@ -0,0 +1,58 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { SerializedJourney } from '@modular-vue/journeys'
4
+ import { createPiniaJourneyPersistence } from '@modular-vue/journeys'
5
+
6
+ /**
7
+ * Pinia-backed journey persistence (slice 3 of the modular-vue adoption —
8
+ * docs/initiatives/modular-vue-adoption.md).
9
+ *
10
+ * A journey's `persistence` adapter is what makes `runtime.start()` mean
11
+ * RESUME: it probes `keyFor(input)` for an in-flight serialized instance and
12
+ * returns it instead of minting a fresh one, so a modal-hosted wizard that is
13
+ * closed and reopened for the same target picks up exactly where it left off
14
+ * (see `JourneyHost`'s start-means-resume contract).
15
+ *
16
+ * We route that through Pinia (via the upstream `createPiniaJourneyPersistence`,
17
+ * the Vue-ecosystem analogue of `createWebStoragePersistence`) rather than a
18
+ * bespoke store: in-flight journeys then participate in the app's existing Pinia
19
+ * devtools/timeline, and one `store.$reset()` drops every persisted journey
20
+ * through a path the app already owns. The binding takes NO `pinia` dependency —
21
+ * the caller (this file) brings the store.
22
+ *
23
+ * Scope is deliberately in-memory / session-only: it survives a modal
24
+ * close→reopen within a session (the demonstrable resume win) but is not wired
25
+ * through `pinia-plugin-persistedstate`, so a full page reload starts fresh.
26
+ * Reload recovery is a later opt-in — a wizard's serialized blob can reference
27
+ * transient run ids that a cross-reload resume would need to re-validate.
28
+ */
29
+
30
+ /**
31
+ * The single store that owns every wizard journey's serialized blob, keyed by
32
+ * the per-journey `keyFor`. `SerializedJourney` is plain JSON by construction,
33
+ * so the record is trivially (de)serializable.
34
+ */
35
+ export const useJourneyPersistenceStore = defineStore('journeyPersistence', () => {
36
+ const journeys = ref<Record<string, SerializedJourney>>({})
37
+ return { journeys }
38
+ })
39
+
40
+ /**
41
+ * Build a Pinia-backed `JourneyPersistence` for a journey, keyed by `keyFor`.
42
+ * Pass the result as a journey registration's `persistence` option. The `store`
43
+ * is a lazy getter so the adapter can be constructed at module-eval time (before
44
+ * any Pinia scope exists) and only resolves the store when the runtime actually
45
+ * loads/saves — inside the client plugin, where Pinia is active.
46
+ *
47
+ * `keyFor` MUST be deterministic for identical input (it's the resume probe
48
+ * key); scope it to the wizard's target (e.g. the service frame id) so reopening
49
+ * the same target resumes and a different target is a fresh flow.
50
+ */
51
+ export function catFactoryJourneyPersistence<TInput, TState>(
52
+ keyFor: (ctx: { journeyId: string; input: TInput }) => string,
53
+ ) {
54
+ return createPiniaJourneyPersistence<TInput, TState>({
55
+ keyFor,
56
+ store: () => useJourneyPersistenceStore(),
57
+ })
58
+ }
@@ -1,6 +1,6 @@
1
1
  import type { AnyModuleDescriptor } from '@modular-vue/core'
2
2
  import { createRegistry } from '@modular-vue/runtime'
3
- import type { ModuleRegistry } from '@modular-vue/runtime'
3
+ import { journeysPlugin } from '@modular-vue/journeys'
4
4
  import { navigationModule } from '~/modular/nav-contributions'
5
5
  import type { NavGates } from '~/modular/nav-contributions'
6
6
  import type { AppSlots } from '~/modular/slots'
@@ -92,11 +92,17 @@ export function __resetConsumerModulesForTest(): void {
92
92
  export function createAppRegistry(
93
93
  deps: AppDeps,
94
94
  extraModules: readonly AnyModuleDescriptor[] = [],
95
- ): ModuleRegistry<AppDeps, AppSlots> {
95
+ ) {
96
+ // `.use(journeysPlugin())` attaches the journeys extension (slice 3): the returned
97
+ // registry gains `registerJourney(...)` and the resolved manifest exposes the
98
+ // `JourneyRuntime` at `extensions.journeys`, which the client plugin provides to the
99
+ // Vue app. The plugin itself carries no `.vue` imports, so it's safe in this
100
+ // unit-tested import graph; the journeys + their step modules (which DO import SFCs)
101
+ // are registered from the client plugin via `extraModules` + `registerJourney`.
96
102
  const registry = createRegistry<AppDeps, AppSlots>({
97
103
  services: { gates: deps.gates },
98
104
  slots: { nav: [], resultViews: [], agentKinds: [] },
99
- })
105
+ }).use(journeysPlugin())
100
106
  for (const mod of [...FIRST_PARTY_MODULES, ...extraModules, ...consumerModules]) {
101
107
  registry.register(mod)
102
108
  }
@@ -1,9 +1,18 @@
1
1
  import { installModularApp } from '@modular-vue/nuxt/runtime'
2
+ import type { ApplicationManifest } from '@modular-vue/runtime'
3
+ import type { NavigationItem } from '@modular-frontend/core'
2
4
  import { resolveComponentRegistry } from '@modular-vue/core'
5
+ import { provideJourneyRuntime } from '@modular-vue/journeys'
6
+ import type { JourneyRuntime } from '@modular-vue/journeys'
3
7
  import { createAppRegistry } from '~/modular/registry'
4
8
  import { navSlotFilter } from '~/modular/nav-contributions'
5
9
  import { createNavGates } from '~/modular/nav-gates'
6
10
  import { resultViewsModule } from '~/modular/result-views'
11
+ import {
12
+ environmentSetupJourney,
13
+ environmentSetupModule,
14
+ environmentSetupPersistence,
15
+ } from '~/modular/journeys/environmentSetup'
7
16
  import type { AppSlots, ResultViewContribution } from '~/modular/slots'
8
17
  import type { CustomAgentKind } from '~/types/domain'
9
18
 
@@ -43,11 +52,35 @@ export default defineNuxtPlugin({
43
52
  name: 'cat-factory:modular',
44
53
  enforce: 'post',
45
54
  setup(nuxtApp) {
46
- const registry = createAppRegistry({ gates: createNavGates() }, [resultViewsModule])
47
- const manifest = installModularApp({ vueApp: nuxtApp.vueApp, $router: useRouter() }, registry, {
48
- slotFilter: navSlotFilter,
55
+ // Register the step-module carriers (they import `.vue`, so they enter via
56
+ // `extraModules`, keeping the unit-tested `registry.ts` graph SFC-free) and the
57
+ // journeys themselves (slice 3). `registerJourney` must run BEFORE the manifest
58
+ // resolves (inside `installModularApp`).
59
+ const registry = createAppRegistry({ gates: createNavGates() }, [
60
+ resultViewsModule,
61
+ environmentSetupModule,
62
+ ])
63
+ registry.registerJourney(environmentSetupJourney, {
64
+ persistence: environmentSetupPersistence,
49
65
  })
50
- const slots = manifest.slots as AppSlots
66
+ // The annotation is still required to break `defineNuxtPlugin`'s self-referential
67
+ // return inference (the plugin provides `modular: manifest`, so an un-annotated
68
+ // `manifest` resolves to `any` — TS7022). But since `@modular-vue/nuxt@0.3.0` now
69
+ // FLOWS the registry's plugin-extension type through `installModularApp`, the
70
+ // annotation can name the real `{ journeys: JourneyRuntime }` extension and TS
71
+ // VERIFIES the `journeysPlugin` actually resolved it (pre-0.3.0 erased `TExtensions`
72
+ // to `unknown`, so the extension had to be recovered with an unchecked
73
+ // `as JourneyRuntime` cast). `manifest.journeys` / `manifest.slots` are now typed,
74
+ // so the downstream casts are gone too.
75
+ const manifest: ApplicationManifest<AppSlots, NavigationItem, { journeys: JourneyRuntime }> =
76
+ installModularApp({ vueApp: nuxtApp.vueApp, $router: useRouter() }, registry, {
77
+ slotFilter: navSlotFilter,
78
+ })
79
+ // Provide the resolved `JourneyRuntime` to the Vue app so `<JourneyProvider>` /
80
+ // `<JourneyHost>` / `<JourneyOutlet>` resolve it from context (the wizard hosts
81
+ // don't hand-thread a runtime).
82
+ provideJourneyRuntime(nuxtApp.vueApp, manifest.journeys)
83
+ const slots = manifest.slots
51
84
  // Fail FAST on a result-view wiring bug (a duplicate id across the first-party +
52
85
  // consumer `resultViews` modules) at BOOT rather than lazily the first time a result
53
86
  // window opens: resolve the merged slot once here. `resolveComponentRegistry` throws on
@@ -23,7 +23,7 @@ import { usePreflightsStore } from '~/stores/preflights'
23
23
  import { useServicesStore } from '~/stores/services'
24
24
  import { useWorkspaceStore } from '~/stores/workspace'
25
25
 
26
- // The environment setup wizard's cross-step state + actions (shared-stacks slice 7). It walks the
26
+ // The environment setup wizard's cross-step DATA + actions (shared-stacks slice 7). It backs the
27
27
  // guided flow — pick a service frame → review the recommended `docker-compose` recipe (detector
28
28
  // facts + the opt-in analyst draft, merged with provenance) → run the machine preflights → save
29
29
  // (persist the recipe on the frame AND register the workspace's docker-compose handler so the
@@ -31,17 +31,19 @@ import { useWorkspaceStore } from '~/stores/workspace'
31
31
  //
32
32
  // The detector + analyst only RECOMMEND; the human confirms/edits the working `recipe` here and the
33
33
  // compose provider keys purely on the saved recipe (the build-flag rule). Mirrors the other infra
34
- // stores' idiom; the flow state is a singleton so the wizard modal + its step children share it.
34
+ // stores' idiom; the flow state is a singleton so the wizard's step children share it.
35
+ //
36
+ // Since slice 3 of the modular-vue adoption (docs/initiatives/modular-vue-adoption.md) the wizard's
37
+ // step NAVIGATION lives in a modular-vue journey (`app/modular/journeys/environmentSetup.ts`), NOT
38
+ // here — this store no longer holds a `step` / `STEP_ORDER` / `goToStep`. It is purely the per-frame
39
+ // data+action layer the journey's step components drive; `beginForFrame` seeds it when a step first
40
+ // targets a frame.
35
41
 
36
42
  /** The seeded analyst-only pipeline the "run deep analysis" trigger starts against the frame. */
37
43
  const ANALYSIS_PIPELINE_ID = 'pl_environment_analysis'
38
44
  /** The analyst agent kind whose `result.custom` carries the drafted recipe. */
39
45
  const ANALYST_AGENT_KIND = 'environment-analyst'
40
46
 
41
- /** The wizard's ordered steps. `trial` is an optional post-save action, not a gate. */
42
- export type EnvWizardStep = 'pick' | 'review' | 'preflight' | 'save'
43
- export const ENV_WIZARD_STEPS: EnvWizardStep[] = ['pick', 'review', 'preflight', 'save']
44
-
45
47
  /** The analyst run's lifecycle as the wizard surfaces it. */
46
48
  export type AnalysisStatus = 'idle' | 'running' | 'ready' | 'failed'
47
49
 
@@ -70,9 +72,11 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
70
72
  const pipelines = usePipelinesStore()
71
73
  const preflights = usePreflightsStore()
72
74
 
73
- // ---- Flow position ------------------------------------------------------
75
+ // ---- Target frame -------------------------------------------------------
76
+ // The frame the flow currently targets. Set by `beginForFrame` when a journey
77
+ // step first mounts against a frame; the journey (not this store) owns which
78
+ // step is showing.
74
79
  const frameId = ref<string | null>(null)
75
- const step = ref<EnvWizardStep>('pick')
76
80
 
77
81
  // ---- Detection ----------------------------------------------------------
78
82
  const detecting = ref(false)
@@ -212,19 +216,18 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
212
216
  trialStarted.value = false
213
217
  }
214
218
 
215
- /** Reset the flow for a (possibly preselected) frame. */
216
- function open(preselectFrameId: string | null) {
217
- frameId.value = preselectFrameId
218
- step.value = preselectFrameId ? 'review' : 'pick'
219
- resetFlowState()
220
- if (preselectFrameId) void detect()
221
- }
222
-
223
- function selectFrame(id: string) {
219
+ /**
220
+ * Seed the data layer for a frame the journey's review step is entering. The
221
+ * journey owns navigation, so this is idempotent by frame: it (re)seeds +
222
+ * detects only when the target frame actually changes, so back-navigating to
223
+ * the review step (or a resume) does NOT clobber the operator's in-progress
224
+ * recipe edits. Selecting a different frame resets the flow for it.
225
+ */
226
+ function beginForFrame(id: string | null) {
227
+ if (frameId.value === id) return
224
228
  frameId.value = id
225
229
  resetFlowState()
226
- step.value = 'review'
227
- void detect()
230
+ if (id) void detect()
228
231
  }
229
232
 
230
233
  /** Re-seed the working recipe from the current merge (detector-only, or +analyst after apply). */
@@ -435,14 +438,9 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
435
438
  }
436
439
  }
437
440
 
438
- function goToStep(next: EnvWizardStep) {
439
- step.value = next
440
- }
441
-
442
441
  return {
443
442
  // state
444
443
  frameId,
445
- step,
446
444
  detecting,
447
445
  detectError,
448
446
  recommendation,
@@ -472,8 +470,7 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
472
470
  analysisStatus,
473
471
  merged,
474
472
  // actions
475
- open,
476
- selectFrame,
473
+ beginForFrame,
477
474
  detect,
478
475
  startAnalysis,
479
476
  applyAnalystDraft,
@@ -484,6 +481,5 @@ export const useEnvironmentWizardStore = defineStore('environmentWizard', () =>
484
481
  runPreflight,
485
482
  save,
486
483
  trialProvision,
487
- goToStep,
488
484
  }
489
485
  })