@cynodia/axiom-core 0.5.0-alpha.1 → 0.6.0-alpha.1
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 +17 -6
- package/dist/authority.d.ts +63 -0
- package/dist/authority.js +253 -0
- package/dist/diagnostics.d.ts +6 -0
- package/dist/diagnostics.js +8 -0
- package/dist/graph.d.ts +15 -3
- package/dist/graph.js +26 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/infer.js +9 -0
- package/dist/ir.d.ts +21 -0
- package/dist/nodes.d.ts +133 -8
- package/dist/presentation.d.ts +28 -1
- package/dist/presentation.js +15 -0
- package/dist/resolve-presentation.d.ts +2 -1
- package/dist/resolve-presentation.js +49 -6
- package/dist/server-ir.d.ts +46 -0
- package/dist/server-ir.js +5 -0
- package/dist/theme.d.ts +20 -1
- package/dist/theme.js +8 -0
- package/dist/types.d.ts +5 -0
- package/dist/ui.d.ts +95 -4
- package/dist/ui.js +20 -0
- package/dist/validate-authority.d.ts +16 -0
- package/dist/validate-authority.js +192 -0
- package/dist/validate-location.js +10 -1
- package/dist/validate-presentation.js +215 -9
- package/dist/validate-value.d.ts +24 -0
- package/dist/validate-value.js +100 -0
- package/dist/validate.js +71 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,13 +6,17 @@ framework.
|
|
|
6
6
|
**Status: experimental / alpha.** The API may change between alpha releases.
|
|
7
7
|
|
|
8
8
|
The Application Graph and its semantic model: nodes, fields, structured types,
|
|
9
|
-
expressions, **locations** (addressable writable positions), edge derivation,
|
|
10
|
-
|
|
9
|
+
expressions, **locations** (addressable writable positions), edge derivation, validation
|
|
10
|
+
and type inference. It also owns the presentation and UX layer — semantic roles, layout and
|
|
11
|
+
spacing tokens, device classes, value formats and the `Theme` — with presentation
|
|
12
|
+
resolution and validation.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Presentation lives here because it is part of the canonical graph: it is intent, not
|
|
15
|
+
styling, and it names no colour, length or CSS property.
|
|
16
|
+
|
|
17
|
+
Main exports: `ApplicationGraph`, `TypeRef` builders, expression builders, `Location`
|
|
18
|
+
builders, `Presentation`, `Theme`, `resolvePresentationMap`, `validateGraph`,
|
|
19
|
+
`VALIDATION_CODES`, `ApplicationIR`.
|
|
16
20
|
|
|
17
21
|
## Installation
|
|
18
22
|
|
|
@@ -26,6 +30,13 @@ Most applications should install the facade package instead, which re-exports th
|
|
|
26
30
|
npm install @cynodia/axiom@alpha
|
|
27
31
|
```
|
|
28
32
|
|
|
33
|
+
|
|
34
|
+
## Documentation
|
|
35
|
+
|
|
36
|
+
The canonical operational contract lives in the `docs/` directory of the
|
|
37
|
+
[`@cynodia/axiom`](https://www.npmjs.com/package/@cynodia/axiom) package, and in
|
|
38
|
+
[the repository](https://github.com/cynodia/axiom). Start with `docs/AGENT_REFERENCE.md`.
|
|
39
|
+
|
|
29
40
|
## License
|
|
30
41
|
|
|
31
42
|
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Expression } from './expressions.js';
|
|
2
|
+
import type { NodeId } from './ids.js';
|
|
3
|
+
import type { ActionDef, ConstraintDef, StateDef, TransitionConstraintDef } from './nodes.js';
|
|
4
|
+
import type { AnyNode } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Who may commit a canonical mutation.
|
|
7
|
+
*
|
|
8
|
+
* Authority and persistence are separate concerns. Authority says *who decides* a value;
|
|
9
|
+
* persistence says *where a decided value survives*. A server-authoritative state may be
|
|
10
|
+
* held only in memory, and a client-authoritative state may be persisted to local storage.
|
|
11
|
+
*/
|
|
12
|
+
export type Authority = 'client' | 'server';
|
|
13
|
+
export declare const AUTHORITIES: readonly Authority[];
|
|
14
|
+
/**
|
|
15
|
+
* The scope an authorization expression resolves the caller through.
|
|
16
|
+
*
|
|
17
|
+
* It is bound to a record keyed by the field ids of the graph's principal entity, so an
|
|
18
|
+
* authorization rule is written with the same `field`/`ref` vocabulary as everything else:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* binary('eq', field(ref(PRINCIPAL), F_USER_ROLE), literal('admin'))
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* It is bound only where a server evaluates. A client never sees it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PRINCIPAL: NodeId;
|
|
27
|
+
/**
|
|
28
|
+
* The authority of a state. Absent metadata means `client`, so every 0.5.x graph keeps
|
|
29
|
+
* executing exactly as it did.
|
|
30
|
+
*/
|
|
31
|
+
export declare function stateAuthority(state: StateDef): Authority;
|
|
32
|
+
/** Whether the client may observe this state at all. */
|
|
33
|
+
export declare function isObservable(state: StateDef): boolean;
|
|
34
|
+
export interface AuthorityContext {
|
|
35
|
+
states: Map<NodeId, StateDef>;
|
|
36
|
+
actions: Map<NodeId, ActionDef>;
|
|
37
|
+
constraints: ConstraintDef[];
|
|
38
|
+
transitionConstraints: TransitionConstraintDef[];
|
|
39
|
+
principalEntityId?: NodeId;
|
|
40
|
+
}
|
|
41
|
+
export declare function authorityContext(nodes: readonly AnyNode[], principalEntityId?: NodeId): AuthorityContext;
|
|
42
|
+
/** Every state a set of expressions reads, following derived state transitively. */
|
|
43
|
+
export declare function statesReadBy(expressions: readonly Expression[], context: AuthorityContext): Set<NodeId>;
|
|
44
|
+
/** The states an action writes, following `for-each`, `invoke` and declared native effects. */
|
|
45
|
+
export declare function statesWrittenBy(action: ActionDef, context: AuthorityContext, visited?: Set<NodeId>): Set<NodeId>;
|
|
46
|
+
/** The states an action reads: its guards, its values, its selectors, its authorization. */
|
|
47
|
+
export declare function statesReadByAction(action: ActionDef, context: AuthorityContext, visited?: Set<NodeId>): Set<NodeId>;
|
|
48
|
+
/**
|
|
49
|
+
* Where an action must execute.
|
|
50
|
+
*
|
|
51
|
+
* An action that writes any server-authoritative state is a **server action**: only the
|
|
52
|
+
* authority that owns the state may commit it. This is derived, never declared, so it
|
|
53
|
+
* cannot disagree with what the action actually does.
|
|
54
|
+
*/
|
|
55
|
+
export declare function actionAuthority(action: ActionDef, context: AuthorityContext): Authority;
|
|
56
|
+
/** Actions the client must send to the authority rather than execute itself. */
|
|
57
|
+
export declare function serverActionIds(context: AuthorityContext): NodeId[];
|
|
58
|
+
/**
|
|
59
|
+
* Every state a server action needs in order to execute: what it writes, what it reads,
|
|
60
|
+
* and the derived state either of those depends on.
|
|
61
|
+
*/
|
|
62
|
+
export declare function serverStateClosure(context: AuthorityContext): Set<NodeId>;
|
|
63
|
+
//# sourceMappingURL=authority.d.ts.map
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { referencedIds } from './derive-edges.js';
|
|
2
|
+
import { actionGuards, isMutationOperation } from './nodes.js';
|
|
3
|
+
import { locationExpressions, locationRootStateId } from './location.js';
|
|
4
|
+
export const AUTHORITIES = ['client', 'server'];
|
|
5
|
+
/**
|
|
6
|
+
* The scope an authorization expression resolves the caller through.
|
|
7
|
+
*
|
|
8
|
+
* It is bound to a record keyed by the field ids of the graph's principal entity, so an
|
|
9
|
+
* authorization rule is written with the same `field`/`ref` vocabulary as everything else:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* binary('eq', field(ref(PRINCIPAL), F_USER_ROLE), literal('admin'))
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* It is bound only where a server evaluates. A client never sees it.
|
|
16
|
+
*/
|
|
17
|
+
export const PRINCIPAL = 'axiom_principal';
|
|
18
|
+
/**
|
|
19
|
+
* The authority of a state. Absent metadata means `client`, so every 0.5.x graph keeps
|
|
20
|
+
* executing exactly as it did.
|
|
21
|
+
*/
|
|
22
|
+
export function stateAuthority(state) {
|
|
23
|
+
return state.authority === 'server' ? 'server' : 'client';
|
|
24
|
+
}
|
|
25
|
+
/** Whether the client may observe this state at all. */
|
|
26
|
+
export function isObservable(state) {
|
|
27
|
+
return state.serverOnly !== true;
|
|
28
|
+
}
|
|
29
|
+
export function authorityContext(nodes, principalEntityId) {
|
|
30
|
+
const states = new Map();
|
|
31
|
+
const actions = new Map();
|
|
32
|
+
const constraints = [];
|
|
33
|
+
const transitionConstraints = [];
|
|
34
|
+
for (const node of nodes) {
|
|
35
|
+
switch (node.kind) {
|
|
36
|
+
case 'state':
|
|
37
|
+
states.set(node.id, node);
|
|
38
|
+
break;
|
|
39
|
+
case 'action':
|
|
40
|
+
actions.set(node.id, node);
|
|
41
|
+
break;
|
|
42
|
+
case 'constraint':
|
|
43
|
+
constraints.push(node);
|
|
44
|
+
break;
|
|
45
|
+
case 'transition-constraint':
|
|
46
|
+
transitionConstraints.push(node);
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
states,
|
|
53
|
+
actions,
|
|
54
|
+
constraints,
|
|
55
|
+
transitionConstraints,
|
|
56
|
+
...(principalEntityId ? { principalEntityId } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Every state a set of expressions reads, following derived state transitively. */
|
|
60
|
+
export function statesReadBy(expressions, context) {
|
|
61
|
+
const found = new Set();
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const walk = (expression) => {
|
|
64
|
+
for (const id of referencedIds(expression)) {
|
|
65
|
+
const state = context.states.get(id);
|
|
66
|
+
if (!state || seen.has(id)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
seen.add(id);
|
|
70
|
+
found.add(id);
|
|
71
|
+
if (state.derivation) {
|
|
72
|
+
walk(state.derivation);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
expressions.forEach(walk);
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
/** Every expression an operation evaluates, including those inside its locations. */
|
|
80
|
+
function operationExpressions(operation) {
|
|
81
|
+
const found = [];
|
|
82
|
+
if (isMutationOperation(operation)) {
|
|
83
|
+
const target = operation.kind === 'remove' ? operation.target : operation.target;
|
|
84
|
+
found.push(...locationExpressions(target));
|
|
85
|
+
if (operation.kind !== 'remove') {
|
|
86
|
+
found.push(operation.value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
switch (operation.kind) {
|
|
90
|
+
case 'for-each':
|
|
91
|
+
found.push(operation.collection);
|
|
92
|
+
for (const nested of operation.operations ?? []) {
|
|
93
|
+
found.push(...operationExpressions(nested));
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case 'invoke':
|
|
97
|
+
found.push(...Object.values(operation.arguments ?? {}));
|
|
98
|
+
break;
|
|
99
|
+
case 'navigate':
|
|
100
|
+
found.push(...Object.values(operation.parameters ?? {}));
|
|
101
|
+
break;
|
|
102
|
+
case 'native':
|
|
103
|
+
found.push(...Object.values(operation.inputs ?? {}));
|
|
104
|
+
if (operation.resultTarget) {
|
|
105
|
+
found.push(...locationExpressions(operation.resultTarget));
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
}
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
/** The states an action writes, following `for-each`, `invoke` and declared native effects. */
|
|
113
|
+
export function statesWrittenBy(action, context, visited = new Set()) {
|
|
114
|
+
const found = new Set();
|
|
115
|
+
if (visited.has(action.id)) {
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
118
|
+
visited.add(action.id);
|
|
119
|
+
const walk = (operations) => {
|
|
120
|
+
for (const operation of operations) {
|
|
121
|
+
if (isMutationOperation(operation)) {
|
|
122
|
+
found.add(locationRootStateId(operation.target));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
switch (operation.kind) {
|
|
126
|
+
case 'for-each':
|
|
127
|
+
walk(operation.operations ?? []);
|
|
128
|
+
break;
|
|
129
|
+
case 'invoke': {
|
|
130
|
+
const target = context.actions.get(operation.actionId);
|
|
131
|
+
if (target) {
|
|
132
|
+
for (const id of statesWrittenBy(target, context, visited)) {
|
|
133
|
+
found.add(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'native':
|
|
139
|
+
if (operation.resultTarget) {
|
|
140
|
+
found.add(locationRootStateId(operation.resultTarget));
|
|
141
|
+
}
|
|
142
|
+
for (const effect of operation.declaredEffects ?? []) {
|
|
143
|
+
if (effect.kind === 'writes-state') {
|
|
144
|
+
found.add(effect.stateId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
walk(action.operations ?? []);
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
155
|
+
/** The states an action reads: its guards, its values, its selectors, its authorization. */
|
|
156
|
+
export function statesReadByAction(action, context, visited = new Set()) {
|
|
157
|
+
if (visited.has(action.id)) {
|
|
158
|
+
return new Set();
|
|
159
|
+
}
|
|
160
|
+
visited.add(action.id);
|
|
161
|
+
const expressions = [
|
|
162
|
+
...actionGuards(action).map((guard) => guard.condition),
|
|
163
|
+
...(action.postconditions ?? []),
|
|
164
|
+
...(action.authorization ? [action.authorization] : []),
|
|
165
|
+
];
|
|
166
|
+
for (const operation of action.operations ?? []) {
|
|
167
|
+
expressions.push(...operationExpressions(operation));
|
|
168
|
+
}
|
|
169
|
+
const found = statesReadBy(expressions, context);
|
|
170
|
+
for (const operation of action.operations ?? []) {
|
|
171
|
+
if (operation.kind === 'invoke') {
|
|
172
|
+
const target = context.actions.get(operation.actionId);
|
|
173
|
+
if (target) {
|
|
174
|
+
for (const id of statesReadByAction(target, context, visited)) {
|
|
175
|
+
found.add(id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return found;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Where an action must execute.
|
|
184
|
+
*
|
|
185
|
+
* An action that writes any server-authoritative state is a **server action**: only the
|
|
186
|
+
* authority that owns the state may commit it. This is derived, never declared, so it
|
|
187
|
+
* cannot disagree with what the action actually does.
|
|
188
|
+
*/
|
|
189
|
+
export function actionAuthority(action, context) {
|
|
190
|
+
for (const stateId of statesWrittenBy(action, context)) {
|
|
191
|
+
const state = context.states.get(stateId);
|
|
192
|
+
if (state && stateAuthority(state) === 'server') {
|
|
193
|
+
return 'server';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return 'client';
|
|
197
|
+
}
|
|
198
|
+
/** Actions the client must send to the authority rather than execute itself. */
|
|
199
|
+
export function serverActionIds(context) {
|
|
200
|
+
return [...context.actions.values()]
|
|
201
|
+
.filter((action) => actionAuthority(action, context) === 'server')
|
|
202
|
+
.map((action) => action.id);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Every state a server action needs in order to execute: what it writes, what it reads,
|
|
206
|
+
* and the derived state either of those depends on.
|
|
207
|
+
*/
|
|
208
|
+
export function serverStateClosure(context) {
|
|
209
|
+
const needed = new Set();
|
|
210
|
+
for (const state of context.states.values()) {
|
|
211
|
+
if (stateAuthority(state) === 'server') {
|
|
212
|
+
needed.add(state.id);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const action of context.actions.values()) {
|
|
216
|
+
if (actionAuthority(action, context) !== 'server') {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
for (const id of statesWrittenBy(action, context)) {
|
|
220
|
+
needed.add(id);
|
|
221
|
+
}
|
|
222
|
+
for (const id of statesReadByAction(action, context)) {
|
|
223
|
+
needed.add(id);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Constraints and transition rules that govern what the server commits are evaluated
|
|
227
|
+
// there, so whatever they read has to be present too.
|
|
228
|
+
const ruleExpressions = [
|
|
229
|
+
...context.constraints.map((constraint) => constraint.expression),
|
|
230
|
+
...context.transitionConstraints.map((constraint) => constraint.expression),
|
|
231
|
+
];
|
|
232
|
+
for (const id of statesReadBy(ruleExpressions, context)) {
|
|
233
|
+
needed.add(id);
|
|
234
|
+
}
|
|
235
|
+
// Derived state is only meaningful if what it derives from is present as well.
|
|
236
|
+
let growing = true;
|
|
237
|
+
while (growing) {
|
|
238
|
+
growing = false;
|
|
239
|
+
for (const id of [...needed]) {
|
|
240
|
+
const state = context.states.get(id);
|
|
241
|
+
if (!state?.derivation) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
for (const dependency of statesReadBy([state.derivation], context)) {
|
|
245
|
+
if (!needed.has(dependency)) {
|
|
246
|
+
needed.add(dependency);
|
|
247
|
+
growing = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return needed;
|
|
253
|
+
}
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -65,5 +65,11 @@ export declare const VALIDATION_CODES: {
|
|
|
65
65
|
readonly formInputMissingLabel: "FORM_INPUT_MISSING_LABEL";
|
|
66
66
|
readonly invalidHeadingStructure: "INVALID_HEADING_STRUCTURE";
|
|
67
67
|
readonly opaquePresentation: "OPAQUE_PRESENTATION";
|
|
68
|
+
readonly clientWriteToServerState: "CLIENT_WRITE_TO_SERVER_STATE";
|
|
69
|
+
readonly serverDependsOnClientState: "SERVER_DEPENDS_ON_CLIENT_STATE";
|
|
70
|
+
readonly authorizationWithoutPrincipal: "AUTHORIZATION_WITHOUT_PRINCIPAL";
|
|
71
|
+
readonly principalReferenceOnClient: "PRINCIPAL_REFERENCE_ON_CLIENT";
|
|
72
|
+
readonly serverOnlyStateObserved: "SERVER_ONLY_STATE_OBSERVED";
|
|
73
|
+
readonly invalidPrincipalEntity: "INVALID_PRINCIPAL_ENTITY";
|
|
68
74
|
};
|
|
69
75
|
//# sourceMappingURL=diagnostics.d.ts.map
|
package/dist/diagnostics.js
CHANGED
|
@@ -50,4 +50,12 @@ export const VALIDATION_CODES = {
|
|
|
50
50
|
formInputMissingLabel: 'FORM_INPUT_MISSING_LABEL',
|
|
51
51
|
invalidHeadingStructure: 'INVALID_HEADING_STRUCTURE',
|
|
52
52
|
opaquePresentation: 'OPAQUE_PRESENTATION',
|
|
53
|
+
// Authority. A graph that cannot execute safely across the trust boundary is rejected
|
|
54
|
+
// rather than left to fail at run time.
|
|
55
|
+
clientWriteToServerState: 'CLIENT_WRITE_TO_SERVER_STATE',
|
|
56
|
+
serverDependsOnClientState: 'SERVER_DEPENDS_ON_CLIENT_STATE',
|
|
57
|
+
authorizationWithoutPrincipal: 'AUTHORIZATION_WITHOUT_PRINCIPAL',
|
|
58
|
+
principalReferenceOnClient: 'PRINCIPAL_REFERENCE_ON_CLIENT',
|
|
59
|
+
serverOnlyStateObserved: 'SERVER_ONLY_STATE_OBSERVED',
|
|
60
|
+
invalidPrincipalEntity: 'INVALID_PRINCIPAL_ENTITY',
|
|
53
61
|
};
|
package/dist/graph.d.ts
CHANGED
|
@@ -10,9 +10,15 @@ export interface EdgeQuery {
|
|
|
10
10
|
kinds?: readonly EdgeKind[];
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* The Application Graph is the canonical representation of an application.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* The Application Graph is the canonical representation of an application. Everything else
|
|
14
|
+
* — the IR, the emitted page, the DOM — is derived from it and is never edited.
|
|
15
|
+
*
|
|
16
|
+
* Reads return **deep clones**, so a node retrieved from the graph must be written back
|
|
17
|
+
* with `updateNode` for the change to take effect.
|
|
18
|
+
*
|
|
19
|
+
* Relationships are derived from the current nodes on demand and cached against a revision
|
|
20
|
+
* counter, so an edge cannot fall out of step with the nodes it describes and no
|
|
21
|
+
* correctness property depends on resynchronizing anything.
|
|
16
22
|
*/
|
|
17
23
|
export declare class ApplicationGraph {
|
|
18
24
|
private data;
|
|
@@ -33,6 +39,12 @@ export declare class ApplicationGraph {
|
|
|
33
39
|
get theme(): Theme;
|
|
34
40
|
/** Exactly what the application declared, before defaults were filled in. */
|
|
35
41
|
get declaredTheme(): ThemeInput | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The entity an authorization expression reads the caller through, bound to `PRINCIPAL`
|
|
44
|
+
* when the authority evaluates the rule.
|
|
45
|
+
*/
|
|
46
|
+
get principalEntityId(): NodeId | undefined;
|
|
47
|
+
setPrincipalEntity(entityId: NodeId | undefined): void;
|
|
36
48
|
setTheme(theme: ThemeInput | undefined): void;
|
|
37
49
|
addNode<T extends AnyNode>(node: NodeInput<T>): NodeId;
|
|
38
50
|
getNode<T extends AnyNode = AnyNode>(id: NodeId): T | undefined;
|
package/dist/graph.js
CHANGED
|
@@ -5,9 +5,15 @@ function clone(value) {
|
|
|
5
5
|
return structuredClone(value);
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* The Application Graph is the canonical representation of an application.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The Application Graph is the canonical representation of an application. Everything else
|
|
9
|
+
* — the IR, the emitted page, the DOM — is derived from it and is never edited.
|
|
10
|
+
*
|
|
11
|
+
* Reads return **deep clones**, so a node retrieved from the graph must be written back
|
|
12
|
+
* with `updateNode` for the change to take effect.
|
|
13
|
+
*
|
|
14
|
+
* Relationships are derived from the current nodes on demand and cached against a revision
|
|
15
|
+
* counter, so an edge cannot fall out of step with the nodes it describes and no
|
|
16
|
+
* correctness property depends on resynchronizing anything.
|
|
11
17
|
*/
|
|
12
18
|
export class ApplicationGraph {
|
|
13
19
|
data;
|
|
@@ -17,7 +23,7 @@ export class ApplicationGraph {
|
|
|
17
23
|
/** Bumped by every change, so the derived edge index can never serve stale data. */
|
|
18
24
|
revision = 0;
|
|
19
25
|
semanticIndex;
|
|
20
|
-
constructor(id, name, version = '0.
|
|
26
|
+
constructor(id, name, version = '0.6.0') {
|
|
21
27
|
this.data = { id, name, version, nodes: {}, edges: {} };
|
|
22
28
|
}
|
|
23
29
|
get id() {
|
|
@@ -40,6 +46,22 @@ export class ApplicationGraph {
|
|
|
40
46
|
get declaredTheme() {
|
|
41
47
|
return this.data.theme ? structuredClone(this.data.theme) : undefined;
|
|
42
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The entity an authorization expression reads the caller through, bound to `PRINCIPAL`
|
|
51
|
+
* when the authority evaluates the rule.
|
|
52
|
+
*/
|
|
53
|
+
get principalEntityId() {
|
|
54
|
+
return this.data.principalEntityId;
|
|
55
|
+
}
|
|
56
|
+
setPrincipalEntity(entityId) {
|
|
57
|
+
if (entityId === undefined) {
|
|
58
|
+
delete this.data.principalEntityId;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.data.principalEntityId = entityId;
|
|
62
|
+
}
|
|
63
|
+
this.revision += 1;
|
|
64
|
+
}
|
|
43
65
|
setTheme(theme) {
|
|
44
66
|
if (theme === undefined) {
|
|
45
67
|
delete this.data.theme;
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export * from './validate-location.js';
|
|
|
15
15
|
export * from './resolve-presentation.js';
|
|
16
16
|
export * from './validate.js';
|
|
17
17
|
export * from './validate-presentation.js';
|
|
18
|
+
export * from './validate-authority.js';
|
|
19
|
+
export * from './validate-value.js';
|
|
18
20
|
export * from './derive-edges.js';
|
|
21
|
+
export * from './authority.js';
|
|
19
22
|
export * from './ir.js';
|
|
23
|
+
export * from './server-ir.js';
|
|
20
24
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -15,5 +15,9 @@ export * from './validate-location.js';
|
|
|
15
15
|
export * from './resolve-presentation.js';
|
|
16
16
|
export * from './validate.js';
|
|
17
17
|
export * from './validate-presentation.js';
|
|
18
|
+
export * from './validate-authority.js';
|
|
19
|
+
export * from './validate-value.js';
|
|
18
20
|
export * from './derive-edges.js';
|
|
21
|
+
export * from './authority.js';
|
|
19
22
|
export * from './ir.js';
|
|
23
|
+
export * from './server-ir.js';
|
package/dist/infer.js
CHANGED
|
@@ -131,6 +131,15 @@ export function inferExpressionType(expression, context, scope) {
|
|
|
131
131
|
return primitiveType('string');
|
|
132
132
|
case 'now':
|
|
133
133
|
return primitiveType('datetime');
|
|
134
|
+
case 'coalesce': {
|
|
135
|
+
// The type of the first argument, with its optionality removed: a fallback is
|
|
136
|
+
// what makes the value present. Inferring this is what lets a repeat over
|
|
137
|
+
// `coalesce(field(...), [])` still know how to identify its members.
|
|
138
|
+
const first = expression.arguments[0]
|
|
139
|
+
? inferExpressionType(expression.arguments[0], context, scope)
|
|
140
|
+
: undefined;
|
|
141
|
+
return first?.kind === 'optional' ? first.valueType : first;
|
|
142
|
+
}
|
|
134
143
|
default:
|
|
135
144
|
return undefined;
|
|
136
145
|
}
|
package/dist/ir.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { FieldIndexEntry } from './graph.js';
|
|
|
4
4
|
import type { UINode } from './ui.js';
|
|
5
5
|
import type { AnyNode } from './types.js';
|
|
6
6
|
import type { TypeRef } from './type-ref.js';
|
|
7
|
+
import type { Authority } from './authority.js';
|
|
7
8
|
import type { ResolvedPresentation } from './presentation.js';
|
|
8
9
|
import type { Theme } from './theme.js';
|
|
9
10
|
export interface RouteSegment {
|
|
@@ -55,6 +56,26 @@ export interface ApplicationIR {
|
|
|
55
56
|
* renderer can mark it without re-deriving the model.
|
|
56
57
|
*/
|
|
57
58
|
locationRequired: Record<NodeId, boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* The field that distinguishes the members of each `repeat` node's collection, resolved
|
|
61
|
+
* here so a runtime carries no type inference of its own.
|
|
62
|
+
*
|
|
63
|
+
* A renderer uses it to give every rendered repeat instance a stable identity. A repeat
|
|
64
|
+
* whose member type cannot be resolved statically is absent from this map, and the
|
|
65
|
+
* renderer falls back to the iteration index.
|
|
66
|
+
*/
|
|
67
|
+
repeatIdentityFields: Record<NodeId, FieldId>;
|
|
68
|
+
/**
|
|
69
|
+
* The authority of each state. A client runtime refuses to write a state whose authority
|
|
70
|
+
* is `server`, whatever path attempts it.
|
|
71
|
+
*/
|
|
72
|
+
authority: Record<NodeId, Authority>;
|
|
73
|
+
/**
|
|
74
|
+
* Actions the client must send to the authority rather than execute itself. Their
|
|
75
|
+
* operations, guards and authorization are **absent** from this IR: a client is never
|
|
76
|
+
* given the rules it is not trusted to apply.
|
|
77
|
+
*/
|
|
78
|
+
remoteActionIds: NodeId[];
|
|
58
79
|
/** The application's visual identity, completed against the default theme. */
|
|
59
80
|
theme: Theme;
|
|
60
81
|
/**
|