@intent-framework/core 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +67 -0
- package/dist/graph.d.ts +12 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +263 -33
- package/dist/registry.d.ts +1 -0
- package/dist/resource.d.ts +10 -1
- package/dist/screen.d.ts +2 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @intent-framework/core
|
|
2
|
+
|
|
3
|
+
Platformless semantic graph and runtime for Intent applications.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @intent-framework/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @intent-framework/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What it provides
|
|
16
|
+
|
|
17
|
+
- `screen()` — define a semantic interaction space
|
|
18
|
+
- `$.state.text()` / `$.state.boolean()` / `$.state.choice()` — reactive state
|
|
19
|
+
- `$.ask()` — user-facing question with validation
|
|
20
|
+
- `$.act()` — executable action with conditions, lifecycle, and feedback
|
|
21
|
+
- `$.resource()` — async state with load/reload lifecycle
|
|
22
|
+
- `$.surface()` — named containment surface
|
|
23
|
+
- `createScreenRuntime()` — runtime that owns screen state and resources
|
|
24
|
+
- `inspectScreen()` — semantic graph snapshot with diagnostics
|
|
25
|
+
- Condition and signal primitives
|
|
26
|
+
|
|
27
|
+
## Minimal example
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { screen, inspectScreen } from "@intent-framework/core"
|
|
31
|
+
|
|
32
|
+
const InviteMember = screen("InviteMember", $ => {
|
|
33
|
+
const email = $.state.text("email")
|
|
34
|
+
|
|
35
|
+
const emailAsk = $.ask("Email", email)
|
|
36
|
+
.required("Email is required")
|
|
37
|
+
.validate(value => value.includes("@") ? true : "Enter a valid email")
|
|
38
|
+
|
|
39
|
+
const invite = $.act("Invite member")
|
|
40
|
+
.primary()
|
|
41
|
+
.when(emailAsk.valid, "Enter a valid email first")
|
|
42
|
+
.does(() => {
|
|
43
|
+
console.log("invite", email.value)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
$.surface("main").contains(emailAsk, invite)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const graph = inspectScreen(InviteMember)
|
|
50
|
+
console.log(graph.diagnostics)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Where this fits
|
|
54
|
+
|
|
55
|
+
Core defines the product graph. It has no DOM, React, Node, or framework dependencies. Renderers (`@intent-framework/dom`), the router (`@intent-framework/router`), and testing (`@intent-framework/testing`) all build on core.
|
|
56
|
+
|
|
57
|
+
## Learn more
|
|
58
|
+
|
|
59
|
+
- [Root README](../../README.md) — project overview and philosophy
|
|
60
|
+
- [Quickstart](../../docs/Quickstart.md) — step-by-step guide
|
|
61
|
+
- [Inspect Screen and Diagnostics Guide](../../docs/Inspect-Screen.md) — graph inspection and diagnostics
|
|
62
|
+
- [Resources Guide](../../docs/Resources.md) — resource lifecycle and runtime scoping
|
|
63
|
+
- [MVP Checkpoint](../../docs/MVP-Checkpoint.md) — current implementation boundaries
|
|
64
|
+
|
|
65
|
+
## Status
|
|
66
|
+
|
|
67
|
+
Experimental alpha. APIs may change. Not recommended for production use.
|
package/dist/graph.d.ts
CHANGED
|
@@ -2,16 +2,24 @@ import type { ScreenDefinition } from "./screen.js";
|
|
|
2
2
|
import type { DefaultScreenServices } from "./act.js";
|
|
3
3
|
import type { AnyResourceNode } from "./resource.js";
|
|
4
4
|
export type DiagnosticSeverity = "info" | "warning" | "error";
|
|
5
|
+
export type FlowDiagnosticMeta = {
|
|
6
|
+
flowNodeId: string;
|
|
7
|
+
flowSemanticNodeId?: string;
|
|
8
|
+
};
|
|
5
9
|
export type GraphDiagnostic = {
|
|
6
10
|
severity: DiagnosticSeverity;
|
|
7
11
|
code: string;
|
|
8
12
|
message: string;
|
|
9
13
|
nodeId?: string;
|
|
14
|
+
semanticNodeId?: string;
|
|
15
|
+
flow?: FlowDiagnosticMeta;
|
|
10
16
|
};
|
|
11
17
|
export type InspectedScreen = {
|
|
12
18
|
name: string;
|
|
19
|
+
semanticId: string;
|
|
13
20
|
asks: Array<{
|
|
14
21
|
id: string;
|
|
22
|
+
semanticId: string;
|
|
15
23
|
label: string;
|
|
16
24
|
kind: string;
|
|
17
25
|
required: boolean;
|
|
@@ -21,6 +29,7 @@ export type InspectedScreen = {
|
|
|
21
29
|
}>;
|
|
22
30
|
acts: Array<{
|
|
23
31
|
id: string;
|
|
32
|
+
semanticId: string;
|
|
24
33
|
label: string;
|
|
25
34
|
primary: boolean;
|
|
26
35
|
enabled: boolean;
|
|
@@ -31,16 +40,19 @@ export type InspectedScreen = {
|
|
|
31
40
|
}>;
|
|
32
41
|
flows: Array<{
|
|
33
42
|
id: string;
|
|
43
|
+
semanticId: string;
|
|
34
44
|
name: string;
|
|
35
45
|
stepCount: number;
|
|
36
46
|
}>;
|
|
37
47
|
surfaces: Array<{
|
|
38
48
|
id: string;
|
|
49
|
+
semanticId: string;
|
|
39
50
|
name: string;
|
|
40
51
|
itemCount: number;
|
|
41
52
|
}>;
|
|
42
53
|
resources: Array<{
|
|
43
54
|
id: string;
|
|
55
|
+
semanticId: string;
|
|
44
56
|
name: string;
|
|
45
57
|
status: string;
|
|
46
58
|
hasValue: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { screen } from "./screen.js";
|
|
2
2
|
export type { ScreenDefinition, ScreenBuilder } from "./screen.js";
|
|
3
3
|
export { inspectScreen } from "./graph.js";
|
|
4
|
-
export type { InspectedScreen, GraphDiagnostic, DiagnosticSeverity } from "./graph.js";
|
|
4
|
+
export type { InspectedScreen, GraphDiagnostic, DiagnosticSeverity, FlowDiagnosticMeta } from "./graph.js";
|
|
5
5
|
export type { TextState, BooleanState, ChoiceState } from "./state.js";
|
|
6
6
|
export type { AskNode, AnyAskNode, AskKind, AskBuilder } from "./ask.js";
|
|
7
7
|
export type { ActNode, ActCondition, ActStatus, FeedbackConfig, ActBuilder, NavigationService, ActionExecutionContext, DefaultScreenServices } from "./act.js";
|
|
8
8
|
export type { FlowNode, FlowStep, FlowBuilder } from "./flow.js";
|
|
9
9
|
export type { SurfaceNode, SurfaceBuilder } from "./surface.js";
|
|
10
|
-
export type { ResourceNode, ResourceConfig, ResourceLoadContext, ResourceStatus, AnyResourceNode } from "./resource.js";
|
|
10
|
+
export type { ResourceNode, ResourceConfig, ResourceCacheOptions, ResourceLoadContext, ResourceStatus, ResourceKey, AnyResourceNode } from "./resource.js";
|
|
11
11
|
export { ResourceRef, createResourceNode } from "./resource.js";
|
|
12
12
|
export { createScreenRuntime } from "./runtime.js";
|
|
13
13
|
export type { ScreenRuntime } from "./runtime.js";
|
package/dist/index.js
CHANGED
|
@@ -37,9 +37,25 @@ function signal(initial) {
|
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region src/resource.ts
|
|
40
|
-
function createResourceNode(id, name, loader, autoLoad = true) {
|
|
40
|
+
function createResourceNode(id, name, loader, autoLoad = true, cache) {
|
|
41
41
|
const statusSignal = signal(0);
|
|
42
42
|
const staleSignal = signal(0);
|
|
43
|
+
const hasKey = typeof cache?.key === "function";
|
|
44
|
+
const DEFAULT_KEY = "";
|
|
45
|
+
function createEntry() {
|
|
46
|
+
return {
|
|
47
|
+
value: void 0,
|
|
48
|
+
status: "idle",
|
|
49
|
+
error: void 0,
|
|
50
|
+
stale: false,
|
|
51
|
+
staleTimer: null,
|
|
52
|
+
cacheTimer: null,
|
|
53
|
+
inFlightPromise: null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const entries = /* @__PURE__ */ new Map();
|
|
57
|
+
let _activeKey = DEFAULT_KEY;
|
|
58
|
+
if (!hasKey) entries.set(DEFAULT_KEY, createEntry());
|
|
43
59
|
let currentStatus = "idle";
|
|
44
60
|
let currentValue = void 0;
|
|
45
61
|
let currentError = void 0;
|
|
@@ -47,10 +63,94 @@ function createResourceNode(id, name, loader, autoLoad = true) {
|
|
|
47
63
|
let lastContext = void 0;
|
|
48
64
|
const notify = () => statusSignal.set(statusSignal.get() + 1);
|
|
49
65
|
const staleNotify = () => staleSignal.set(staleSignal.get() + 1);
|
|
66
|
+
const shouldDeduplicate = cache ? cache.deduplicate !== false : false;
|
|
50
67
|
let _ready;
|
|
51
68
|
let _pending;
|
|
52
69
|
let _failed;
|
|
53
70
|
let _stale;
|
|
71
|
+
function getActiveEntry() {
|
|
72
|
+
let entry = entries.get(_activeKey);
|
|
73
|
+
if (!entry) {
|
|
74
|
+
entry = createEntry();
|
|
75
|
+
entries.set(_activeKey, entry);
|
|
76
|
+
}
|
|
77
|
+
return entry;
|
|
78
|
+
}
|
|
79
|
+
function syncFromEntry(entry) {
|
|
80
|
+
currentStatus = entry.status;
|
|
81
|
+
currentValue = entry.value;
|
|
82
|
+
currentError = entry.error;
|
|
83
|
+
if (currentStale !== entry.stale) {
|
|
84
|
+
currentStale = entry.stale;
|
|
85
|
+
staleNotify();
|
|
86
|
+
}
|
|
87
|
+
notify();
|
|
88
|
+
}
|
|
89
|
+
function syncFromActiveEntry() {
|
|
90
|
+
syncFromEntry(getActiveEntry());
|
|
91
|
+
}
|
|
92
|
+
function encodeResourceKey(key) {
|
|
93
|
+
if (Array.isArray(key)) return ["array", key.map(encodeResourceKey)];
|
|
94
|
+
if (key === null) return ["null"];
|
|
95
|
+
if (key === void 0) return ["undefined"];
|
|
96
|
+
if (typeof key === "string") return ["string", key];
|
|
97
|
+
if (typeof key === "boolean") return ["boolean", key];
|
|
98
|
+
if (typeof key === "number") {
|
|
99
|
+
if (Number.isNaN(key)) return ["number", "NaN"];
|
|
100
|
+
if (key === Infinity) return ["number", "Infinity"];
|
|
101
|
+
if (key === -Infinity) return ["number", "-Infinity"];
|
|
102
|
+
if (Object.is(key, -0)) return ["number", "-0"];
|
|
103
|
+
return ["number", key];
|
|
104
|
+
}
|
|
105
|
+
const exhaustive = key;
|
|
106
|
+
return exhaustive;
|
|
107
|
+
}
|
|
108
|
+
function normalizeKey(key) {
|
|
109
|
+
return JSON.stringify(encodeResourceKey(key));
|
|
110
|
+
}
|
|
111
|
+
function resolveKey(ctx) {
|
|
112
|
+
if (!hasKey) return DEFAULT_KEY;
|
|
113
|
+
const context = ctx ?? lastContext ?? {};
|
|
114
|
+
return normalizeKey(cache.key(context));
|
|
115
|
+
}
|
|
116
|
+
function _clearEntryStaleTimer(entry) {
|
|
117
|
+
if (entry.staleTimer != null) {
|
|
118
|
+
clearTimeout(entry.staleTimer);
|
|
119
|
+
entry.staleTimer = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function _startEntryStaleTimer(entry, key) {
|
|
123
|
+
_clearEntryStaleTimer(entry);
|
|
124
|
+
if (cache?.staleTime != null && isFinite(cache.staleTime)) entry.staleTimer = setTimeout(() => {
|
|
125
|
+
if (!entry.stale) {
|
|
126
|
+
entry.stale = true;
|
|
127
|
+
_startEntryCacheTimer(entry, key);
|
|
128
|
+
if (entry === getActiveEntry()) {
|
|
129
|
+
currentStale = true;
|
|
130
|
+
staleNotify();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, cache.staleTime);
|
|
134
|
+
}
|
|
135
|
+
function _clearEntryCacheTimer(entry) {
|
|
136
|
+
if (entry.cacheTimer != null) {
|
|
137
|
+
clearTimeout(entry.cacheTimer);
|
|
138
|
+
entry.cacheTimer = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function _startEntryCacheTimer(entry, key) {
|
|
142
|
+
_clearEntryCacheTimer(entry);
|
|
143
|
+
if (cache?.cacheTime != null && isFinite(cache.cacheTime)) entry.cacheTimer = setTimeout(() => {
|
|
144
|
+
if (entry === getActiveEntry()) {
|
|
145
|
+
entry.value = void 0;
|
|
146
|
+
entry.error = void 0;
|
|
147
|
+
entry.status = "idle";
|
|
148
|
+
entry.stale = false;
|
|
149
|
+
_clearEntryCacheTimer(entry);
|
|
150
|
+
syncFromActiveEntry();
|
|
151
|
+
} else entries.delete(key);
|
|
152
|
+
}, cache.cacheTime);
|
|
153
|
+
}
|
|
54
154
|
function getReady() {
|
|
55
155
|
if (!_ready) _ready = createCondition(() => currentStatus === "ready", (notify$1) => statusSignal.subscribe(() => notify$1()));
|
|
56
156
|
return _ready;
|
|
@@ -67,34 +167,68 @@ function createResourceNode(id, name, loader, autoLoad = true) {
|
|
|
67
167
|
if (!_stale) _stale = createCondition(() => currentStale, (notify$1) => staleSignal.subscribe(() => notify$1()));
|
|
68
168
|
return _stale;
|
|
69
169
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
170
|
+
function executeLoad(context) {
|
|
171
|
+
const key = resolveKey(context);
|
|
172
|
+
const prevActiveKey = _activeKey;
|
|
173
|
+
_activeKey = key;
|
|
174
|
+
let entry = entries.get(key);
|
|
175
|
+
if (!entry) {
|
|
176
|
+
entry = createEntry();
|
|
177
|
+
entries.set(key, entry);
|
|
178
|
+
}
|
|
179
|
+
_clearEntryCacheTimer(entry);
|
|
180
|
+
if (shouldDeduplicate && entry.inFlightPromise) {
|
|
181
|
+
if (key !== prevActiveKey) syncFromEntry(entry);
|
|
182
|
+
return entry.inFlightPromise;
|
|
183
|
+
}
|
|
77
184
|
if (context !== void 0) lastContext = context;
|
|
78
185
|
const loadContext = context ?? lastContext ?? {};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
186
|
+
entry.stale = false;
|
|
187
|
+
entry.status = "pending";
|
|
188
|
+
entry.value = void 0;
|
|
189
|
+
entry.error = void 0;
|
|
190
|
+
syncFromActiveEntry();
|
|
191
|
+
const promise = (async () => {
|
|
192
|
+
try {
|
|
193
|
+
const result = await Promise.resolve(loader(loadContext));
|
|
194
|
+
if (entries.get(key) !== entry) return;
|
|
195
|
+
entry.value = result;
|
|
196
|
+
entry.status = "ready";
|
|
197
|
+
entry.stale = false;
|
|
198
|
+
_clearEntryCacheTimer(entry);
|
|
199
|
+
if (entry === getActiveEntry()) syncFromEntry(entry);
|
|
200
|
+
staleNotify();
|
|
201
|
+
_startEntryStaleTimer(entry, key);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
if (entries.get(key) !== entry) return;
|
|
204
|
+
entry.error = e;
|
|
205
|
+
entry.status = "failed";
|
|
206
|
+
entry.stale = false;
|
|
207
|
+
if (entry === getActiveEntry()) syncFromEntry(entry);
|
|
208
|
+
staleNotify();
|
|
209
|
+
} finally {
|
|
210
|
+
entry.inFlightPromise = null;
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
entry.inFlightPromise = promise;
|
|
214
|
+
return promise;
|
|
93
215
|
}
|
|
94
216
|
function invalidate() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
217
|
+
const entry = getActiveEntry();
|
|
218
|
+
if (!entry.stale) {
|
|
219
|
+
entry.stale = true;
|
|
220
|
+
_startEntryCacheTimer(entry, _activeKey);
|
|
221
|
+
if (entry === getActiveEntry()) {
|
|
222
|
+
currentStale = true;
|
|
223
|
+
staleNotify();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function dispose() {
|
|
228
|
+
for (const entry of entries.values()) {
|
|
229
|
+
_clearEntryStaleTimer(entry);
|
|
230
|
+
_clearEntryCacheTimer(entry);
|
|
231
|
+
entry.inFlightPromise = null;
|
|
98
232
|
}
|
|
99
233
|
}
|
|
100
234
|
const node = {
|
|
@@ -127,7 +261,8 @@ function createResourceNode(id, name, loader, autoLoad = true) {
|
|
|
127
261
|
invalidate,
|
|
128
262
|
subscribe(fn) {
|
|
129
263
|
return statusSignal.subscribe(fn);
|
|
130
|
-
}
|
|
264
|
+
},
|
|
265
|
+
dispose
|
|
131
266
|
};
|
|
132
267
|
return node;
|
|
133
268
|
}
|
|
@@ -351,6 +486,11 @@ function getSurfaces() {
|
|
|
351
486
|
function getResources() {
|
|
352
487
|
return resourceMap;
|
|
353
488
|
}
|
|
489
|
+
function nextSuffix(baseId, exists) {
|
|
490
|
+
let counter = 2;
|
|
491
|
+
while (exists(`${baseId}-${counter}`)) counter++;
|
|
492
|
+
return `${baseId}-${counter}`;
|
|
493
|
+
}
|
|
354
494
|
|
|
355
495
|
//#endregion
|
|
356
496
|
//#region src/ask.ts
|
|
@@ -408,7 +548,9 @@ function computeAskError(node) {
|
|
|
408
548
|
var AskBuilder = class {
|
|
409
549
|
node;
|
|
410
550
|
constructor(label, stateRef) {
|
|
411
|
-
const
|
|
551
|
+
const baseId = `ask_${label.toLowerCase().replace(/\s+/g, "_")}`;
|
|
552
|
+
const existing = getAsks();
|
|
553
|
+
const id = existing.has(baseId) ? nextSuffix(baseId, (id$1) => existing.has(id$1)) : baseId;
|
|
412
554
|
const onChange = stateRef.onChange;
|
|
413
555
|
const subscribeToState = onChange ? (fn) => onChange((_v) => fn()) : void 0;
|
|
414
556
|
this.node = createAskNode(id, label, stateRef, subscribeToState);
|
|
@@ -520,7 +662,9 @@ async function executeAct(node, context, notify) {
|
|
|
520
662
|
var ActBuilder = class {
|
|
521
663
|
node;
|
|
522
664
|
constructor(label) {
|
|
523
|
-
const
|
|
665
|
+
const baseId = `act_${label.toLowerCase().replace(/\s+/g, "_")}`;
|
|
666
|
+
const existing = getActs();
|
|
667
|
+
const id = existing.has(baseId) ? nextSuffix(baseId, (id$1) => existing.has(id$1)) : baseId;
|
|
524
668
|
this.node = createActNode(id, label, [], null, void 0, false);
|
|
525
669
|
registerActNode(this.node);
|
|
526
670
|
}
|
|
@@ -578,7 +722,9 @@ var ActBuilder = class {
|
|
|
578
722
|
var FlowBuilder = class {
|
|
579
723
|
node;
|
|
580
724
|
constructor(name) {
|
|
581
|
-
const
|
|
725
|
+
const baseId = `flow_${name}`;
|
|
726
|
+
const existing = getFlows();
|
|
727
|
+
const id = existing.has(baseId) ? nextSuffix(baseId, (id$1) => existing.has(id$1)) : baseId;
|
|
582
728
|
this.node = {
|
|
583
729
|
id,
|
|
584
730
|
name,
|
|
@@ -623,7 +769,9 @@ var FlowBuilder = class {
|
|
|
623
769
|
var SurfaceBuilder = class {
|
|
624
770
|
node;
|
|
625
771
|
constructor(name) {
|
|
626
|
-
const
|
|
772
|
+
const baseId = `surface_${name}`;
|
|
773
|
+
const existing = getSurfaces();
|
|
774
|
+
const id = existing.has(baseId) ? nextSuffix(baseId, (id$1) => existing.has(id$1)) : baseId;
|
|
627
775
|
this.node = {
|
|
628
776
|
id,
|
|
629
777
|
name,
|
|
@@ -662,13 +810,15 @@ function screen(name, fn) {
|
|
|
662
810
|
flow: (n) => new FlowBuilder(n),
|
|
663
811
|
surface: (n) => new SurfaceBuilder(n),
|
|
664
812
|
resource: (n, config) => {
|
|
665
|
-
const
|
|
813
|
+
const baseId = `resource_${n}`;
|
|
814
|
+
const id = configs.some((c) => c.id === baseId) ? nextSuffix(baseId, (id$1) => configs.some((c) => c.id === id$1)) : baseId;
|
|
666
815
|
const ref = new ResourceRef(id, n, config.load, config.autoLoad ?? true);
|
|
667
816
|
configs.push({
|
|
668
817
|
id,
|
|
669
818
|
name: n,
|
|
670
819
|
autoLoad: config.autoLoad ?? true,
|
|
671
820
|
loader: config.load,
|
|
821
|
+
cache: config.cache,
|
|
672
822
|
ref
|
|
673
823
|
});
|
|
674
824
|
return ref;
|
|
@@ -691,6 +841,28 @@ function screen(name, fn) {
|
|
|
691
841
|
|
|
692
842
|
//#endregion
|
|
693
843
|
//#region src/graph.ts
|
|
844
|
+
const NODE_KINDS = {
|
|
845
|
+
ask: "ask",
|
|
846
|
+
act: "action",
|
|
847
|
+
flow: "flow",
|
|
848
|
+
surface: "surface",
|
|
849
|
+
resource: "resource"
|
|
850
|
+
};
|
|
851
|
+
function slugify(text) {
|
|
852
|
+
return text.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
853
|
+
}
|
|
854
|
+
function createSemanticIdFactory(kind) {
|
|
855
|
+
const prefix = NODE_KINDS[kind] ?? kind;
|
|
856
|
+
const used = /* @__PURE__ */ new Map();
|
|
857
|
+
let unnamed = 0;
|
|
858
|
+
return (source) => {
|
|
859
|
+
const slug = slugify(source);
|
|
860
|
+
const base = slug.length > 0 ? slug : String(++unnamed);
|
|
861
|
+
const count = used.get(base) ?? 0;
|
|
862
|
+
used.set(base, count + 1);
|
|
863
|
+
return count === 0 ? `${prefix}:${base}` : `${prefix}:${base}-${count + 1}`;
|
|
864
|
+
};
|
|
865
|
+
}
|
|
694
866
|
function computeDiagnostics(screenDef) {
|
|
695
867
|
const diagnostics = [];
|
|
696
868
|
const primaryActions = screenDef.acts.filter((a) => a.primary);
|
|
@@ -725,13 +897,66 @@ function computeDiagnostics(screenDef) {
|
|
|
725
897
|
message: "Action is defined but not included in any surface.",
|
|
726
898
|
nodeId: act.id
|
|
727
899
|
});
|
|
900
|
+
if (screenDef.flows.length > 0) {
|
|
901
|
+
const flowNodeIds = /* @__PURE__ */ new Set();
|
|
902
|
+
for (const flow of screenDef.flows) for (const step of flow.steps) flowNodeIds.add(step.node.id);
|
|
903
|
+
for (const flow of screenDef.flows) for (const step of flow.steps) if (!surfacedNodeIds.has(step.node.id)) diagnostics.push({
|
|
904
|
+
severity: "warning",
|
|
905
|
+
code: "flow-step-not-surfaced",
|
|
906
|
+
message: `"${step.node.label}" is a flow step but not included in any surface.`,
|
|
907
|
+
nodeId: step.node.id,
|
|
908
|
+
flow: { flowNodeId: flow.id }
|
|
909
|
+
});
|
|
910
|
+
for (const flow of screenDef.flows) if (flow.steps.length > 0) {
|
|
911
|
+
const hasSurfacedStep = flow.steps.some((step) => surfacedNodeIds.has(step.node.id));
|
|
912
|
+
if (!hasSurfacedStep) diagnostics.push({
|
|
913
|
+
severity: "warning",
|
|
914
|
+
code: "orphaned-flow",
|
|
915
|
+
message: `"${flow.name}" has no surfaced steps.`,
|
|
916
|
+
flow: { flowNodeId: flow.id }
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
for (const ask of screenDef.asks) if (surfacedNodeIds.has(ask.id) && !flowNodeIds.has(ask.id)) diagnostics.push({
|
|
920
|
+
severity: "info",
|
|
921
|
+
code: "surfaced-node-not-in-any-flow",
|
|
922
|
+
message: `"${ask.label}" is surfaced but not referenced in any flow.`,
|
|
923
|
+
nodeId: ask.id
|
|
924
|
+
});
|
|
925
|
+
for (const act of screenDef.acts) if (surfacedNodeIds.has(act.id) && !flowNodeIds.has(act.id)) diagnostics.push({
|
|
926
|
+
severity: "info",
|
|
927
|
+
code: "surfaced-node-not-in-any-flow",
|
|
928
|
+
message: `"${act.label}" is surfaced but not referenced in any flow.`,
|
|
929
|
+
nodeId: act.id
|
|
930
|
+
});
|
|
931
|
+
}
|
|
728
932
|
return diagnostics;
|
|
729
933
|
}
|
|
730
934
|
function inspectScreen(screenDef, runtimeResources) {
|
|
935
|
+
const diagnostics = computeDiagnostics(screenDef);
|
|
936
|
+
const askIds = createSemanticIdFactory("ask");
|
|
937
|
+
const actIds = createSemanticIdFactory("act");
|
|
938
|
+
const flowIds = createSemanticIdFactory("flow");
|
|
939
|
+
const surfaceIds = createSemanticIdFactory("surface");
|
|
940
|
+
const resourceIds = createSemanticIdFactory("resource");
|
|
941
|
+
const idToSemantic = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const a of screenDef.asks) idToSemantic.set(a.id, askIds(a.label));
|
|
943
|
+
for (const a of screenDef.acts) idToSemantic.set(a.id, actIds(a.label));
|
|
944
|
+
const idToFlowSemantic = /* @__PURE__ */ new Map();
|
|
945
|
+
for (const f of screenDef.flows) idToFlowSemantic.set(f.id, flowIds(f.name));
|
|
946
|
+
const augmentedDiagnostics = diagnostics.map((d) => ({
|
|
947
|
+
...d,
|
|
948
|
+
semanticNodeId: d.nodeId ? idToSemantic.get(d.nodeId) : void 0,
|
|
949
|
+
flow: d.flow ? {
|
|
950
|
+
...d.flow,
|
|
951
|
+
flowSemanticNodeId: idToFlowSemantic.get(d.flow.flowNodeId)
|
|
952
|
+
} : void 0
|
|
953
|
+
}));
|
|
731
954
|
return {
|
|
732
955
|
name: screenDef.name,
|
|
956
|
+
semanticId: `screen:${slugify(screenDef.name)}`,
|
|
733
957
|
asks: screenDef.asks.map((a) => ({
|
|
734
958
|
id: a.id,
|
|
959
|
+
semanticId: idToSemantic.get(a.id),
|
|
735
960
|
label: a.label,
|
|
736
961
|
kind: a.kind,
|
|
737
962
|
required: a.required,
|
|
@@ -741,6 +966,7 @@ function inspectScreen(screenDef, runtimeResources) {
|
|
|
741
966
|
})),
|
|
742
967
|
acts: screenDef.acts.map((a) => ({
|
|
743
968
|
id: a.id,
|
|
969
|
+
semanticId: idToSemantic.get(a.id),
|
|
744
970
|
label: a.label,
|
|
745
971
|
primary: a.primary,
|
|
746
972
|
enabled: a.enabled.current,
|
|
@@ -751,23 +977,26 @@ function inspectScreen(screenDef, runtimeResources) {
|
|
|
751
977
|
})),
|
|
752
978
|
flows: screenDef.flows.map((f) => ({
|
|
753
979
|
id: f.id,
|
|
980
|
+
semanticId: idToFlowSemantic.get(f.id),
|
|
754
981
|
name: f.name,
|
|
755
982
|
stepCount: f.steps.length
|
|
756
983
|
})),
|
|
757
984
|
surfaces: screenDef.surfaces.map((s) => ({
|
|
758
985
|
id: s.id,
|
|
986
|
+
semanticId: surfaceIds(s.name),
|
|
759
987
|
name: s.name,
|
|
760
988
|
itemCount: s.items.length
|
|
761
989
|
})),
|
|
762
990
|
resources: (runtimeResources ?? []).map((r) => ({
|
|
763
991
|
id: r.id,
|
|
992
|
+
semanticId: resourceIds(r.name),
|
|
764
993
|
name: r.name,
|
|
765
994
|
status: r.status,
|
|
766
995
|
hasValue: r.value !== void 0,
|
|
767
996
|
stale: r.stale.current,
|
|
768
997
|
error: r.status === "failed" ? r.error instanceof Error ? r.error.message : String(r.error) : void 0
|
|
769
998
|
})),
|
|
770
|
-
diagnostics:
|
|
999
|
+
diagnostics: augmentedDiagnostics
|
|
771
1000
|
};
|
|
772
1001
|
}
|
|
773
1002
|
|
|
@@ -814,7 +1043,7 @@ var ScreenRuntime = class {
|
|
|
814
1043
|
this._started = true;
|
|
815
1044
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
816
1045
|
for (const config of this._screen.resourceConfigs) {
|
|
817
|
-
const node = createResourceNode(config.id, config.name, config.loader, false);
|
|
1046
|
+
const node = createResourceNode(config.id, config.name, config.loader, false, config.cache);
|
|
818
1047
|
this._resourceNodes.push(node);
|
|
819
1048
|
nodeMap.set(config.id, node);
|
|
820
1049
|
}
|
|
@@ -836,6 +1065,7 @@ var ScreenRuntime = class {
|
|
|
836
1065
|
this._disposed = true;
|
|
837
1066
|
for (const unsub of this._unsubscribers) unsub();
|
|
838
1067
|
this._unsubscribers = [];
|
|
1068
|
+
for (const node of this._resourceNodes) node.dispose();
|
|
839
1069
|
for (const config of this._screen.resourceConfigs) if (config.ref && this._resourceNodeMap) {
|
|
840
1070
|
const node = this._resourceNodeMap.get(config.id);
|
|
841
1071
|
if (node) config.ref._disconnect(node);
|
package/dist/registry.d.ts
CHANGED
|
@@ -23,3 +23,4 @@ export declare function getActs(): Map<string, ActNode<any>>;
|
|
|
23
23
|
export declare function getFlows(): Map<string, FlowNode>;
|
|
24
24
|
export declare function getSurfaces(): Map<string, SurfaceNode>;
|
|
25
25
|
export declare function getResources(): Map<string, AnyResourceNode>;
|
|
26
|
+
export declare function nextSuffix(baseId: string, exists: (id: string) => boolean): string;
|
package/dist/resource.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Condition } from "./signal.js";
|
|
2
2
|
import type { ActionExecutionContext, DefaultScreenServices } from "./act.js";
|
|
3
3
|
export type ResourceStatus = "idle" | "pending" | "ready" | "failed";
|
|
4
|
+
export type ResourceKey = string | number | boolean | null | undefined | ResourceKey[];
|
|
4
5
|
export type ResourceLoadContext<TServices extends object = DefaultScreenServices> = ActionExecutionContext<TServices>;
|
|
5
6
|
type ResourceLoader<TValue, TServices extends object> = (() => TValue | Promise<TValue>) | ((context: ResourceLoadContext<TServices>) => TValue | Promise<TValue>);
|
|
6
7
|
export type ResourceNode<TValue, TServices extends object = DefaultScreenServices> = {
|
|
@@ -18,17 +19,25 @@ export type ResourceNode<TValue, TServices extends object = DefaultScreenService
|
|
|
18
19
|
reload: (context?: ResourceLoadContext<TServices>) => Promise<void>;
|
|
19
20
|
invalidate: () => void;
|
|
20
21
|
subscribe: (fn: () => void) => () => void;
|
|
22
|
+
dispose: () => void;
|
|
21
23
|
};
|
|
22
24
|
export type AnyResourceNode = ResourceNode<unknown, any>;
|
|
25
|
+
export type ResourceCacheOptions<TServices extends object = DefaultScreenServices> = {
|
|
26
|
+
key?: (context: ResourceLoadContext<TServices>) => ResourceKey;
|
|
27
|
+
staleTime?: number;
|
|
28
|
+
cacheTime?: number;
|
|
29
|
+
deduplicate?: boolean;
|
|
30
|
+
};
|
|
23
31
|
export type ResourceConfig<TValue = unknown, TServices extends object = DefaultScreenServices> = {
|
|
24
32
|
id: string;
|
|
25
33
|
name: string;
|
|
26
34
|
autoLoad: boolean;
|
|
27
35
|
loader: ResourceLoader<TValue, TServices>;
|
|
36
|
+
cache?: ResourceCacheOptions<TServices>;
|
|
28
37
|
ref?: ResourceRef<TValue, TServices>;
|
|
29
38
|
};
|
|
30
39
|
export declare function createResourceConfig<TValue, TServices extends object = DefaultScreenServices>(id: string, name: string, loader: ResourceLoader<TValue, TServices>, autoLoad?: boolean): ResourceConfig<TValue, TServices>;
|
|
31
|
-
export declare function createResourceNode<TValue, TServices extends object = DefaultScreenServices>(id: string, name: string, loader: ResourceLoader<TValue, TServices>, autoLoad?: boolean): ResourceNode<TValue, TServices>;
|
|
40
|
+
export declare function createResourceNode<TValue, TServices extends object = DefaultScreenServices>(id: string, name: string, loader: ResourceLoader<TValue, TServices>, autoLoad?: boolean, cache?: ResourceCacheOptions<TServices>): ResourceNode<TValue, TServices>;
|
|
32
41
|
export declare class ResourceRef<TValue, TServices extends object = DefaultScreenServices> {
|
|
33
42
|
readonly id: string;
|
|
34
43
|
readonly name: string;
|
package/dist/screen.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AnyAskNode } from "./ask.js";
|
|
|
2
2
|
import type { ActNode, DefaultScreenServices } from "./act.js";
|
|
3
3
|
import type { FlowNode } from "./flow.js";
|
|
4
4
|
import type { SurfaceNode } from "./surface.js";
|
|
5
|
-
import type { ResourceConfig, ResourceLoadContext } from "./resource.js";
|
|
5
|
+
import type { ResourceCacheOptions, ResourceConfig, ResourceLoadContext } from "./resource.js";
|
|
6
6
|
import { ResourceRef } from "./resource.js";
|
|
7
7
|
import { type TextState, type BooleanState, type ChoiceState } from "./state.js";
|
|
8
8
|
import { AskBuilder } from "./ask.js";
|
|
@@ -31,6 +31,7 @@ export type ScreenBuilder<TServices extends object = DefaultScreenServices> = {
|
|
|
31
31
|
resource: <T>(name: string, config: {
|
|
32
32
|
load: (() => Promise<T>) | ((context: ResourceLoadContext<TServices>) => Promise<T>);
|
|
33
33
|
autoLoad?: boolean;
|
|
34
|
+
cache?: ResourceCacheOptions<TServices>;
|
|
34
35
|
}) => ResourceRef<T, TServices>;
|
|
35
36
|
};
|
|
36
37
|
export type ScreenDefinition<TServices extends object = DefaultScreenServices> = {
|