akanjs 3.0.0-alpha.11 → 3.0.0-alpha.13
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/base/symbols.ts +4 -0
- package/constant/index.ts +1 -0
- package/constant/mask.ts +60 -0
- package/dictionary/dictInfo.ts +12 -1
- package/fetch/client/fetchClient.ts +9 -0
- package/package.json +1 -1
- package/server/akanApp.ts +3 -1
- package/service/predefinedAdaptor/index.ts +1 -0
- package/service/predefinedAdaptor/insightQuery.ts +183 -0
- package/signal/mcp/Msg.ts +5 -33
- package/store/action.ts +9 -20
- package/store/actionTag.ts +28 -0
- package/store/agent/AgentBridge.ts +281 -0
- package/store/agent/StoreCatalogue.ts +296 -0
- package/store/agent/index.ts +3 -0
- package/store/agent/types.ts +50 -0
- package/store/databaseStateNames.ts +31 -0
- package/store/formSetterNames.ts +21 -0
- package/store/index.ts +7 -0
- package/store/rootStore.ts +2 -1
- package/store/sliceRole.ts +36 -0
- package/store/state.ts +2 -12
- package/store/store.ts +5 -0
- package/store/storeInstance.ts +54 -16
- package/store/storeRegistry.ts +10 -0
- package/types/base/symbols.d.ts +4 -0
- package/types/constant/index.d.ts +1 -0
- package/types/constant/mask.d.ts +34 -0
- package/types/dictionary/dictInfo.d.ts +9 -2
- package/types/fetch/client/fetchClient.d.ts +9 -0
- package/types/service/predefinedAdaptor/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
- package/types/signal/mcp/Msg.d.ts +3 -4
- package/types/store/actionTag.d.ts +17 -0
- package/types/store/agent/AgentBridge.d.ts +70 -0
- package/types/store/agent/StoreCatalogue.d.ts +21 -0
- package/types/store/agent/index.d.ts +3 -0
- package/types/store/agent/types.d.ts +49 -0
- package/types/store/agent.d.ts +1 -0
- package/types/store/databaseStateNames.d.ts +25 -0
- package/types/store/formSetterNames.d.ts +16 -0
- package/types/store/index.d.ts +6 -0
- package/types/store/rootStore.d.ts +4 -1
- package/types/store/sliceRole.d.ts +25 -0
- package/types/store/store.d.ts +4 -1
- package/types/store/storeInstance.d.ts +16 -0
- package/types/store/storeRegistry.d.ts +3 -0
- package/types/ui/Agent/Dock.d.ts +16 -0
- package/types/ui/Agent/Section.d.ts +10 -0
- package/types/ui/Agent/StateKey.d.ts +15 -0
- package/types/ui/Agent/Tool.d.ts +15 -0
- package/types/ui/Agent/Transcript.d.ts +13 -0
- package/types/ui/Agent/index.d.ts +11 -0
- package/types/ui/Agent.d.ts +1 -0
- package/types/ui/agentAttrs.d.ts +14 -0
- package/types/ui/index.d.ts +2 -0
- package/ui/Agent/Dock.tsx +61 -0
- package/ui/Agent/Section.tsx +24 -0
- package/ui/Agent/StateKey.tsx +42 -0
- package/ui/Agent/Tool.tsx +66 -0
- package/ui/Agent/Transcript.tsx +33 -0
- package/ui/Agent/index.ts +7 -0
- package/ui/Button.tsx +2 -0
- package/ui/Field.tsx +15 -7
- package/ui/Input.tsx +7 -0
- package/ui/Select.tsx +2 -1
- package/ui/Switch.tsx +2 -0
- package/ui/agentAttrs.ts +19 -0
- package/ui/index.ts +2 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { capitalize, lowerlize } from "akanjs/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The keys `makeFormSetter` publishes for one field of one model.
|
|
5
|
+
*
|
|
6
|
+
* Shared with the agent catalogue, which has to answer what `setNameOnUser` takes without parsing the name apart —
|
|
7
|
+
* `set(.+)On(.+)` has more than one reading whenever a field or a model name contains `On`. Computing the same names
|
|
8
|
+
* forward from the same field metadata is unambiguous, and keeps the two from drifting when a name changes here.
|
|
9
|
+
*/
|
|
10
|
+
export const formSetterNames = (className: string, key: string) => {
|
|
11
|
+
const classKeyName = capitalize(key);
|
|
12
|
+
return {
|
|
13
|
+
field: lowerlize(key),
|
|
14
|
+
Field: classKeyName,
|
|
15
|
+
setFieldOnModel: `set${classKeyName}On${className}`,
|
|
16
|
+
addFieldOnModel: `add${classKeyName}On${className}`,
|
|
17
|
+
subFieldOnModel: `sub${classKeyName}On${className}`,
|
|
18
|
+
addOrSubFieldOnModel: `addOrSub${classKeyName}On${className}`,
|
|
19
|
+
uploadFieldOnModel: `upload${classKeyName}On${className}`,
|
|
20
|
+
};
|
|
21
|
+
};
|
package/store/index.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
export * from "./action";
|
|
2
|
+
export * from "./actionTag";
|
|
3
|
+
export * from "./agent";
|
|
2
4
|
export * from "./baseSt";
|
|
5
|
+
export * from "./databaseStateNames";
|
|
6
|
+
export * from "./formSetterNames";
|
|
3
7
|
export * from "./rootStore";
|
|
8
|
+
export * from "./sliceRole";
|
|
4
9
|
export * from "./state";
|
|
5
10
|
export * from "./stateBuilder";
|
|
6
11
|
export * from "./store";
|
|
12
|
+
|
|
13
|
+
export { StoreInstance } from "./storeInstance";
|
|
7
14
|
export * from "./storeRegistry";
|
|
8
15
|
export * from "./types";
|
package/store/rootStore.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ACTION_META, Cls, STATE_DERIVED_META, STATE_INIT_META, STATE_META } from "akanjs/base";
|
|
1
|
+
import type { ACTION_META, ACTION_OWNER_META, Cls, STATE_DERIVED_META, STATE_INIT_META, STATE_META } from "akanjs/base";
|
|
2
2
|
import type { SerializedSlice } from "akanjs/signal";
|
|
3
3
|
import type { StateDerivedMeta, StateInitializerMap } from "./stateBuilder";
|
|
4
4
|
import type { SetGetWritable } from "./types";
|
|
@@ -20,6 +20,7 @@ export type RootStoreCls<
|
|
|
20
20
|
[STATE_INIT_META]: StateInitializerMap;
|
|
21
21
|
[STATE_DERIVED_META]: StateDerivedMeta;
|
|
22
22
|
[ACTION_META]: { [key: string]: (...args: any[]) => any };
|
|
23
|
+
[ACTION_OWNER_META]: { [key: string]: string };
|
|
23
24
|
slice: { [key: string]: { [key: string]: SerializedSlice } };
|
|
24
25
|
_slice: SliceInfoObj;
|
|
25
26
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SerializedArg } from "akanjs/signal";
|
|
2
|
+
import type { SliceStateKey } from "./state";
|
|
3
|
+
|
|
4
|
+
/** The generated slice actions, named by what they do rather than by the key any one slice publishes them under. */
|
|
5
|
+
export type SliceActionKey =
|
|
6
|
+
| "initModel"
|
|
7
|
+
| "refreshModel"
|
|
8
|
+
| "selectModel"
|
|
9
|
+
| "setPageOfModel"
|
|
10
|
+
| "addPageOfModel"
|
|
11
|
+
| "setLimitOfModel"
|
|
12
|
+
| "setQueryArgsOfModel"
|
|
13
|
+
| "setSortOfModel";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a generated key on `st.do` / `st.use` actually is.
|
|
17
|
+
*
|
|
18
|
+
* A slice publishes its keys with the model and the suffix spliced into the role name — `setPageOfUserInOrg` for the
|
|
19
|
+
* `setPageOfModel` role of `user`'s `inOrg` slice — and the splicing is string replacement over capitalized names.
|
|
20
|
+
* Recording the role while the key is being built is what saves a reader of the finished store from running that
|
|
21
|
+
* replacement backwards, which does not have one answer: on a model named `page`, `setPageOfPage` is both the
|
|
22
|
+
* paging action and a field setter.
|
|
23
|
+
*/
|
|
24
|
+
export interface SliceActionRole {
|
|
25
|
+
role: SliceActionKey;
|
|
26
|
+
refName: string;
|
|
27
|
+
sliceName: string;
|
|
28
|
+
/** The slice's own arguments, which the `initModel` and `setQueryArgsOfModel` roles take positionally. */
|
|
29
|
+
args: SerializedArg[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SliceStateRole {
|
|
33
|
+
role: SliceStateKey;
|
|
34
|
+
refName: string;
|
|
35
|
+
sliceName: string;
|
|
36
|
+
}
|
package/store/state.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
SlceDbSort,
|
|
15
15
|
SliceCls,
|
|
16
16
|
} from "akanjs/signal";
|
|
17
|
+
import { databaseStateNames } from "./databaseStateNames";
|
|
17
18
|
import type { StoreSliceArgs, StoreSliceMap, StoreSliceSuffixCap, Submit } from "./types";
|
|
18
19
|
|
|
19
20
|
export type SliceStateKey =
|
|
@@ -151,18 +152,7 @@ export type DefaultState<
|
|
|
151
152
|
|
|
152
153
|
export const createDatabaseState = (refName: string) => {
|
|
153
154
|
const cnst = ConstantRegistry.getDatabase(refName);
|
|
154
|
-
const
|
|
155
|
-
const names = {
|
|
156
|
-
model: fieldName,
|
|
157
|
-
Model: className,
|
|
158
|
-
modelLoading: `${fieldName}Loading`,
|
|
159
|
-
modelForm: `${fieldName}Form`,
|
|
160
|
-
modelFormLoading: `${fieldName}FormLoading`,
|
|
161
|
-
modelSubmit: `${fieldName}Submit`,
|
|
162
|
-
modelViewAt: `${fieldName}ViewAt`,
|
|
163
|
-
modelModal: `${fieldName}Modal`,
|
|
164
|
-
modelOperation: `${fieldName}Operation`,
|
|
165
|
-
};
|
|
155
|
+
const names = databaseStateNames(refName);
|
|
166
156
|
const baseState = {
|
|
167
157
|
[names.model]: null,
|
|
168
158
|
[names.modelLoading]: true,
|
package/store/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACTION_META,
|
|
3
|
+
ACTION_OWNER_META,
|
|
3
4
|
type Cls,
|
|
4
5
|
type MergeAllActionTypes,
|
|
5
6
|
type MergeAllKeyOfObjects,
|
|
@@ -120,6 +121,7 @@ export type StoreCls<
|
|
|
120
121
|
[STATE_INIT_META]: StateInitializerMap;
|
|
121
122
|
[STATE_DERIVED_META]: StateDerivedMeta;
|
|
122
123
|
[ACTION_META]: { [key: string]: (...args: any[]) => any };
|
|
124
|
+
[ACTION_OWNER_META]: { [key: string]: string };
|
|
123
125
|
slice: { [key: string]: SerializedSlice };
|
|
124
126
|
_slice: SlceCls[typeof SLICE_META];
|
|
125
127
|
}
|
|
@@ -187,6 +189,7 @@ export function store<Sig extends ClientSignal<any, any, any> | string, State>(
|
|
|
187
189
|
static [STATE_INIT_META] = {};
|
|
188
190
|
static [STATE_DERIVED_META] = createEmptyDerivedMeta();
|
|
189
191
|
static [ACTION_META] = {};
|
|
192
|
+
static [ACTION_OWNER_META] = {};
|
|
190
193
|
static slice = {};
|
|
191
194
|
} as StoreCls;
|
|
192
195
|
const writableStateRaw = stateFactory(createWritableStateBuilder());
|
|
@@ -204,6 +207,7 @@ export function store<Sig extends ClientSignal<any, any, any> | string, State>(
|
|
|
204
207
|
writable.meta,
|
|
205
208
|
);
|
|
206
209
|
Object.assign(storeCls[ACTION_META], ...libStores.map((libStore) => libStore[ACTION_META]));
|
|
210
|
+
Object.assign(storeCls[ACTION_OWNER_META], ...libStores.map((libStore) => libStore[ACTION_OWNER_META] ?? {}));
|
|
207
211
|
applyMixins(storeCls, libStores);
|
|
208
212
|
if (signal) {
|
|
209
213
|
const signalState = {
|
|
@@ -218,6 +222,7 @@ export function store<Sig extends ClientSignal<any, any, any> | string, State>(
|
|
|
218
222
|
};
|
|
219
223
|
Object.assign(storeCls.prototype, actions);
|
|
220
224
|
Object.assign(storeCls[ACTION_META], actions);
|
|
225
|
+
Object.assign(storeCls[ACTION_OWNER_META], Object.fromEntries(Object.keys(actions).map((key) => [key, refName])));
|
|
221
226
|
Object.assign(storeCls.slice, signal.serializedSignal.slice ?? {});
|
|
222
227
|
}
|
|
223
228
|
if (derivedStateFactory) {
|
package/store/storeInstance.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { ACTION_META, STATE_DERIVED_META, STATE_INIT_META } from "akanjs/base";
|
|
1
|
+
import { ACTION_META, ACTION_OWNER_META, STATE_DERIVED_META, STATE_INIT_META } from "akanjs/base";
|
|
2
2
|
import { Translator } from "akanjs/client";
|
|
3
3
|
import { capitalize, Logger, parseAkanI18nEnv } from "akanjs/common";
|
|
4
|
+
import type { SerializedArg } from "akanjs/signal";
|
|
4
5
|
import { enableMapSet, produce } from "immer";
|
|
5
6
|
import type { RefObject } from "react";
|
|
7
|
+
import { actionTagOf, tagAction } from "./actionTag";
|
|
6
8
|
import { useEffect, useRef, useSyncExternalStore } from "./hooks";
|
|
7
9
|
import type { RootStoreCls } from "./rootStore";
|
|
10
|
+
import type { SliceActionKey, SliceActionRole, SliceStateRole } from "./sliceRole";
|
|
8
11
|
import type { SliceStateKey } from "./state";
|
|
9
12
|
import { evaluateInitializers, type SearchParamsState, type StateDerivedMeta } from "./stateBuilder";
|
|
10
13
|
|
|
@@ -14,16 +17,6 @@ type StoreStateRecord = Record<string, unknown>;
|
|
|
14
17
|
type StoreAction = (...args: unknown[]) => unknown;
|
|
15
18
|
type TranslationParam = Record<string, string | number>;
|
|
16
19
|
|
|
17
|
-
type SliceActionKey =
|
|
18
|
-
| "initModel"
|
|
19
|
-
| "refreshModel"
|
|
20
|
-
| "selectModel"
|
|
21
|
-
| "setPageOfModel"
|
|
22
|
-
| "addPageOfModel"
|
|
23
|
-
| "setLimitOfModel"
|
|
24
|
-
| "setQueryArgsOfModel"
|
|
25
|
-
| "setSortOfModel";
|
|
26
|
-
|
|
27
20
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
28
21
|
Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
29
22
|
|
|
@@ -172,6 +165,41 @@ export class StoreInstance {
|
|
|
172
165
|
do: { [key: string]: StoreAction } = {};
|
|
173
166
|
slice: { [key: string]: unknown } = {};
|
|
174
167
|
|
|
168
|
+
readonly #sliceActionRoles = new Map<string, SliceActionRole>();
|
|
169
|
+
readonly #sliceStateRoles = new Map<string, SliceStateRole>();
|
|
170
|
+
readonly #actionArity = new Map<string, number>();
|
|
171
|
+
readonly #actionOwners = new Map<string, string>();
|
|
172
|
+
|
|
173
|
+
/** Which module declared each action. The dictionary node its words are written in is named after it. */
|
|
174
|
+
get actionOwners(): ReadonlyMap<string, string> {
|
|
175
|
+
return this.#actionOwners;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* How many arguments each action declares.
|
|
180
|
+
*
|
|
181
|
+
* Recorded because `do[key]` is a rest-argument wrapper around the real method, so its own `length` is zero for
|
|
182
|
+
* everything — the arity is gone by the time anyone holding the instance could ask. A rest parameter on the method
|
|
183
|
+
* itself still reads as zero; nothing can recover that.
|
|
184
|
+
*/
|
|
185
|
+
get actionArity(): ReadonlyMap<string, number> {
|
|
186
|
+
return this.#actionArity;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** What each generated slice key is, for a reader that has only the finished store. See `SliceActionRole`. */
|
|
190
|
+
get sliceActionRoles(): ReadonlyMap<string, SliceActionRole> {
|
|
191
|
+
return this.#sliceActionRoles;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
get sliceStateRoles(): ReadonlyMap<string, SliceStateRole> {
|
|
195
|
+
return this.#sliceStateRoles;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Keys the store materializes from a computation, the URL, or storage. `set` throws on them. */
|
|
199
|
+
get derivedKeys(): ReadonlySet<string> {
|
|
200
|
+
return this.#derivedMeta.derivedKeys;
|
|
201
|
+
}
|
|
202
|
+
|
|
175
203
|
constructor(store?: RootStoreCls) {
|
|
176
204
|
if (store) this.addStore(store);
|
|
177
205
|
}
|
|
@@ -189,6 +217,7 @@ export class StoreInstance {
|
|
|
189
217
|
hasNewStateKey = true;
|
|
190
218
|
}
|
|
191
219
|
if (hasNewStateKey) this.#state = nextState;
|
|
220
|
+
for (const [key, refName] of Object.entries(store[ACTION_OWNER_META] ?? {})) this.#actionOwners.set(key, refName);
|
|
192
221
|
this.#mergeActions(store[ACTION_META]);
|
|
193
222
|
this.#extendAccessors(derivedState, store[ACTION_META]);
|
|
194
223
|
this.#buildSlices(store);
|
|
@@ -199,6 +228,7 @@ export class StoreInstance {
|
|
|
199
228
|
#mergeActions(actions: { [key: string]: StoreAction }) {
|
|
200
229
|
for (const [k, method] of Object.entries(actions)) {
|
|
201
230
|
this.#ctx[k] = (...args: unknown[]) => method.call(this.#ctx, ...args);
|
|
231
|
+
this.#actionArity.set(k, method.length);
|
|
202
232
|
}
|
|
203
233
|
}
|
|
204
234
|
|
|
@@ -208,11 +238,11 @@ export class StoreInstance {
|
|
|
208
238
|
this.use[k] = () => this.sel((s) => s[k]);
|
|
209
239
|
if (this.#derivedMeta.derivedKeys.has(k)) continue;
|
|
210
240
|
const setKey = `set${capitalize(k)}`;
|
|
211
|
-
this.do[setKey] = (value: unknown) => this.set({ [k]: value });
|
|
241
|
+
this.do[setKey] = tagAction((value: unknown) => this.set({ [k]: value }), { action: setKey, state: k });
|
|
212
242
|
}
|
|
213
243
|
}
|
|
214
244
|
for (const k of Object.keys(actions)) {
|
|
215
|
-
|
|
245
|
+
const dispatch = async (...args: unknown[]) => {
|
|
216
246
|
Logger.verbose(`${k} action loading...`);
|
|
217
247
|
const start = Date.now();
|
|
218
248
|
try {
|
|
@@ -225,6 +255,8 @@ export class StoreInstance {
|
|
|
225
255
|
throw error;
|
|
226
256
|
}
|
|
227
257
|
};
|
|
258
|
+
|
|
259
|
+
this.do[k] = tagAction(dispatch, actionTagOf(actions[k]) ?? { action: k });
|
|
228
260
|
}
|
|
229
261
|
}
|
|
230
262
|
|
|
@@ -254,7 +286,7 @@ export class StoreInstance {
|
|
|
254
286
|
});
|
|
255
287
|
}
|
|
256
288
|
|
|
257
|
-
#buildSlice(refName: string, sliceName: string, serializedSlice: { args?:
|
|
289
|
+
#buildSlice(refName: string, sliceName: string, serializedSlice: { args?: SerializedArg[] }) {
|
|
258
290
|
const [fieldName, className] = [refName, capitalize(refName)];
|
|
259
291
|
const names: { [key in SliceStateKey | SliceActionKey | "model" | "Model"]: string } = {
|
|
260
292
|
model: fieldName,
|
|
@@ -324,14 +356,20 @@ export class StoreInstance {
|
|
|
324
356
|
argLength: serializedSlice.args?.length ?? 0,
|
|
325
357
|
};
|
|
326
358
|
|
|
359
|
+
const args = serializedSlice.args ?? [];
|
|
327
360
|
for (const key of Object.keys(namesOfSliceAction) as SliceActionKey[]) {
|
|
328
361
|
const rootActionKey = namesOfSliceAction[key];
|
|
329
|
-
if (this.do[rootActionKey])
|
|
362
|
+
if (!this.do[rootActionKey]) continue;
|
|
363
|
+
targetSlice.do[names[key]] = this.do[rootActionKey];
|
|
364
|
+
this.#sliceActionRoles.set(rootActionKey, { role: key, refName, sliceName, args });
|
|
330
365
|
}
|
|
331
366
|
|
|
332
367
|
for (const key of Object.keys(namesOfSliceState) as SliceStateKey[]) {
|
|
333
368
|
const rootStateKey = namesOfSliceState[key];
|
|
334
|
-
if (this.use[rootStateKey])
|
|
369
|
+
if (this.use[rootStateKey]) {
|
|
370
|
+
targetSlice.use[names[key]] = this.use[rootStateKey];
|
|
371
|
+
this.#sliceStateRoles.set(rootStateKey, { role: key, refName, sliceName });
|
|
372
|
+
}
|
|
335
373
|
const setRootKey = `set${capitalize(rootStateKey)}`;
|
|
336
374
|
const setLocalKey = `set${capitalize(names[key])}`;
|
|
337
375
|
if (this.do[setRootKey]) targetSlice.do[setLocalKey] = this.do[setRootKey];
|
package/store/storeRegistry.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACTION_META,
|
|
3
|
+
ACTION_OWNER_META,
|
|
3
4
|
type MergeAllKeyOfObjects,
|
|
4
5
|
type MergeAllKeyOfTypes,
|
|
5
6
|
type MergeAllTypes,
|
|
@@ -37,15 +38,23 @@ function getStoreRegistryState(): StoreRegistryState {
|
|
|
37
38
|
|
|
38
39
|
export class StoreRegistry {
|
|
39
40
|
static #state = getStoreRegistryState();
|
|
41
|
+
/** The one store every `st.use` / `st.do` in the process goes through. What an agent bridge drives. */
|
|
42
|
+
static get instance(): StoreInstance {
|
|
43
|
+
return StoreRegistry.#state.instance;
|
|
44
|
+
}
|
|
40
45
|
static register<StrCls extends StoreCls>(store: StrCls): StrCls {
|
|
41
46
|
const parentStore = Object.getPrototypeOf(store) as StoreCls | null;
|
|
42
47
|
const actions = { ...(parentStore?.[ACTION_META] ?? {}) };
|
|
48
|
+
|
|
49
|
+
const owners = { ...(parentStore?.[ACTION_OWNER_META] ?? {}) };
|
|
43
50
|
Object.entries(Object.getOwnPropertyDescriptors(store.prototype)).forEach(([key, descriptor]) => {
|
|
44
51
|
if (key === "constructor") return;
|
|
45
52
|
if (!descriptor.value || typeof descriptor.value !== "function") return;
|
|
46
53
|
actions[key] = descriptor.value;
|
|
54
|
+
owners[key] = store.refName;
|
|
47
55
|
});
|
|
48
56
|
store[ACTION_META] = actions;
|
|
57
|
+
store[ACTION_OWNER_META] = owners;
|
|
49
58
|
StoreRegistry.#state.store.set(store.refName, store);
|
|
50
59
|
return store;
|
|
51
60
|
}
|
|
@@ -69,6 +78,7 @@ export class StoreRegistry {
|
|
|
69
78
|
static [STATE_INIT_META] = Object.assign({}, ...stores.map((store) => store[STATE_INIT_META]));
|
|
70
79
|
static [STATE_DERIVED_META] = mergeDerivedMeta(...stores.map((store) => store[STATE_DERIVED_META]));
|
|
71
80
|
static [ACTION_META] = Object.assign({}, ...stores.map((store) => store[ACTION_META]));
|
|
81
|
+
static [ACTION_OWNER_META] = Object.assign({}, ...stores.map((store) => store[ACTION_OWNER_META] ?? {}));
|
|
72
82
|
static slice: { [key: string]: { [key: string]: SerializedSlice } } = {};
|
|
73
83
|
}
|
|
74
84
|
stores.forEach((store) => {
|
package/types/base/symbols.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare const STATE_META: unique symbol;
|
|
|
13
13
|
export declare const STATE_INIT_META: unique symbol;
|
|
14
14
|
export declare const STATE_DERIVED_META: unique symbol;
|
|
15
15
|
export declare const ACTION_META: unique symbol;
|
|
16
|
+
/** Which module declared each action, which is the dictionary node its words are written in. */
|
|
17
|
+
export declare const ACTION_OWNER_META: unique symbol;
|
|
18
|
+
/** What a dispatcher does, carried on the function so a component handed one can annotate the DOM with it. */
|
|
19
|
+
export declare const ACTION_TAG: unique symbol;
|
|
16
20
|
export declare const SERVER_VALUE: unique symbol;
|
|
17
21
|
export declare const CLIENT_VALUE: unique symbol;
|
|
18
22
|
export declare const DEFAULT_VALUE: unique symbol;
|
|
@@ -5,6 +5,7 @@ export * from "./deserialize.d.ts";
|
|
|
5
5
|
export * from "./fieldInfo.d.ts";
|
|
6
6
|
export * from "./getDefault.d.ts";
|
|
7
7
|
export * from "./immerify.d.ts";
|
|
8
|
+
export * from "./mask.d.ts";
|
|
8
9
|
export * from "./purify.d.ts";
|
|
9
10
|
export * from "./serialize.d.ts";
|
|
10
11
|
export * from "./textFieldPathSet.d.ts";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A model as masking reads it — the constructor, for the field metadata it carries at runtime.
|
|
3
|
+
*
|
|
4
|
+
* Structural rather than `ConstantModelRef` so that anything holding the class can name it, and read through
|
|
5
|
+
* `FIELD_META` the way `resolveReturn` reads it.
|
|
6
|
+
*/
|
|
7
|
+
export interface MaskModel {
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
/** The part of a field's metadata masking turns on. Mirrors what `resolveReturn` branches over. */
|
|
11
|
+
interface MaskField {
|
|
12
|
+
fieldType?: string;
|
|
13
|
+
isClass?: boolean;
|
|
14
|
+
modelRef?: MaskModel;
|
|
15
|
+
}
|
|
16
|
+
export declare const maskFieldsOf: (model: MaskModel) => Record<string, MaskField> | null;
|
|
17
|
+
/** The `hidden` and `secret` field names of `model` that `value` still carries populated. */
|
|
18
|
+
export declare const leakingFieldsOf: (model: MaskModel, value: Record<string, unknown>) => string[];
|
|
19
|
+
/**
|
|
20
|
+
* Strips what a model marks `hidden` or `secret`, by the model the caller names rather than by the one the value
|
|
21
|
+
* happens to still carry.
|
|
22
|
+
*
|
|
23
|
+
* That distinction is the whole point. A check that reads the class off the value can only mask what arrives as an
|
|
24
|
+
* instance, so a `{ ...doc }` spread, a `toJSON()`, an `immerify()`, or a round-trip through `JSON.stringify` reaches
|
|
25
|
+
* its destination with the metadata already gone and nothing can be done about it. A named model is metadata the
|
|
26
|
+
* value cannot lose, so a hydrated document and a plain object copied out of one mask identically.
|
|
27
|
+
*
|
|
28
|
+
* This is the field half of `resolveReturn` and deliberately not the whole of it. That one also loads every relation
|
|
29
|
+
* it walks past, which is right for a query's return value and wrong here, where the value is already in hand.
|
|
30
|
+
*
|
|
31
|
+
* Returns `unknown` rather than the argument's type, because what comes back is missing fields that type promises.
|
|
32
|
+
*/
|
|
33
|
+
export declare const mask: (model: MaskModel, value: unknown) => unknown;
|
|
34
|
+
export {};
|
|
@@ -180,8 +180,15 @@ export declare class ModelDictInfo<Languages extends [string, ...string[]] = [st
|
|
|
180
180
|
[K in Enum["value"]]: FieldTranslation<Languages>;
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Every parameter of `ModelDictInfo` is listed here positionally, so a parameter added to the class has to be
|
|
185
|
+
* added to all three lists below in the same slot. Omitting one does not fail to compile — inference silently
|
|
186
|
+
* shifts, so the last parameter falls off the end and becomes its default `never`: adding `StoreKey` before
|
|
187
|
+
* `ErrorKey` once cost an extending app the whole of the lib's `EtcKey` (`.translate()`) union, which reads at
|
|
188
|
+
* the call site as `l("<model>.<key>")` no longer existing.
|
|
189
|
+
*/
|
|
190
|
+
type AnyModelDictInfo = ModelDictInfo<any, any, any, any, any, any, any, any, any, any, any, any>;
|
|
191
|
+
type MergeTwoModelDicts<ModelDict1, ModelDict2> = ModelDict1 extends ModelDictInfo<infer Languages1, infer ModelKey1, infer InsightKey1, infer QueryKey1, infer SortKey1, infer EnumKey1, infer BaseSignalKey1, infer SliceKey1, infer EndpointKey1, infer StoreKey1, infer ErrorKey1, infer EtcKey1> ? ModelDict2 extends ModelDictInfo<infer _Languages2, infer ModelKey2, infer InsightKey2, infer QueryKey2, infer SortKey2, infer EnumKey2, infer BaseSignalKey2, infer SliceKey2, infer EndpointKey2, infer StoreKey2, infer ErrorKey2, infer EtcKey2> ? ModelDictInfo<Languages1, ModelKey1 | ModelKey2, InsightKey1 | InsightKey2, QueryKey1 | QueryKey2, SortKey1 | SortKey2, EnumKey1 | EnumKey2, BaseSignalKey1 | BaseSignalKey2, SliceKey1 | SliceKey2, EndpointKey1 | EndpointKey2, StoreKey1 | StoreKey2, ErrorKey1 | ErrorKey2, EtcKey1 | EtcKey2> : ModelDict1 : never;
|
|
185
192
|
type MergeModelDicts<ModelDicts extends AnyModelDictInfo[]> = ModelDicts extends [
|
|
186
193
|
infer First extends AnyModelDictInfo,
|
|
187
194
|
...infer Rest extends AnyModelDictInfo[]
|
|
@@ -34,6 +34,15 @@ export declare class FetchClient {
|
|
|
34
34
|
constructor(origin: string, handler?: Record<string, FetchHandler>, serializedSignal?: {
|
|
35
35
|
[key: string]: SerializedSignal;
|
|
36
36
|
}, ErrorCls?: ErrorConstructor | undefined);
|
|
37
|
+
/**
|
|
38
|
+
* Every signal any client in this process has applied, which is the whole callable surface of the app.
|
|
39
|
+
*
|
|
40
|
+
* A copy, because this is the registry each client merges its own signals into and a reader that mutated it
|
|
41
|
+
* would change what the next client applies. Read by the agent catalogue, which needs the argument schemas.
|
|
42
|
+
*/
|
|
43
|
+
static get sharedSerializedSignal(): {
|
|
44
|
+
[key: string]: SerializedSignal;
|
|
45
|
+
};
|
|
37
46
|
static resetSharedRegistry(): void;
|
|
38
47
|
static resetSharedClient(): void;
|
|
39
48
|
setErrorConstructor(ErrorCls?: ErrorConstructor): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AkanSqlClient } from "./database.adaptor";
|
|
2
|
+
export interface InsightQueryOptions {
|
|
3
|
+
/** Rows to return at most. Clamped to `InsightQuery.maxRows`, which no caller can raise. */
|
|
4
|
+
limit?: number;
|
|
5
|
+
/** How long to wait for the driver, in ms. See the note on `#raced` for which dialects this can actually stop. */
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface InsightQueryResult {
|
|
9
|
+
columns: string[];
|
|
10
|
+
rows: Record<string, unknown>[];
|
|
11
|
+
/** The ceiling cut the answer short, so the caller knows not to read it as complete. */
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One read-only SQL statement, for an agent or an operator asking a question the domain endpoints cannot express.
|
|
16
|
+
*
|
|
17
|
+
* This is the layer-bypassing read, and every safeguard the framework has is bypassed with it — guards, soft delete,
|
|
18
|
+
* cascade, `_postRemove`, and the `hidden`/`secret` masking every other response path performs. So it is read-only
|
|
19
|
+
* by construction rather than by convention, and it is deliberately *not* wired to an endpoint here: the framework
|
|
20
|
+
* owns no guard strong enough to sit in front of it. An app that wants it writes the endpoint with its own
|
|
21
|
+
* `SuperAdmin`, the same way guards ship with the library that owns the model.
|
|
22
|
+
*
|
|
23
|
+
* Three things enforce read-only, and only the third is ours:
|
|
24
|
+
*
|
|
25
|
+
* 1. The statement is wrapped as a derived table — `SELECT * FROM (<sql>) AS "akanInsight" LIMIT ?`. Nothing but a
|
|
26
|
+
* query is legal in that position, in either dialect, so a write is a syntax error from the engine rather than a
|
|
27
|
+
* pattern this code had to recognise. A second statement smuggled behind `;` is a syntax error for the same
|
|
28
|
+
* reason, and the row ceiling rides along on the same wrapper.
|
|
29
|
+
* 2. A rejection before execution, so the caller reads why rather than a syntax error. It runs on the statement with
|
|
30
|
+
* comments and string literals removed, because that is what makes `-- ` and `'…'` unable to hide anything.
|
|
31
|
+
* 3. **`_doc` never crosses the boundary.** Every non-base field lives in that one JSON column, which is where the
|
|
32
|
+
* plan's "re-apply schema-based masking" runs into the fact that an arbitrary SELECT has no model to mask by. So
|
|
33
|
+
* the enforceable rule is the column itself: unnameable in the statement, dropped from the rows, and any cell
|
|
34
|
+
* that still arrives holding a JSON object or array is refused. An insight is made of scalars; a value that is
|
|
35
|
+
* not one is either a document or indistinguishable from it.
|
|
36
|
+
*
|
|
37
|
+
* What that costs is real and worth saying: this answers "how many, since when, grouped how" over base columns and
|
|
38
|
+
* the search mirror, and it cannot read a domain field. Field-level reads go through the domain tools, which mask.
|
|
39
|
+
*/
|
|
40
|
+
export declare class InsightQuery {
|
|
41
|
+
#private;
|
|
42
|
+
/** Not an option. A caller asking for more gets this, because the point is that no caller sets the ceiling. */
|
|
43
|
+
static readonly maxRows = 1000;
|
|
44
|
+
constructor(client: AkanSqlClient);
|
|
45
|
+
run(sql: string, { limit, timeoutMs }?: InsightQueryOptions): Promise<{
|
|
46
|
+
columns: string[];
|
|
47
|
+
rows: Record<string, unknown>[];
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MaskModel } from "akanjs/constant";
|
|
1
2
|
import type { JsonSchema } from "../schema.d.ts";
|
|
2
3
|
export type PromptRole = "user" | "assistant";
|
|
3
4
|
/**
|
|
@@ -60,11 +61,9 @@ export interface PromptFileSource {
|
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
63
|
* A model class, named by the caller so an attachment can be masked by what it *is* rather than by what it still
|
|
63
|
-
* carries at runtime.
|
|
64
|
+
* carries at runtime. The same model any other audience masks by — see `mask` in `akanjs/constant`.
|
|
64
65
|
*/
|
|
65
|
-
export
|
|
66
|
-
name: string;
|
|
67
|
-
}
|
|
66
|
+
export type PromptModel = MaskModel;
|
|
68
67
|
/**
|
|
69
68
|
* Builds the messages a `prompt()` endpoint returns.
|
|
70
69
|
*
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ActionTag {
|
|
2
|
+
/** The `st.do` key this function is. */
|
|
3
|
+
action: string;
|
|
4
|
+
/** The state path it writes, when it writes exactly one — `userForm.name` for a field setter. */
|
|
5
|
+
state?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Marks a dispatcher with what it does, so a component handed one by reference can say so in the DOM.
|
|
9
|
+
*
|
|
10
|
+
* `onChange={st.do.setNameOnUser}` is the house form for every model field, which means the component already holds
|
|
11
|
+
* everything an annotation needs — it just has no way to read it off a function. This is that way, and it is why
|
|
12
|
+
* `data-akan-*` costs an app no code at all: nobody writes the attribute, the setter carries its own name.
|
|
13
|
+
*
|
|
14
|
+
* Non-enumerable, so it survives neither `{...fn}` nor `JSON.stringify` and shows up in no spread.
|
|
15
|
+
*/
|
|
16
|
+
export declare const tagAction: <T extends (...args: never[]) => unknown>(fn: T, tag: ActionTag) => T;
|
|
17
|
+
export declare const actionTagOf: (value: unknown) => ActionTag | undefined;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AgentRefusal, AgentUndescribed, JsonSchema, SerializedSignal } from "akanjs/signal";
|
|
2
|
+
import type { StoreInstance } from "../storeInstance.d.ts";
|
|
3
|
+
import type { SerializedStoreState, StoreActionEffect } from "./types.d.ts";
|
|
4
|
+
export interface AgentTool {
|
|
5
|
+
name: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
/** One flat named object, the shape MCP publishes. The bridge maps it onto the action's positional parameters. */
|
|
9
|
+
inputSchema: JsonSchema;
|
|
10
|
+
effect: StoreActionEffect;
|
|
11
|
+
}
|
|
12
|
+
/** One call the agent made, in the order it made them. */
|
|
13
|
+
export interface AgentCall {
|
|
14
|
+
name: string;
|
|
15
|
+
args: Record<string, unknown>;
|
|
16
|
+
at: Date;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface AgentBridgeOptions {
|
|
20
|
+
/** Resolves a dictionary key to its text. Defaults to the seeded `Translator` in the active locale. */
|
|
21
|
+
resolveDescription?: (key: string) => string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What an in-page agent may do to the app the user is looking at.
|
|
25
|
+
*
|
|
26
|
+
* Every call goes through `st.do`, which is the same single dispatch point a click goes through — so the agent
|
|
27
|
+
* cannot reach past what the UI already lets this user do, the app re-renders from the write, and the user watches
|
|
28
|
+
* the result rather than being told about it. That is why the exposure default here is the opposite of the MCP
|
|
29
|
+
* catalogue's: an external agent's `tools/list` is an attack surface built out of names the operator never chose to
|
|
30
|
+
* publish, while this one is the user's own session, under their own credential, with them watching.
|
|
31
|
+
*
|
|
32
|
+
* It is deliberately not an agent. There is no model, no provider, and no key here — an app wires whichever it uses
|
|
33
|
+
* to `tools`, `call`, and `read`. The framework's half is the catalogue, the argument checking, the masking, and the
|
|
34
|
+
* transcript; the conversation is the app's.
|
|
35
|
+
*/
|
|
36
|
+
export declare class AgentBridge {
|
|
37
|
+
#private;
|
|
38
|
+
readonly tools: AgentTool[];
|
|
39
|
+
readonly refusals: AgentRefusal[];
|
|
40
|
+
/** Published entries with no words an author wrote. What a source scanner cannot see, per `AgentCatalogue`. */
|
|
41
|
+
readonly undescribed: AgentUndescribed[];
|
|
42
|
+
/**
|
|
43
|
+
* The bridge for the app running in this process: the one store every `st.do` goes through, and every signal any
|
|
44
|
+
* client has applied. An app needs no arguments to reach its own agent surface.
|
|
45
|
+
*/
|
|
46
|
+
static of(options?: AgentBridgeOptions): AgentBridge;
|
|
47
|
+
constructor(instance: StoreInstance, serializedSignal: Record<string, SerializedSignal>, options?: AgentBridgeOptions);
|
|
48
|
+
get state(): {
|
|
49
|
+
[key: string]: SerializedStoreState;
|
|
50
|
+
};
|
|
51
|
+
get transcript(): readonly AgentCall[];
|
|
52
|
+
subscribe(listener: () => void): () => void;
|
|
53
|
+
/**
|
|
54
|
+
* The value behind a state key, stripped of what the model marks `hidden` or `secret`.
|
|
55
|
+
*
|
|
56
|
+
* Masking is not optional here even though the data mostly came from the server already masked: `<model>Form`
|
|
57
|
+
* holds what the *user* typed, credentials included, and an in-page agent ships what it reads to a remote model.
|
|
58
|
+
* The mask is by the declared model rather than by the value's class, because `immerify` copies a form into a
|
|
59
|
+
* plain object and the class is gone by the time anyone can ask.
|
|
60
|
+
*/
|
|
61
|
+
read(key: string): unknown;
|
|
62
|
+
/**
|
|
63
|
+
* Dispatches through `st.do`, so what happens is what happens when the user clicks.
|
|
64
|
+
*
|
|
65
|
+
* Arguments arrive named and are mapped onto the action's parameters in declared order. An omitted optional one
|
|
66
|
+
* becomes `null`, which is what the slice query builders already expect; an omitted required one is refused,
|
|
67
|
+
* because the alternative is a call that writes `undefined` into state and reports success.
|
|
68
|
+
*/
|
|
69
|
+
call(name: string, args?: Record<string, unknown>): Promise<void>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentRefusal, SerializedSignal } from "akanjs/signal";
|
|
2
|
+
import type { StoreInstance } from "../storeInstance.d.ts";
|
|
3
|
+
import type { SerializedStore } from "./types.d.ts";
|
|
4
|
+
/**
|
|
5
|
+
* What one built store offers an agent, derived from the store the browser is already running.
|
|
6
|
+
*
|
|
7
|
+
* The store is the audience-neutral half of the client surface the way a signal registry is the server's: a key on
|
|
8
|
+
* `st.do` is the same call the user's own click makes, so an agent that drives it cannot reach past what the UI
|
|
9
|
+
* already permits. That is why the default here is the opposite of the MCP catalogue's — every key is published
|
|
10
|
+
* unless something about it cannot be described, and each of those is recorded as a refusal rather than dropped.
|
|
11
|
+
*
|
|
12
|
+
* Nothing is re-declared. An action's arguments come from the endpoint it is named after, from the field metadata
|
|
13
|
+
* the form setter was generated from, or from the role the store recorded while building the slice; a key that
|
|
14
|
+
* matches none of those three is published only when it takes no arguments at all.
|
|
15
|
+
*/
|
|
16
|
+
export declare class StoreCatalogue {
|
|
17
|
+
#private;
|
|
18
|
+
readonly store: SerializedStore;
|
|
19
|
+
readonly refusals: AgentRefusal[];
|
|
20
|
+
constructor(instance: StoreInstance, serializedSignal: Record<string, SerializedSignal>);
|
|
21
|
+
}
|