@funnelsgrove/cli 0.1.11 → 0.1.12
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.
- package/README.md +22 -2
- package/dist/cli.d.ts +18 -1
- package/dist/cli.js +37 -5
- package/dist/localSync.d.ts +1 -0
- package/dist/localSync.js +4 -0
- package/package.json +1 -1
- package/template_docs/AGENTS.md +21 -7
- package/template_docs/docs/ab-experiments.md +3 -2
- package/template_docs/docs/analytics.md +7 -4
- package/template_docs/docs/editing-flow.md +40 -3
- package/template_docs/docs/editing-step.md +1 -1
- package/template_docs/docs/publishing-and-versioning.md +56 -3
- package/template_docs/docs/qa-checklist.md +9 -2
- package/template_docs/docs/step-ui-guidelines.md +11 -7
package/README.md
CHANGED
|
@@ -18,13 +18,27 @@ Common workflow:
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
fgrove status
|
|
21
|
+
git status --short
|
|
22
|
+
fgrove github status
|
|
21
23
|
fgrove docs
|
|
24
|
+
# if GitHub is connected: git push, then fgrove github pull
|
|
25
|
+
# if GitHub is not connected:
|
|
22
26
|
fgrove sync up --message 'Update funnel copy'
|
|
23
27
|
fgrove publish --env preview
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
Inside a synced folder, `fgrove` reads `.funnelsgrove-sync.json` first, so you do not need to run `fgrove use` when switching between local funnel directories. Use `fgrove use` only when you want a global fallback context for commands outside a synced folder.
|
|
27
31
|
|
|
32
|
+
`fgrove sync down` refuses to overwrite local changes in an existing synced
|
|
33
|
+
folder unless `--force` is passed. If `fgrove sync up` says the remote draft
|
|
34
|
+
changed since your local sync, download the latest draft into a clean temporary
|
|
35
|
+
directory, merge your local changes, rerun checks, and sync again.
|
|
36
|
+
|
|
37
|
+
`fgrove sync up` is for funnels without GitHub source sync. When GitHub is
|
|
38
|
+
connected, commit and push source changes with normal git, then run `fgrove
|
|
39
|
+
github pull` to sync GitHub into the hosted draft. Do not run `fgrove sync up`
|
|
40
|
+
for the same local diff.
|
|
41
|
+
|
|
28
42
|
Use `fgrove env pull` from a synced folder to refresh only the ignored local `.env` file after project settings change, without replacing source files.
|
|
29
43
|
|
|
30
44
|
GitHub sync workflow:
|
|
@@ -32,12 +46,18 @@ GitHub sync workflow:
|
|
|
32
46
|
```bash
|
|
33
47
|
fgrove github status --funnel claimbee-general
|
|
34
48
|
fgrove github connect --funnel claimbee-general --account The-Solid-Grove --repo claimbee-funnel
|
|
35
|
-
|
|
49
|
+
git push
|
|
36
50
|
fgrove github pull --funnel claimbee-general
|
|
37
51
|
fgrove publish --funnel claimbee-general --env preview
|
|
38
52
|
```
|
|
39
53
|
|
|
40
|
-
The GitHub commands use the FunnelsGrove API only.
|
|
54
|
+
The GitHub commands use the FunnelsGrove API only. `fgrove github pull` pulls
|
|
55
|
+
the repository into the hosted draft after you push normal git commits.
|
|
56
|
+
`fgrove publish` waits for the current draft to reach GitHub before publishing.
|
|
57
|
+
`fgrove github push` still exists for explicit hosted-draft-to-GitHub recovery
|
|
58
|
+
work, but it is not the normal path for local source changes. Local `.env*`
|
|
59
|
+
files remain CLI-local runtime material from `sync down`; they are not sent to
|
|
60
|
+
GitHub sync.
|
|
41
61
|
|
|
42
62
|
Analytics workflow:
|
|
43
63
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type ActiveContext } from './authStore.js';
|
|
3
|
-
import { type SyncManifest } from './localSync.js';
|
|
3
|
+
import { type SourceFile, type SyncManifest } from './localSync.js';
|
|
4
|
+
import { type GitHubStatusResponse } from './githubOutput.js';
|
|
4
5
|
import { type AnalyticsOutputFormat } from './analyticsOutput.js';
|
|
5
6
|
type AnalyticsCommandInput = {
|
|
6
7
|
date: string;
|
|
@@ -26,4 +27,20 @@ export declare function buildAnalyticsCommandInput(options: {
|
|
|
26
27
|
format?: string;
|
|
27
28
|
timezone?: string;
|
|
28
29
|
}): AnalyticsCommandInput;
|
|
30
|
+
export declare function buildPatchSourceInput(input: {
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
funnelId: string;
|
|
33
|
+
message?: string;
|
|
34
|
+
baseDraftVersionId?: string;
|
|
35
|
+
files: SourceFile[];
|
|
36
|
+
deletedPaths: string[];
|
|
37
|
+
}): {
|
|
38
|
+
workspaceId: string;
|
|
39
|
+
funnelId: string;
|
|
40
|
+
message?: string;
|
|
41
|
+
baseDraftVersionId?: string;
|
|
42
|
+
files: SourceFile[];
|
|
43
|
+
deletedPaths: string[];
|
|
44
|
+
};
|
|
45
|
+
export declare function assertCanDraftSyncSource(status: GitHubStatusResponse): void;
|
|
29
46
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { callTrpcProcedure } from './apiClient.js';
|
|
9
9
|
import { clearActiveContext, getDefaultAuthConfigPath, loadActiveContext, loadAuthToken, saveActiveContext, saveAuthToken, } from './authStore.js';
|
|
10
|
-
import { buildSyncManifest, chunkChangedSourceFiles, collectChangedSourceFiles, collectSourceFiles, ensureGitignore, formatSyncUploadSummary, readSyncManifest, writeLocalEnvFile, writeSourceFiles, writeSyncManifest, } from './localSync.js';
|
|
10
|
+
import { buildSyncManifest, chunkChangedSourceFiles, collectChangedSourceFiles, collectSourceFiles, ensureGitignore, formatSyncUploadSummary, hasLocalSourceChanges, readSyncManifest, writeLocalEnvFile, writeSourceFiles, writeSyncManifest, } from './localSync.js';
|
|
11
11
|
import { pullEnvFile } from './envSync.js';
|
|
12
12
|
import { formatGitHubConnectRows, formatGitHubJobRows, formatGitHubStatusRows, } from './githubOutput.js';
|
|
13
13
|
import { syncGitHubDraftIfConnected } from './githubSyncFlow.js';
|
|
@@ -73,6 +73,21 @@ export function buildAnalyticsCommandInput(options) {
|
|
|
73
73
|
timezone,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
export function buildPatchSourceInput(input) {
|
|
77
|
+
return {
|
|
78
|
+
workspaceId: input.workspaceId,
|
|
79
|
+
funnelId: input.funnelId,
|
|
80
|
+
message: input.message,
|
|
81
|
+
baseDraftVersionId: input.baseDraftVersionId,
|
|
82
|
+
files: input.files,
|
|
83
|
+
deletedPaths: input.deletedPaths,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function assertCanDraftSyncSource(status) {
|
|
87
|
+
if (status.connection && status.connection.status !== 'disconnected') {
|
|
88
|
+
throw new Error('This funnel is connected to GitHub. Commit and push changes with git, then run `fgrove github pull` to sync the hosted draft. Do not use `fgrove sync up` for the same change.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
76
91
|
const getApiUrl = () => {
|
|
77
92
|
const options = program.opts();
|
|
78
93
|
return options.apiUrl || process.env.FUNNELSGROVE_API_URL || DEFAULT_API_URL;
|
|
@@ -753,7 +768,8 @@ addExamples(syncCommand
|
|
|
753
768
|
.description('Download funnel draft source to a local directory')
|
|
754
769
|
.option('--workspace <id-or-slug-or-name>', 'Workspace id, slug, or name')
|
|
755
770
|
.option('--funnel <id-or-slug>', 'Funnel id or slug')
|
|
756
|
-
.requiredOption('--dir <path>', 'Local target directory')
|
|
771
|
+
.requiredOption('--dir <path>', 'Local target directory')
|
|
772
|
+
.option('--force', 'Overwrite local changes in an existing synced directory'), [
|
|
757
773
|
'fgrove sync down --funnel claimbee-ios --dir ./claimbee-ios',
|
|
758
774
|
])
|
|
759
775
|
.action(async (options) => {
|
|
@@ -764,6 +780,9 @@ addExamples(syncCommand
|
|
|
764
780
|
funnel: options.funnel,
|
|
765
781
|
dir: options.dir,
|
|
766
782
|
});
|
|
783
|
+
if (!options.force && target.manifest && await hasLocalSourceChanges(target.sourceDir, target.manifest)) {
|
|
784
|
+
throw new Error('Local sync directory has unuploaded changes. Commit, stash, sync up, or merge them before `fgrove sync down`; pass `--force` only if overwriting local changes is intentional.');
|
|
785
|
+
}
|
|
767
786
|
const result = await callApi({
|
|
768
787
|
path: 'funnels.exportSource',
|
|
769
788
|
type: 'query',
|
|
@@ -820,7 +839,7 @@ addExamples(envCommand
|
|
|
820
839
|
});
|
|
821
840
|
addExamples(syncCommand
|
|
822
841
|
.command('up')
|
|
823
|
-
.description('Upload local source into a new funnel draft version')
|
|
842
|
+
.description('Upload local source into a new funnel draft version for funnels without GitHub source sync')
|
|
824
843
|
.option('--workspace <id-or-slug-or-name>', 'Workspace id, slug, or name')
|
|
825
844
|
.option('--funnel <id-or-slug>', 'Funnel id or slug')
|
|
826
845
|
.option('--dir <path>', 'Local source directory', '.')
|
|
@@ -836,6 +855,16 @@ addExamples(syncCommand
|
|
|
836
855
|
funnel: options.funnel,
|
|
837
856
|
dir: options.dir,
|
|
838
857
|
});
|
|
858
|
+
const githubStatus = await callApi({
|
|
859
|
+
path: 'github.status',
|
|
860
|
+
type: 'query',
|
|
861
|
+
token,
|
|
862
|
+
data: {
|
|
863
|
+
workspaceId: target.workspaceId,
|
|
864
|
+
funnelId: target.funnelId,
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
assertCanDraftSyncSource(githubStatus);
|
|
839
868
|
const changes = target.manifest
|
|
840
869
|
? await collectChangedSourceFiles(target.sourceDir, target.manifest)
|
|
841
870
|
: null;
|
|
@@ -854,19 +883,22 @@ addExamples(syncCommand
|
|
|
854
883
|
if (changes) {
|
|
855
884
|
console.log(formatSyncUploadSummary(changes).join('\n'));
|
|
856
885
|
const batches = chunkChangedSourceFiles(changes);
|
|
886
|
+
let baseDraftVersionId = changes.currentManifest.draftVersionId;
|
|
857
887
|
for (const batch of batches) {
|
|
858
888
|
result = await callApi({
|
|
859
889
|
path: 'funnels.patchSource',
|
|
860
890
|
type: 'mutation',
|
|
861
891
|
token,
|
|
862
|
-
data: {
|
|
892
|
+
data: buildPatchSourceInput({
|
|
863
893
|
workspaceId: target.workspaceId,
|
|
864
894
|
funnelId: target.funnelId,
|
|
865
895
|
message: options.message,
|
|
896
|
+
baseDraftVersionId,
|
|
866
897
|
files: batch.files,
|
|
867
898
|
deletedPaths: batch.deletedPaths,
|
|
868
|
-
},
|
|
899
|
+
}),
|
|
869
900
|
});
|
|
901
|
+
baseDraftVersionId = result.versionId;
|
|
870
902
|
syncedFileCount += result.syncedFiles.length;
|
|
871
903
|
deletedFileCount += result.deletedFiles?.length || 0;
|
|
872
904
|
}
|
package/dist/localSync.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare function writeLocalEnvFile(rootDir: string, envFile: string | nul
|
|
|
35
35
|
export declare function readSyncManifest(rootDir: string): Promise<SyncManifest | null>;
|
|
36
36
|
export declare function collectSourceFiles(rootDir: string): Promise<SourceFile[]>;
|
|
37
37
|
export declare function collectChangedSourceFiles(rootDir: string, previousManifest: SyncManifest): Promise<ChangedSourceFiles>;
|
|
38
|
+
export declare function hasLocalSourceChanges(rootDir: string, previousManifest: SyncManifest): Promise<boolean>;
|
|
38
39
|
export declare function chunkChangedSourceFiles(changes: Pick<ChangedSourceFiles, 'deletedPaths' | 'files'>, maxContentChars?: number): SourceFilePatchBatch[];
|
|
39
40
|
export declare function formatSyncUploadSummary(changes: Pick<ChangedSourceFiles, 'deletedPaths' | 'files'>, largestFileLimit?: number): string[];
|
|
40
41
|
export declare function writeSourceFiles(rootDir: string, files: SourceFile[]): Promise<void>;
|
package/dist/localSync.js
CHANGED
|
@@ -127,6 +127,10 @@ export async function collectChangedSourceFiles(rootDir, previousManifest) {
|
|
|
127
127
|
files: await readSourceFiles(rootDir, changedManifestFiles),
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
export async function hasLocalSourceChanges(rootDir, previousManifest) {
|
|
131
|
+
const changes = await collectChangedSourceFiles(rootDir, previousManifest);
|
|
132
|
+
return changes.files.length > 0 || changes.deletedPaths.length > 0;
|
|
133
|
+
}
|
|
130
134
|
export function chunkChangedSourceFiles(changes, maxContentChars = DEFAULT_PATCH_BATCH_CONTENT_CHARS) {
|
|
131
135
|
const batches = [];
|
|
132
136
|
let currentBatch = {
|
package/package.json
CHANGED
package/template_docs/AGENTS.md
CHANGED
|
@@ -33,23 +33,37 @@ Most edits touch one step file plus its matching `content/` and `editor/` files,
|
|
|
33
33
|
## Standard Workflow
|
|
34
34
|
|
|
35
35
|
1. `fgrove status` — confirm the active project and funnel.
|
|
36
|
-
2.
|
|
37
|
-
3. `
|
|
38
|
-
4. `
|
|
39
|
-
5.
|
|
40
|
-
6.
|
|
41
|
-
7.
|
|
36
|
+
2. `git status --short` — if local changes exist, checkpoint them before any refresh.
|
|
37
|
+
3. `fgrove github status` — when GitHub is connected and remote is ahead, run `fgrove github pull`, then poll `fgrove github status` until the pull job is completed or skipped before syncing the latest draft into a clean directory and merging the local checkpoint intentionally.
|
|
38
|
+
4. When GitHub is not connected, use `fgrove sync down --funnel <id-or-slug> --dir <temp-dir>` or an already selected `fgrove use` context as the remote source, then merge that clean hosted draft with local changes before editing further.
|
|
39
|
+
5. Make the scoped edit per the topic doc.
|
|
40
|
+
6. `npm run test:run && npm run lint` (or the checks this tree defines).
|
|
41
|
+
7. `npm run dev` — verify in local preview at all four default breakpoints: small 375x667, medium 393x852, large 402x874, and desktop-small 1280x800 ([docs/step-ui-guidelines.md](docs/step-ui-guidelines.md)).
|
|
42
|
+
8. Run the relevant part of [docs/qa-checklist.md](docs/qa-checklist.md).
|
|
43
|
+
9. Ask the user before publishing. If GitHub is connected, commit and push with normal git, run `fgrove github pull`, then poll `fgrove github status` until the pull job is completed or skipped. Do not also run `fgrove sync up` for the same diff.
|
|
44
|
+
10. If GitHub is not connected, run `fgrove sync up --message '<summary>'`.
|
|
45
|
+
11. Publish preview with `fgrove publish --env preview --message '<summary>'`.
|
|
46
|
+
12. QA the preview URL. Production publish only on explicit request, after preview QA.
|
|
42
47
|
|
|
43
48
|
`fgrove env pull` refreshes the local `.env` when remote project settings changed.
|
|
49
|
+
Do not run `fgrove sync down` over a dirty synced directory unless discarding
|
|
50
|
+
local changes is intentional and `--force` is passed. If `fgrove sync up`
|
|
51
|
+
reports that the remote draft changed since this directory was synced, download
|
|
52
|
+
the current draft into a temp directory, merge local changes, rerun checks, and
|
|
53
|
+
sync again.
|
|
54
|
+
For GitHub-connected funnels, source changes must flow through GitHub first:
|
|
55
|
+
`git push`, then `fgrove github pull`, then publish. Do not mix `fgrove sync up`
|
|
56
|
+
with the same local source diff.
|
|
44
57
|
|
|
45
58
|
## Behavioral Rules
|
|
46
59
|
|
|
47
60
|
1. **Think before coding.** State assumptions. If multiple interpretations exist, present them — don't pick silently. If something is unclear, ask before editing.
|
|
48
61
|
2. **Simplicity first.** Minimum code that solves the problem. No speculative abstractions, options, or error handling for impossible states.
|
|
49
62
|
3. **Surgical changes.** Touch only what the request requires. Match existing style. Remove only orphans your own change created. Every changed line should trace to the request.
|
|
50
|
-
4. **Goal-driven execution.** Define the success check before editing ("step renders at
|
|
63
|
+
4. **Goal-driven execution.** Define the success check before editing ("step renders at all default breakpoints and Continue advances to step-X"), then loop until it passes.
|
|
51
64
|
5. **Image performance locked.** Keep build-time raster compression plus AVIF/WebP variants enabled. For funnel step images, use `funnelManifest.assets` + step `assetIds`, priority/preload only for first-viewport images, and low-priority next-step warming from the shell. Do not preload the whole funnel image set.
|
|
52
65
|
6. **Meaningful URLs.** New step `path` values are public product routes, so use readable slugs like `/motivation`, `/fitness-goal`, or `/email-capture`. Sequential ids and `step-NN-*` filenames are okay for ordering, but do not create public routes like `/step-1`.
|
|
66
|
+
7. **Flow labeling hygiene.** When changing `edgesByStepId`, keep manifest `branches` current for conditional paths that own steps before reconverging. Keep builder metadata ClaimBee-style and source-readable: `edgesByStepId` keys/targets and `branches: [...]` must use inline string literals, not `someStep.id` variables or a `branches: flowBranches` indirection. Each branch needs a readable `name`, answer-derived `label` such as `yes-branch`, useful `tags`, and the owned `stepIds`. Running experiment variants need labels/tags like `paywall-test-control` and `paywall-test-variant-b`; stopped A/B variants or other inactive screens should keep stable tags and remain unreachable from the default flow so builder marks them `unused`.
|
|
53
67
|
|
|
54
68
|
Architecture docs are part of the change: if you change routing, runtime state, SDK contracts, URL handoff parameters, checkout behavior, analytics events, or shared package boundaries, update the matching doc in the same change.
|
|
55
69
|
|
|
@@ -14,8 +14,8 @@ export const experiments = defineFunnelExperiments([
|
|
|
14
14
|
type: 'paywall', // 'step' for quiz steps, 'paywall' for paywall tests
|
|
15
15
|
status: 'running', // 'paused' | 'stopped' removes it from the manifest
|
|
16
16
|
launchDate: '2026-06-12T00:00:00.000Z',
|
|
17
|
-
control: { stepId: 'paywall', trafficPercent: 50 },
|
|
18
|
-
variant: { stepId: 'paywall-b', trafficPercent: 50 },
|
|
17
|
+
control: { stepId: 'paywall', label: 'paywall-ab-control', trafficPercent: 50 },
|
|
18
|
+
variant: { stepId: 'paywall-b', label: 'paywall-ab-variant-b', trafficPercent: 50 },
|
|
19
19
|
},
|
|
20
20
|
] as const);
|
|
21
21
|
```
|
|
@@ -46,6 +46,7 @@ Check: opening the source URL promotes to the assigned route, each variant rende
|
|
|
46
46
|
|
|
47
47
|
- One deliberate change per experiment. Don't bundle unrelated edits into a variant.
|
|
48
48
|
- `control` stays stable; never edit the control step as part of launching a variant.
|
|
49
|
+
- Add stable `control.label` and `variant.label` values in the `<experiment-id>-control` / `<experiment-id>-variant-b` format so builder labels and analytics views stay readable.
|
|
49
50
|
- Variant keys and experiment ids are durable — they flow into analytics. Never recycle an id for a different hypothesis.
|
|
50
51
|
- Keep every variant step's outgoing edge in `edgesByStepId`, or assigned visitors strand.
|
|
51
52
|
- If a paywall variant changes plans or pricing, follow [payment-plans-and-discounts.md](payment-plans-and-discounts.md) for the plan/discount sync rules and QA both variants' checkout ([qa-checklist.md](qa-checklist.md)).
|
|
@@ -18,15 +18,15 @@ Analytics should describe meaningful user behavior. `@funnelsgrove/analytics` is
|
|
|
18
18
|
|
|
19
19
|
Experiment assignments are attached as feature flag properties when available. Preview runtime skips normal analytics delivery.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Standard Checkout Events
|
|
22
22
|
|
|
23
|
-
Use
|
|
23
|
+
Use named helpers from `@funnelsgrove/analytics` for checkout lifecycle events:
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
publicAnalyticsSdk.
|
|
27
|
-
eventType: 'checkout_started',
|
|
26
|
+
publicAnalyticsSdk.trackCheckoutStarted({
|
|
28
27
|
stepId: stepPaywallId,
|
|
29
28
|
stepName: stepPaywall.name || stepPaywall.title,
|
|
29
|
+
stepType: stepPaywall.type,
|
|
30
30
|
metadata: {
|
|
31
31
|
planId,
|
|
32
32
|
providerPlanId,
|
|
@@ -37,6 +37,8 @@ publicAnalyticsSdk.track({
|
|
|
37
37
|
});
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Use `trackPaymentInfoSubmitted(...)` for payment details submitted, `trackCheckoutCompleted(...)` after a confirmed checkout, and `trackPaymentCheckoutSucceeded(...)` / `trackPaymentCheckoutReturned(...)` on redirect return screens.
|
|
41
|
+
|
|
40
42
|
Flush before redirects or payment handoff when losing the page would drop the event:
|
|
41
43
|
|
|
42
44
|
```ts
|
|
@@ -60,3 +62,4 @@ Provider-specific behavior is documented separately. Read [Meta Pixel and Conver
|
|
|
60
62
|
- Use `@funnelsgrove/analytics` for custom tracking; do not call the provider SDK directly.
|
|
61
63
|
- When changing flow, verify step start/end still fires on the new path.
|
|
62
64
|
- When changing offers, verify checkout events include plan id, provider plan id, coupon id, amount, and mode.
|
|
65
|
+
- Use `stepType: 'paywall_offer'` on paywall checkout events so analytics rollups do not depend on step names.
|
|
@@ -6,15 +6,18 @@ Flow is product logic. The source of truth is `src/config/funnel.manifest.ts`; r
|
|
|
6
6
|
|
|
7
7
|
The manifest defines:
|
|
8
8
|
|
|
9
|
-
- `viewport`: the designed shell size
|
|
9
|
+
- `viewport`: the designed shell size. New funnels also declare default QA
|
|
10
|
+
`breakpoints`: small 375x667, medium 393x852, large 402x874, and
|
|
11
|
+
desktop-small 1280x800.
|
|
10
12
|
- `assets`: image metadata used by runtime/build tooling. Declare every
|
|
11
13
|
funnel-critical raster image here with stable `src`, `width`, and `height`
|
|
12
14
|
so publish can reduce image size during the build and generate AVIF/WebP
|
|
13
15
|
variants. Funnel shells may use this data to warm likely next-step images,
|
|
14
16
|
but first-viewport images should still use the framework's normal
|
|
15
17
|
priority/preload mechanism.
|
|
16
|
-
- `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, and optional `assetIds`. New `path` values must be meaningful public route slugs, not `/step-1` style URLs. Sequential ids are acceptable when the funnel uses them internally.
|
|
18
|
+
- `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, optional `tags`, and optional `assetIds`. New `path` values must be meaningful public route slugs, not `/step-1` style URLs. Sequential ids are acceptable when the funnel uses them internally.
|
|
17
19
|
- `edgesByStepId`: graph edges between steps.
|
|
20
|
+
- `branches`: builder metadata for conditional paths that own one or more steps before reconverging.
|
|
18
21
|
- `experiments`: optional variant routing.
|
|
19
22
|
|
|
20
23
|
Keep `steps[].id`, `path`, and answer keys stable unless the request is a migration.
|
|
@@ -43,6 +46,36 @@ eligibility: [
|
|
|
43
46
|
|
|
44
47
|
The flow controller resolves the next step from configured edges first, then falls back to the sequential order. It also keeps the URL in sync through `getPathForStep(...)` and browser history.
|
|
45
48
|
|
|
49
|
+
## Branch Labels
|
|
50
|
+
|
|
51
|
+
A branch starts when one step has different next steps depending on an answer. If a direction owns one or more branch-only steps before all directions share a common next step again, add a `branches` entry:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
branches: [
|
|
55
|
+
{
|
|
56
|
+
id: 'device-android',
|
|
57
|
+
name: 'Device type',
|
|
58
|
+
sourceStepId: 'eligibility',
|
|
59
|
+
conditionId: 'eligibility:yes',
|
|
60
|
+
label: 'yes-branch',
|
|
61
|
+
tags: ['android-branch', 'yes-branch'],
|
|
62
|
+
stepIds: ['active-google-claim', 'subscriptions'],
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`name` is the readable branch group shown in builder. `label` should come from the answer that opened the path, such as `yes-branch`, `no-branch`, or `family-branch`. `stepIds` includes only the steps owned by that direction; stop before the common next step. Directions that jump directly to the common next step do not need a branch entry.
|
|
68
|
+
|
|
69
|
+
Keep branch and edge metadata builder-readable, following the ClaimBee pattern:
|
|
70
|
+
`branches` should be an inline array on the manifest, and `edgesByStepId`
|
|
71
|
+
should use literal string keys and `toStepId` values. Avoid hiding builder
|
|
72
|
+
metadata behind constants such as `branches: flowBranches`, computed keys such
|
|
73
|
+
as `[emailCaptureManifestStep.id]`, or targets such as
|
|
74
|
+
`toStepId: emailCaptureManifestStep.id`; the builder reads these fields from
|
|
75
|
+
source text without executing the module.
|
|
76
|
+
|
|
77
|
+
Use `steps[].tags` for stable builder labels that belong to one step, including current experiment labels such as `paywall-test-control`. Keep stopped A/B variants and other intentional inactive screens tagged too; builder derives `unused` automatically when those steps are not reachable from the default entry point or any active experiment path.
|
|
78
|
+
|
|
46
79
|
## Step Navigation
|
|
47
80
|
|
|
48
81
|
Use the funnel context:
|
|
@@ -78,7 +111,11 @@ Keep the control variant stable and do not remove a running variant until analyt
|
|
|
78
111
|
|
|
79
112
|
- Add or update the manifest step.
|
|
80
113
|
- Register the component in `src/runtime/step-registry.ts`.
|
|
81
|
-
- Update `edgesByStepId`, `entryPoints`, and `assetIds` if needed.
|
|
114
|
+
- Update `edgesByStepId`, `entryPoints`, `branches`, step `tags`, and `assetIds` if needed.
|
|
115
|
+
- Label every branch-owned path with `branches[].name`, answer-derived `label`, useful `tags`, and owned `stepIds`.
|
|
116
|
+
- Keep builder metadata inline/literal so groups, labels, and `unused` badges are visible in builder.
|
|
117
|
+
- Label every running experiment variant with stable tags/labels like `<experiment-id>-control` and `<experiment-id>-variant-b`.
|
|
118
|
+
- Confirm intentionally inactive/unreachable steps will show as `unused` in builder.
|
|
82
119
|
- Use a meaningful public `path` for every new step. Internal ids can be
|
|
83
120
|
sequential, but URLs should describe the screen.
|
|
84
121
|
- Keep image preloading manifest-driven: step images belong in
|
|
@@ -94,7 +94,7 @@ motivation: {
|
|
|
94
94
|
motivation: [{ toStepId: 'step-3' }],
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
**Verify:** `npm run dev`, open `/motivation`, confirm it renders at
|
|
97
|
+
**Verify:** `npm run dev`, open `/motivation`, confirm it renders at small 375x667, medium 393x852, large 402x874, and desktop-small 1280x800, Continue (or auto-advance) lands on the next step, and `npm run test:run && npm run lint` pass.
|
|
98
98
|
|
|
99
99
|
## Editing an Existing Step
|
|
100
100
|
|
|
@@ -6,10 +6,53 @@ Local edits are not public until they are synced and published. Sync creates or
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
fgrove status
|
|
9
|
+
git status --short
|
|
10
|
+
fgrove github status
|
|
11
|
+
fgrove publish --env preview --message '<summary>'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Before syncing down over an existing synced directory, check local changes. If
|
|
15
|
+
`git status --short` shows local edits, checkpoint them first. `fgrove sync
|
|
16
|
+
down` refuses to overwrite a dirty synced directory by default; use `--force`
|
|
17
|
+
only when discarding local changes is intentional.
|
|
18
|
+
|
|
19
|
+
When GitHub is connected and its branch is ahead, run `fgrove github pull` to
|
|
20
|
+
pull GitHub into the hosted draft, then poll `fgrove github status` until the
|
|
21
|
+
pull job is completed or skipped. Sync that latest draft into a clean directory
|
|
22
|
+
and merge local changes intentionally before continuing.
|
|
23
|
+
|
|
24
|
+
For GitHub-connected funnels, source changes must be pushed to GitHub first,
|
|
25
|
+
then pulled into the hosted draft:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git push
|
|
29
|
+
fgrove github pull
|
|
30
|
+
fgrove github status
|
|
31
|
+
fgrove publish --env preview --message '<summary>'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Poll `fgrove github status` until the pull job is completed or skipped before
|
|
35
|
+
publishing. Do not run `fgrove sync up` for the same source diff; the CLI and
|
|
36
|
+
API reject draft source sync for GitHub-connected funnels.
|
|
37
|
+
|
|
38
|
+
When GitHub is not connected, the hosted draft is the remote source of truth.
|
|
39
|
+
Download it into a temporary clean directory with `fgrove sync down --funnel
|
|
40
|
+
<id-or-slug> --dir <temp-dir>` or an already selected `fgrove use` context,
|
|
41
|
+
compare it with local changes, merge intentionally, rerun checks, then `fgrove
|
|
42
|
+
sync up`.
|
|
43
|
+
|
|
44
|
+
For funnels without GitHub, sync local source directly to the hosted draft:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
9
47
|
fgrove sync up --message '<summary>'
|
|
10
48
|
fgrove publish --env preview --message '<summary>'
|
|
11
49
|
```
|
|
12
50
|
|
|
51
|
+
If `fgrove sync up` reports that the remote draft changed since the local
|
|
52
|
+
directory was synced, do not retry blindly. Download the current draft into a
|
|
53
|
+
temporary clean directory, merge local changes against it, rerun checks, and
|
|
54
|
+
sync again.
|
|
55
|
+
|
|
13
56
|
Refresh the ignored local `.env` from the remote project when project settings change:
|
|
14
57
|
|
|
15
58
|
```bash
|
|
@@ -40,7 +83,10 @@ calling the publish ready.
|
|
|
40
83
|
|
|
41
84
|
## Local Sync Contract
|
|
42
85
|
|
|
43
|
-
The CLI writes `.funnelsgrove-sync.json` into the local tree. Keep it there. It
|
|
86
|
+
The CLI writes `.funnelsgrove-sync.json` into the local tree. Keep it there. It
|
|
87
|
+
records workspace id, funnel id, current draft version id, and source hashes so
|
|
88
|
+
later `sync up` can patch only changed/deleted files and detect stale remote
|
|
89
|
+
drafts before overwriting them.
|
|
44
90
|
|
|
45
91
|
Local-only files are excluded from upload:
|
|
46
92
|
|
|
@@ -57,8 +103,15 @@ Do not edit generated build output as the source of truth.
|
|
|
57
103
|
## Agent Rules
|
|
58
104
|
|
|
59
105
|
- Run `fgrove status` before syncing.
|
|
106
|
+
- Check local changes before `sync down`; checkpoint or merge instead of
|
|
107
|
+
overwriting.
|
|
108
|
+
- For GitHub-connected funnels, push with normal git, then run `fgrove github
|
|
109
|
+
pull`; do not run `fgrove sync up` for the same source change.
|
|
60
110
|
- Use clear sync and publish messages.
|
|
61
|
-
- Sync before publishing
|
|
111
|
+
- Sync the hosted draft before publishing: GitHub-connected funnels use normal
|
|
112
|
+
`git push` plus `fgrove github pull`; funnels without GitHub use `fgrove sync
|
|
113
|
+
up`.
|
|
62
114
|
- Publish preview first and verify the returned URL.
|
|
63
|
-
- If preview verification fails, fix locally, run checks, sync
|
|
115
|
+
- If preview verification fails, fix locally, run checks, sync the hosted draft
|
|
116
|
+
again through the correct source path, and publish a new preview.
|
|
64
117
|
- Do not production publish without explicit instruction and target domain.
|
|
@@ -22,7 +22,14 @@ A production publish without a QA-passed matching preview build is a blocker unl
|
|
|
22
22
|
|
|
23
23
|
## 2. Visual Pass
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Run every visual pass at all four default breakpoints (rules in [step-ui-guidelines.md](step-ui-guidelines.md)):
|
|
26
|
+
|
|
27
|
+
| Breakpoint | Size |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| small | **375x667** |
|
|
30
|
+
| medium | **393x852** |
|
|
31
|
+
| large | **402x874** |
|
|
32
|
+
| desktop-small | **1280x800** |
|
|
26
33
|
|
|
27
34
|
- Nothing intersects or overlaps: text never collides with images, cards, badges, or the action bar; modals and dialogs fit the viewport.
|
|
28
35
|
- Continue button sits on an opaque bar at the bottom and stays visible on every step, including while content scrolls.
|
|
@@ -70,4 +77,4 @@ If checkout, payment mode, or test credentials are unavailable, name the skipped
|
|
|
70
77
|
|
|
71
78
|
## Reporting
|
|
72
79
|
|
|
73
|
-
Every QA run ends with a short report: stage + URLs tested, steps/branches/variants covered,
|
|
80
|
+
Every QA run ends with a short report: stage + URLs tested, steps/branches/variants covered, breakpoint results for small 375x667, medium 393x852, large 402x874, and desktop-small 1280x800, image optimization/preload result when relevant, paywall items 1–7 pass/fail, payment method used, console findings, and named blockers or explicitly skipped items with the reason. Blockers block production unless the user accepts the risk in so many words.
|
|
@@ -4,14 +4,18 @@ Every step must pass these rules before it counts as done. They are distilled fr
|
|
|
4
4
|
|
|
5
5
|
## Viewports
|
|
6
6
|
|
|
7
|
-
Funnels are mobile-first
|
|
7
|
+
Funnels are mobile-first, but QA must cover the desktop-small shell too. Build
|
|
8
|
+
and verify every created or edited step at the default breakpoints in
|
|
9
|
+
`src/config/funnel.manifest.ts`:
|
|
8
10
|
|
|
9
|
-
|
|
|
11
|
+
| Breakpoint | Size | Role |
|
|
10
12
|
| --- | --- | --- |
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
+
| small | **375 x 667** | iPhone SE and smaller-width stress check. |
|
|
14
|
+
| medium | **393 x 852** | iPhone 15 baseline. |
|
|
15
|
+
| large | **402 x 874** | iPhone 17 Pro baseline. |
|
|
16
|
+
| desktop-small | **1280 x 800** | 13-inch MacBook baseline. |
|
|
13
17
|
|
|
14
|
-
Pass criteria at
|
|
18
|
+
Pass criteria at every breakpoint: no horizontal scroll, no clipped or overlapping content, the primary CTA visible without scrolling on selection/input steps, and tap targets at least 44px tall.
|
|
15
19
|
|
|
16
20
|
## Layout Shell Contract
|
|
17
21
|
|
|
@@ -95,7 +99,7 @@ Use the ClaimBee/Blessly image loading pattern:
|
|
|
95
99
|
|
|
96
100
|
## Content-Fit Audit
|
|
97
101
|
|
|
98
|
-
Run on every created or edited step
|
|
102
|
+
Run on every created or edited step at small 375x667, medium 393x852, large 402x874, and desktop-small 1280x800:
|
|
99
103
|
|
|
100
104
|
1. Open the step in local preview (`npm run dev`).
|
|
101
105
|
2. Check: nothing clipped, nothing overlapping, no text truncated mid-word, images loaded with correct aspect, CTA fully visible above the fold on selection/input steps.
|
|
@@ -103,4 +107,4 @@ Run on every created or edited step, at 430x932 then 390x844:
|
|
|
103
107
|
4. Check disabled→enabled CTA transition where applicable.
|
|
104
108
|
5. Fix and re-check before moving to another step.
|
|
105
109
|
|
|
106
|
-
Report the audit (
|
|
110
|
+
Report the audit (all four breakpoints, pass/fail per step) in your summary.
|