@lotics/app-sdk 0.80.0 → 0.81.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.
- package/AGENTS.md +1 -1
- package/dist/src/hooks.js +13 -2
- package/dist/src/mock.d.ts +43 -13
- package/dist/src/mock.js +37 -13
- package/docs/data_fetching.md +3 -2
- package/docs/runtime.md +22 -4
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -24,7 +24,7 @@ signature; open the file.**
|
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state — and the `AgentRunLanding` every leg resolves), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics; a chat mutation refetches your queries through the realtime channel, not a separate poke). **A `file` input carries its own content** — no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`.** Also **what the member's own chat agent can do with your app while it is open** — the alias catalog it reads and how to shape a mutating alias for it. |
|
|
26
26
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen, and why a per-input bound is a tenancy floor rather than an authorization check (a caller-supplied id must be intersected with the record server-side). |
|
|
27
|
-
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, `openExternal`/`downloadFile`, geofencing, analytics, `useConfig` (App-Packages installation config), `getAppBinding` (package apps' runtime `F`/`OPT`/`ROLE` resolution via the generated `.lotics/app_fields.ts`), and the publish chain for package contributors. |
|
|
27
|
+
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, the design-time mock harness (`fixture` + `?__mock=1` — queries AND workflows, so an AI screen's in-flight/done/error states are reviewable without running or paying for anything), `openExternal`/`downloadFile`, geofencing, analytics, `useConfig` (App-Packages installation config), `getAppBinding` (package apps' runtime `F`/`OPT`/`ROLE` resolution via the generated `.lotics/app_fields.ts`), and the publish chain for package contributors. |
|
|
28
28
|
|
|
29
29
|
## Non-negotiables (each detailed in its doc)
|
|
30
30
|
|
package/dist/src/hooks.js
CHANGED
|
@@ -21,10 +21,21 @@ import useSWR from "swr";
|
|
|
21
21
|
import useSWRInfinite from "swr/infinite";
|
|
22
22
|
import { rpc, rpcAgentRun, rpcAgentRunContinue, postHostNotification, subscribeQueriesChanged, } from "./rpc.js";
|
|
23
23
|
import { initialAgentRunState, reduceAgentChunk, parseSseChunks, adoptSettledRun, pendingInteractiveCall, buildChoiceOutput, applyInteractiveAnswer, landingOf, proseOf, ABORTED, } from "./agent_stream.js";
|
|
24
|
-
import { getMockRows, hasMockFlag } from "./mock.js";
|
|
24
|
+
import { getMockRows, getMockWorkflow, hasMockFlag } from "./mock.js";
|
|
25
25
|
export { buildChoiceOutput } from "./agent_stream.js";
|
|
26
26
|
export function useWorkflow(alias) {
|
|
27
|
-
return useCallback((inputs) =>
|
|
27
|
+
return useCallback(async (inputs) => {
|
|
28
|
+
// Checked per CALL, not per render: a fixture registered after mount or
|
|
29
|
+
// swapped by HMR is picked up, and an app that never runs this workflow
|
|
30
|
+
// does no work here. Mock mode needs BOTH the `?__mock=1` flag and a
|
|
31
|
+
// registered fixture, so a bundle carrying demo data cannot answer real
|
|
32
|
+
// traffic with it.
|
|
33
|
+
const mock = getMockWorkflow(alias);
|
|
34
|
+
if (mock) {
|
|
35
|
+
return typeof mock === "function" ? await mock(inputs ?? {}) : mock;
|
|
36
|
+
}
|
|
37
|
+
return rpc("workflow", { alias, inputs: inputs ?? {} });
|
|
38
|
+
}, [alias]);
|
|
28
39
|
}
|
|
29
40
|
// Shared SWR config: surface a failed query immediately, keep the last good
|
|
30
41
|
// rows (no retry loop that masks the error), and honor the focus/reconnect
|
package/dist/src/mock.d.ts
CHANGED
|
@@ -1,33 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Demo / design-time fixture support for `useQuery`.
|
|
2
|
+
* Demo / design-time fixture support for `useQuery` and `useWorkflow`.
|
|
3
3
|
*
|
|
4
4
|
* Activation contract:
|
|
5
5
|
*
|
|
6
6
|
* 1. App passes `{ fixture }` to `mount(<App />, { fixture })`. The fixture
|
|
7
|
-
* is a `{ queries: { alias: Row[] }
|
|
8
|
-
* app declared in
|
|
7
|
+
* is a `{ queries: { alias: Row[] }, workflows: { alias: Result } }` map
|
|
8
|
+
* keyed by the same aliases the app declared in
|
|
9
|
+
* `package.json#lotics.queries` / `#lotics.workflows`.
|
|
9
10
|
* 2. At runtime, the user (or a screenshot script) loads the app with the
|
|
10
11
|
* `?__mock=1` URL search param. Without that param the SDK ignores the
|
|
11
|
-
* fixture entirely and
|
|
12
|
+
* fixture entirely and both hooks flow through the RPC bridge as usual.
|
|
12
13
|
*
|
|
13
14
|
* The two-step gate keeps demo data shipping in the bundle from leaking into
|
|
14
15
|
* normal traffic — the param namespace (`__mock` prefix) is reserved and
|
|
15
16
|
* unlikely to collide with app-side query state. Apps that don't pass a
|
|
16
|
-
* fixture pay nothing: `getMockRows`
|
|
17
|
-
* hook path is unchanged.
|
|
17
|
+
* fixture pay nothing: `getMockRows` / `getMockWorkflow` return `null` for
|
|
18
|
+
* every alias and the hook path is unchanged.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* `workflows` exists because the side-effect argument runs the other way: a
|
|
21
|
+
* mocked workflow does not RUN, so it produces no notification and no audit
|
|
22
|
+
* trail — it PREVENTS them. What it cannot produce is the followup state a
|
|
23
|
+
* subsequent `useQuery` would read, which is the author's call and is already
|
|
24
|
+
* true of a mocked query. Without it, an app whose only AI surface is a
|
|
25
|
+
* workflow that reads and calls `agent(...)` — the standard shape — had no
|
|
26
|
+
* non-billing path to its own in-flight / done / error states at all, so those
|
|
27
|
+
* three screens could not be reviewed without spending on a live workspace.
|
|
24
28
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
29
|
+
* A fixture entry may be the RESULT, or a FUNCTION of the inputs. The function
|
|
30
|
+
* form is what makes the in-flight state reachable: resolve on a timer and the
|
|
31
|
+
* app renders the pending branch it otherwise never shows. It also lets one
|
|
32
|
+
* alias answer differently per input, which is how an error branch is reviewed
|
|
33
|
+
* beside a success one.
|
|
34
|
+
*
|
|
35
|
+
* What's deliberately *not* mocked: `useFileUpload`. Bytes and progress are a
|
|
36
|
+
* different shape from a request/response pair, and nothing has needed it.
|
|
37
|
+
*/
|
|
38
|
+
import type { WorkflowResult } from "./hooks.js";
|
|
39
|
+
/**
|
|
40
|
+
* What a mocked workflow answers with: a fixed result, or a function of the
|
|
41
|
+
* inputs it was called with. Return a promise from the function to hold the
|
|
42
|
+
* caller in its pending state for as long as the review needs.
|
|
27
43
|
*/
|
|
44
|
+
export type MockWorkflow = WorkflowResult | ((inputs: Record<string, unknown>) => WorkflowResult | Promise<WorkflowResult>);
|
|
28
45
|
export interface AppFixture {
|
|
29
46
|
/** Map of query alias → rows the hook should return when mock mode is on. */
|
|
30
47
|
queries?: Record<string, Array<Record<string, unknown>>>;
|
|
48
|
+
/** Map of workflow alias → the result it resolves with when mock mode is on.
|
|
49
|
+
* The workflow never executes, so nothing it would have written is written. */
|
|
50
|
+
workflows?: Record<string, MockWorkflow>;
|
|
31
51
|
}
|
|
32
52
|
/**
|
|
33
53
|
* Called by `mount({ fixture })`. Module-level state because the SDK has no
|
|
@@ -51,3 +71,13 @@ export declare function hasMockFlag(): boolean;
|
|
|
51
71
|
* only some queries and let the rest flow through to real data.
|
|
52
72
|
*/
|
|
53
73
|
export declare function getMockRows(alias: string): Array<Record<string, unknown>> | null;
|
|
74
|
+
/**
|
|
75
|
+
* The fixture entry for a workflow alias when mock mode is active *and* the
|
|
76
|
+
* fixture has an entry for it. Otherwise null — the hook falls through to the
|
|
77
|
+
* real RPC path, so an app may mock one workflow and let the rest execute.
|
|
78
|
+
*
|
|
79
|
+
* Resolved when the workflow is CALLED rather than when the hook is created, so
|
|
80
|
+
* a fixture registered after mount (or replaced by HMR) is picked up, and an
|
|
81
|
+
* app that never calls the workflow pays nothing.
|
|
82
|
+
*/
|
|
83
|
+
export declare function getMockWorkflow(alias: string): MockWorkflow | null;
|
package/dist/src/mock.js
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Demo / design-time fixture support for `useQuery`.
|
|
2
|
+
* Demo / design-time fixture support for `useQuery` and `useWorkflow`.
|
|
3
3
|
*
|
|
4
4
|
* Activation contract:
|
|
5
5
|
*
|
|
6
6
|
* 1. App passes `{ fixture }` to `mount(<App />, { fixture })`. The fixture
|
|
7
|
-
* is a `{ queries: { alias: Row[] }
|
|
8
|
-
* app declared in
|
|
7
|
+
* is a `{ queries: { alias: Row[] }, workflows: { alias: Result } }` map
|
|
8
|
+
* keyed by the same aliases the app declared in
|
|
9
|
+
* `package.json#lotics.queries` / `#lotics.workflows`.
|
|
9
10
|
* 2. At runtime, the user (or a screenshot script) loads the app with the
|
|
10
11
|
* `?__mock=1` URL search param. Without that param the SDK ignores the
|
|
11
|
-
* fixture entirely and
|
|
12
|
+
* fixture entirely and both hooks flow through the RPC bridge as usual.
|
|
12
13
|
*
|
|
13
14
|
* The two-step gate keeps demo data shipping in the bundle from leaking into
|
|
14
15
|
* normal traffic — the param namespace (`__mock` prefix) is reserved and
|
|
15
16
|
* unlikely to collide with app-side query state. Apps that don't pass a
|
|
16
|
-
* fixture pay nothing: `getMockRows`
|
|
17
|
-
* hook path is unchanged.
|
|
17
|
+
* fixture pay nothing: `getMockRows` / `getMockWorkflow` return `null` for
|
|
18
|
+
* every alias and the hook path is unchanged.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* `workflows` exists because the side-effect argument runs the other way: a
|
|
21
|
+
* mocked workflow does not RUN, so it produces no notification and no audit
|
|
22
|
+
* trail — it PREVENTS them. What it cannot produce is the followup state a
|
|
23
|
+
* subsequent `useQuery` would read, which is the author's call and is already
|
|
24
|
+
* true of a mocked query. Without it, an app whose only AI surface is a
|
|
25
|
+
* workflow that reads and calls `agent(...)` — the standard shape — had no
|
|
26
|
+
* non-billing path to its own in-flight / done / error states at all, so those
|
|
27
|
+
* three screens could not be reviewed without spending on a live workspace.
|
|
24
28
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
29
|
+
* A fixture entry may be the RESULT, or a FUNCTION of the inputs. The function
|
|
30
|
+
* form is what makes the in-flight state reachable: resolve on a timer and the
|
|
31
|
+
* app renders the pending branch it otherwise never shows. It also lets one
|
|
32
|
+
* alias answer differently per input, which is how an error branch is reviewed
|
|
33
|
+
* beside a success one.
|
|
34
|
+
*
|
|
35
|
+
* What's deliberately *not* mocked: `useFileUpload`. Bytes and progress are a
|
|
36
|
+
* different shape from a request/response pair, and nothing has needed it.
|
|
27
37
|
*/
|
|
28
38
|
let registeredFixture;
|
|
29
39
|
/**
|
|
@@ -70,3 +80,17 @@ export function getMockRows(alias) {
|
|
|
70
80
|
const rows = registeredFixture?.queries?.[alias];
|
|
71
81
|
return rows ?? null;
|
|
72
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The fixture entry for a workflow alias when mock mode is active *and* the
|
|
85
|
+
* fixture has an entry for it. Otherwise null — the hook falls through to the
|
|
86
|
+
* real RPC path, so an app may mock one workflow and let the rest execute.
|
|
87
|
+
*
|
|
88
|
+
* Resolved when the workflow is CALLED rather than when the hook is created, so
|
|
89
|
+
* a fixture registered after mount (or replaced by HMR) is picked up, and an
|
|
90
|
+
* app that never calls the workflow pays nothing.
|
|
91
|
+
*/
|
|
92
|
+
export function getMockWorkflow(alias) {
|
|
93
|
+
if (!isMockMode())
|
|
94
|
+
return null;
|
|
95
|
+
return registeredFixture?.workflows?.[alias] ?? null;
|
|
96
|
+
}
|
package/docs/data_fetching.md
CHANGED
|
@@ -119,8 +119,9 @@ server validates system conditions by `type` and never reads `field_key` on them
|
|
|
119
119
|
queries currently on screen (mounted hooks only); it never reaches into app data, it only tells
|
|
120
120
|
the app its rendered rows may be stale. Inert standalone and in mock mode.
|
|
121
121
|
- A design-time fixture registered via `mount(<App />, { fixture })` plus the `?__mock=1` URL flag
|
|
122
|
-
short-circuits all three hooks (rows come from the fixture, no request, `loading` stays `false`)
|
|
123
|
-
|
|
122
|
+
short-circuits all three hooks (rows come from the fixture, no request, `loading` stays `false`).
|
|
123
|
+
The same fixture mocks `useWorkflow`, so a screen's in-flight / done / error states are
|
|
124
|
+
reviewable without running anything — see [./runtime.md](./runtime.md).
|
|
124
125
|
|
|
125
126
|
### Error messages you will actually see
|
|
126
127
|
|
package/docs/runtime.md
CHANGED
|
@@ -47,6 +47,16 @@ mount(<App />, {
|
|
|
47
47
|
orders: MOCK_ORDERS, // alias → rows, same aliases as package.json#lotics.queries
|
|
48
48
|
customers: MOCK_CUSTOMERS,
|
|
49
49
|
},
|
|
50
|
+
workflows: {
|
|
51
|
+
// A fixed result, for the settled state.
|
|
52
|
+
archive: { status: "success", message: "Archived 3 orders." },
|
|
53
|
+
// Or a FUNCTION of the inputs — the only way to reach the IN-FLIGHT state,
|
|
54
|
+
// and the way to put an error branch beside a success one.
|
|
55
|
+
publish: (inputs) =>
|
|
56
|
+
inputs.dryRun
|
|
57
|
+
? { status: "error", message: "Row 2 has no customer." }
|
|
58
|
+
: new Promise((r) => setTimeout(() => r({ status: "success" }), 1200)),
|
|
59
|
+
},
|
|
50
60
|
},
|
|
51
61
|
});
|
|
52
62
|
```
|
|
@@ -55,7 +65,7 @@ Activation is a **two-step gate** — both must hold, so demo data shipping in t
|
|
|
55
65
|
bundle never leaks into normal traffic:
|
|
56
66
|
|
|
57
67
|
1. A fixture is registered via `mount({ fixture })` (`AppFixture` type:
|
|
58
|
-
`dist/src/mock.d.ts` — `{ queries
|
|
68
|
+
`dist/src/mock.d.ts` — `{ queries?, workflows? }`).
|
|
59
69
|
2. The page URL carries `?__mock=1` (exactly `1`). Without the flag the fixture
|
|
60
70
|
is completely inert.
|
|
61
71
|
|
|
@@ -68,9 +78,17 @@ transport. Calling `mount` again (HMR) replaces the registration last-write-wins
|
|
|
68
78
|
as fetched rows, so shape them exactly like the query's real output — the same
|
|
69
79
|
serialized cells your `row.*` / `readSelect` / `readFiles` readers decode —
|
|
70
80
|
or the readers will decode nothing.
|
|
71
|
-
- **
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
- **A mocked workflow does not RUN.** `useWorkflow(alias)` resolves the fixture
|
|
82
|
+
entry and sends nothing, so there is no notification, no audit trail and no
|
|
83
|
+
spend — the side-effect argument is the reason to mock it, not a reason not
|
|
84
|
+
to. What a mock cannot produce is the followup state a later query would read;
|
|
85
|
+
that is yours to fixture too, exactly as it already is for queries.
|
|
86
|
+
**Reach the in-flight state with a function** that resolves on a timer: an app
|
|
87
|
+
whose only AI surface is a workflow calling `agent(...)` otherwise has no
|
|
88
|
+
non-billing path to its own thinking / done / error screens, which is how
|
|
89
|
+
those three ship unreviewed.
|
|
90
|
+
- **Not mocked:** uploads, `useFieldOptions`, members, comments, agent runs. In
|
|
91
|
+
mock mode those still hit the real transport.
|
|
74
92
|
- **Analytics is disabled** whenever `?__mock=1` is present, fixture or not — a
|
|
75
93
|
screenshot/design-time load emits no events.
|
|
76
94
|
- The `__mock` param-name prefix is reserved by the SDK; don't use it for
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Runtime SDK for Lotics custom-code apps
|
|
3
|
+
"version": "0.81.0",
|
|
4
|
+
"description": "Runtime SDK for Lotics custom-code apps \u2014 typed hooks, postMessage bridge, mount entry point",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|