@cynodia/axiom-core 0.4.1-alpha.1 → 0.5.2-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 CHANGED
@@ -6,8 +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
- validation and type inference.
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.
13
+
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`.
11
20
 
12
21
  ## Installation
13
22
 
@@ -21,6 +30,13 @@ Most applications should install the facade package instead, which re-exports th
21
30
  npm install @cynodia/axiom@alpha
22
31
  ```
23
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
+
24
40
  ## License
25
41
 
26
42
  MIT
@@ -50,5 +50,20 @@ export declare const VALIDATION_CODES: {
50
50
  readonly initialValueInvalidEntity: "INITIAL_VALUE_INVALID_ENTITY";
51
51
  readonly scopeShadowing: "SCOPE_SHADOWING";
52
52
  readonly scopeCollidesWithNode: "SCOPE_COLLIDES_WITH_NODE";
53
+ readonly ephemeralStatePersisted: "EPHEMERAL_STATE_PERSISTED";
54
+ readonly unknownPresentationToken: "UNKNOWN_PRESENTATION_TOKEN";
55
+ readonly presentationSemanticConflict: "PRESENTATION_SEMANTIC_CONFLICT";
56
+ readonly multiplePrimaryActions: "MULTIPLE_PRIMARY_ACTIONS";
57
+ readonly formWithoutPrimaryAction: "FORM_WITHOUT_PRIMARY_ACTION";
58
+ readonly destructiveActionPresentedAsSuccess: "DESTRUCTIVE_ACTION_PRESENTED_AS_SUCCESS";
59
+ readonly destructiveActionUnmarked: "DESTRUCTIVE_ACTION_UNMARKED";
60
+ readonly excessiveHorizontalActions: "EXCESSIVE_HORIZONTAL_ACTIONS";
61
+ readonly emptyStateWithoutRecoveryAction: "EMPTY_STATE_WITHOUT_RECOVERY_ACTION";
62
+ readonly rigidHorizontalLayout: "RIGID_HORIZONTAL_LAYOUT";
63
+ readonly conflictingSizing: "CONFLICTING_SIZING";
64
+ readonly interactiveElementMissingLabel: "INTERACTIVE_ELEMENT_MISSING_LABEL";
65
+ readonly formInputMissingLabel: "FORM_INPUT_MISSING_LABEL";
66
+ readonly invalidHeadingStructure: "INVALID_HEADING_STRUCTURE";
67
+ readonly opaquePresentation: "OPAQUE_PRESENTATION";
53
68
  };
54
69
  //# sourceMappingURL=diagnostics.d.ts.map
@@ -33,4 +33,21 @@ export const VALIDATION_CODES = {
33
33
  initialValueInvalidEntity: 'INITIAL_VALUE_INVALID_ENTITY',
34
34
  scopeShadowing: 'SCOPE_SHADOWING',
35
35
  scopeCollidesWithNode: 'SCOPE_COLLIDES_WITH_NODE',
36
+ ephemeralStatePersisted: 'EPHEMERAL_STATE_PERSISTED',
37
+ // Presentation and UX. Everything here is a warning except an unknown token, which the
38
+ // renderer genuinely cannot act on.
39
+ unknownPresentationToken: 'UNKNOWN_PRESENTATION_TOKEN',
40
+ presentationSemanticConflict: 'PRESENTATION_SEMANTIC_CONFLICT',
41
+ multiplePrimaryActions: 'MULTIPLE_PRIMARY_ACTIONS',
42
+ formWithoutPrimaryAction: 'FORM_WITHOUT_PRIMARY_ACTION',
43
+ destructiveActionPresentedAsSuccess: 'DESTRUCTIVE_ACTION_PRESENTED_AS_SUCCESS',
44
+ destructiveActionUnmarked: 'DESTRUCTIVE_ACTION_UNMARKED',
45
+ excessiveHorizontalActions: 'EXCESSIVE_HORIZONTAL_ACTIONS',
46
+ emptyStateWithoutRecoveryAction: 'EMPTY_STATE_WITHOUT_RECOVERY_ACTION',
47
+ rigidHorizontalLayout: 'RIGID_HORIZONTAL_LAYOUT',
48
+ conflictingSizing: 'CONFLICTING_SIZING',
49
+ interactiveElementMissingLabel: 'INTERACTIVE_ELEMENT_MISSING_LABEL',
50
+ formInputMissingLabel: 'FORM_INPUT_MISSING_LABEL',
51
+ invalidHeadingStructure: 'INVALID_HEADING_STRUCTURE',
52
+ opaquePresentation: 'OPAQUE_PRESENTATION',
36
53
  };
package/dist/graph.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { EdgeId, FieldId, NodeId } from './ids.js';
2
2
  import type { EdgeKind, FieldDef, GraphEdge } from './nodes.js';
3
3
  import type { AnyNode, ApplicationGraphData, NodeInput, NodeKind, NodeOfKind } from './types.js';
4
+ import type { Theme, ThemeInput } from './theme.js';
4
5
  export interface FieldIndexEntry {
5
6
  entityId: NodeId;
6
7
  field: FieldDef;
@@ -9,9 +10,15 @@ export interface EdgeQuery {
9
10
  kinds?: readonly EdgeKind[];
10
11
  }
11
12
  /**
12
- * The Application Graph is the canonical representation of an application. Reads return
13
- * deep clones, so a node retrieved from the graph must be written back with
14
- * `updateNode` for the change to take effect.
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.
15
22
  */
16
23
  export declare class ApplicationGraph {
17
24
  private data;
@@ -25,6 +32,14 @@ export declare class ApplicationGraph {
25
32
  get id(): string;
26
33
  get name(): string;
27
34
  get version(): string;
35
+ /**
36
+ * The application's visual identity, completed against the default theme. A theme is
37
+ * presentation only: changing it cannot change an action, a constraint or a route.
38
+ */
39
+ get theme(): Theme;
40
+ /** Exactly what the application declared, before defaults were filled in. */
41
+ get declaredTheme(): ThemeInput | undefined;
42
+ setTheme(theme: ThemeInput | undefined): void;
28
43
  addNode<T extends AnyNode>(node: NodeInput<T>): NodeId;
29
44
  getNode<T extends AnyNode = AnyNode>(id: NodeId): T | undefined;
30
45
  hasNode(id: NodeId): boolean;
package/dist/graph.js CHANGED
@@ -1,12 +1,19 @@
1
1
  import { deriveEdges } from './derive-edges.js';
2
2
  import { createEdgeId, createNodeId } from './ids.js';
3
+ import { resolveTheme } from './theme.js';
3
4
  function clone(value) {
4
5
  return structuredClone(value);
5
6
  }
6
7
  /**
7
- * The Application Graph is the canonical representation of an application. Reads return
8
- * deep clones, so a node retrieved from the graph must be written back with
9
- * `updateNode` for the change to take effect.
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.
10
17
  */
11
18
  export class ApplicationGraph {
12
19
  data;
@@ -16,7 +23,7 @@ export class ApplicationGraph {
16
23
  /** Bumped by every change, so the derived edge index can never serve stale data. */
17
24
  revision = 0;
18
25
  semanticIndex;
19
- constructor(id, name, version = '0.4.1') {
26
+ constructor(id, name, version = '0.5.2') {
20
27
  this.data = { id, name, version, nodes: {}, edges: {} };
21
28
  }
22
29
  get id() {
@@ -28,6 +35,26 @@ export class ApplicationGraph {
28
35
  get version() {
29
36
  return this.data.version;
30
37
  }
38
+ /**
39
+ * The application's visual identity, completed against the default theme. A theme is
40
+ * presentation only: changing it cannot change an action, a constraint or a route.
41
+ */
42
+ get theme() {
43
+ return resolveTheme(this.data.theme);
44
+ }
45
+ /** Exactly what the application declared, before defaults were filled in. */
46
+ get declaredTheme() {
47
+ return this.data.theme ? structuredClone(this.data.theme) : undefined;
48
+ }
49
+ setTheme(theme) {
50
+ if (theme === undefined) {
51
+ delete this.data.theme;
52
+ }
53
+ else {
54
+ this.data.theme = structuredClone(theme);
55
+ }
56
+ this.revision += 1;
57
+ }
31
58
  addNode(node) {
32
59
  const id = (node.id ?? createNodeId(node.kind));
33
60
  if (this.data.nodes[id]) {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from './ids.js';
2
2
  export * from './diagnostics.js';
3
3
  export * from './location.js';
4
+ export * from './presentation.js';
5
+ export * from './theme.js';
4
6
  export * from './type-ref.js';
5
7
  export * from './expressions.js';
6
8
  export * from './nodes.js';
@@ -10,7 +12,9 @@ export * from './graph.js';
10
12
  export * from './infer.js';
11
13
  export * from './context.js';
12
14
  export * from './validate-location.js';
15
+ export * from './resolve-presentation.js';
13
16
  export * from './validate.js';
17
+ export * from './validate-presentation.js';
14
18
  export * from './derive-edges.js';
15
19
  export * from './ir.js';
16
20
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from './ids.js';
2
2
  export * from './diagnostics.js';
3
3
  export * from './location.js';
4
+ export * from './presentation.js';
5
+ export * from './theme.js';
4
6
  export * from './type-ref.js';
5
7
  export * from './expressions.js';
6
8
  export * from './nodes.js';
@@ -10,6 +12,8 @@ export * from './graph.js';
10
12
  export * from './infer.js';
11
13
  export * from './context.js';
12
14
  export * from './validate-location.js';
15
+ export * from './resolve-presentation.js';
13
16
  export * from './validate.js';
17
+ export * from './validate-presentation.js';
14
18
  export * from './derive-edges.js';
15
19
  export * from './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,8 @@ 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 { ResolvedPresentation } from './presentation.js';
8
+ import type { Theme } from './theme.js';
7
9
  export interface RouteSegment {
8
10
  kind: 'static' | 'parameter';
9
11
  value: string;
@@ -48,5 +50,30 @@ export interface ApplicationIR {
48
50
  * to canonical application state from a write to a draft.
49
51
  */
50
52
  locationRoots: Record<NodeId, NodeId>;
53
+ /**
54
+ * Whether the field each input addresses is declared required, resolved here so a
55
+ * renderer can mark it without re-deriving the model.
56
+ */
57
+ locationRequired: Record<NodeId, boolean>;
58
+ /**
59
+ * The field that distinguishes the members of each `repeat` node's collection, resolved
60
+ * here so a runtime carries no type inference of its own.
61
+ *
62
+ * A renderer uses it to give every rendered repeat instance a stable identity. A repeat
63
+ * whose member type cannot be resolved statically is absent from this map, and the
64
+ * renderer falls back to the iteration index.
65
+ */
66
+ repeatIdentityFields: Record<NodeId, FieldId>;
67
+ /** The application's visual identity, completed against the default theme. */
68
+ theme: Theme;
69
+ /**
70
+ * Presentation with every question already answered, per UI node: renderer defaults,
71
+ * theme, inheritance, semantic inference, node declaration and responsive overrides all
72
+ * resolved. A renderer reads this and needs to know nothing about how it was decided.
73
+ *
74
+ * It is deliberately still semantic — roles, tokens and device classes, not CSS — so a
75
+ * second renderer remains possible.
76
+ */
77
+ presentation: Record<NodeId, ResolvedPresentation>;
51
78
  }
52
79
  //# sourceMappingURL=ir.d.ts.map
package/dist/nodes.d.ts CHANGED
@@ -2,21 +2,29 @@ import type { Expression } from './expressions.js';
2
2
  import type { CollectionItemLocation, Location } from './location.js';
3
3
  import type { EdgeId, FieldId, NodeId } from './ids.js';
4
4
  import type { TypeRef } from './type-ref.js';
5
- /** Minimal, optional presentation hints. Styling is not a 0.2 research objective. */
6
- export interface PresentationHints {
7
- role?: 'primary' | 'secondary' | 'danger';
8
- density?: 'compact' | 'normal';
9
- emphasis?: 'normal' | 'strong';
10
- }
5
+ import type { ConfirmationPresentation } from './presentation.js';
11
6
  export interface NodeBase {
12
7
  id: NodeId;
13
8
  name?: string;
14
9
  metadata?: Record<string, unknown>;
15
10
  }
11
+ /**
12
+ * A field of an entity.
13
+ *
14
+ * Fields are independently identifiable and globally unique across the graph, and
15
+ * **instance data is keyed by `FieldId`, never by `name`**: a record looks like
16
+ * `{ [F_TITLE]: 'Dune' }`, not `{ title: 'Dune' }`. `name` is metadata for humans and
17
+ * resolves nothing.
18
+ */
16
19
  export interface FieldDef {
17
20
  id: FieldId;
18
21
  name?: string;
19
22
  valueType: TypeRef;
23
+ /**
24
+ * The value must be **present** in every canonical instance — not `null` and not
25
+ * `undefined`. It says nothing about emptiness: `0`, `false`, `''` and `[]` all satisfy
26
+ * it. Express "must not be blank" as a `ConstraintDef` using `non-empty`.
27
+ */
20
28
  required?: boolean;
21
29
  defaultValue?: LiteralValue;
22
30
  metadata?: Record<string, unknown>;
@@ -24,10 +32,21 @@ export interface FieldDef {
24
32
  export type LiteralValue = string | number | boolean | null | LiteralValue[] | {
25
33
  [key: string]: LiteralValue;
26
34
  };
35
+ /**
36
+ * A record type. Instances of it are found wherever a state's declared type says they
37
+ * live — including nested inside collections and inside other entities — and are validated
38
+ * there.
39
+ */
27
40
  export interface EntityDef extends NodeBase {
28
41
  kind: 'entity';
29
42
  fields: FieldDef[];
30
- /** Field used to distinguish instances of this entity. */
43
+ /**
44
+ * Field used to distinguish instances of this entity.
45
+ *
46
+ * Required in order to address an instance by identity (`identitySelector`), and
47
+ * required by every `TransitionConstraintDef` on this entity — without it such a rule is
48
+ * **silently skipped**. Declare it on every entity stored in a collection.
49
+ */
31
50
  identityFieldId?: FieldId;
32
51
  }
33
52
  export type StatePersistence = {
@@ -39,17 +58,50 @@ export type StatePersistence = {
39
58
  kind: 'remote';
40
59
  sourceId: NodeId;
41
60
  };
61
+ /**
62
+ * A named application value: stored, or computed from other state.
63
+ *
64
+ * A state with no `derivation` is stored, and one that is neither `draft` nor `ephemeral`
65
+ * is **canonical** — the state entity constraints and schema conformance apply to. Stored
66
+ * values are deeply frozen on entry to the store, and every read hands out a copy.
67
+ */
42
68
  export interface StateDef extends NodeBase {
43
69
  kind: 'state';
44
70
  valueType: TypeRef;
71
+ /**
72
+ * Seed data, keyed by `FieldId` wherever it contains a record. It is walked against
73
+ * `valueType` recursively at validation time, so data keyed by field *name* is rejected
74
+ * rather than surfacing later as an inexplicably empty UI.
75
+ *
76
+ * Absent, the state starts at the default for its type: `optional` → `null`,
77
+ * `collection` → `[]`, `number` → `0`, `boolean` → `false`, other primitive → `''`,
78
+ * `enum` → its first value, `entity` → `null`.
79
+ */
45
80
  initialValue?: LiteralValue;
46
- /** When present the state is computed rather than stored. */
81
+ /**
82
+ * When present the state is computed rather than stored.
83
+ *
84
+ * Derived state is **read-only**: a write is rejected by `validateGraph` and by the
85
+ * runtime. It is recomputed on demand and handed out as a frozen deep copy, so nothing
86
+ * can work by sharing an object with the state it was derived from. Instance validation
87
+ * skips it, because the data is already validated where it is stored.
88
+ */
47
89
  derivation?: Expression;
48
90
  /**
49
91
  * Marks a state that holds work in progress. Draft instances are incomplete by
50
92
  * definition, so instance validation skips them until an action commits the value.
51
93
  */
52
94
  draft?: boolean;
95
+ /**
96
+ * Marks ephemeral presentation state — which panel is expanded, which tab is selected,
97
+ * whether a dialog is open. It is not canonical domain state: instance validation skips
98
+ * it, and it may not be persisted. Marking it says so in the graph instead of leaving an
99
+ * agent to guess which states are domain facts.
100
+ *
101
+ * It changes what a state *is*, never what is permitted: a write reaching domain state
102
+ * is governed exactly as before.
103
+ */
104
+ ephemeral?: boolean;
53
105
  persistence?: StatePersistence;
54
106
  }
55
107
  export interface ActionParameter {
@@ -67,6 +119,18 @@ export interface ActionGuard {
67
119
  condition: Expression;
68
120
  failureMode?: FailureMode;
69
121
  }
122
+ /**
123
+ * Behaviour expressed as data, executed as a transaction.
124
+ *
125
+ * An invocation proceeds: resolve the action, bind parameters, evaluate preconditions in
126
+ * order, ask for confirmation if required — **none of which opens a transaction, so a
127
+ * refusal at any of those stages mutates nothing** — then begin a transaction, run the
128
+ * operations sequentially against provisional state, evaluate entity constraints,
129
+ * transition constraints and postconditions, and either commit everything or roll back
130
+ * every mutation.
131
+ *
132
+ * `invokeAction` returns the diagnostics of that invocation.
133
+ */
70
134
  export interface ActionDef extends NodeBase {
71
135
  kind: 'action';
72
136
  parameters?: ActionParameter[];
@@ -80,6 +144,8 @@ export interface ActionDef extends NodeBase {
80
144
  destructive?: boolean;
81
145
  requiresConfirmation?: boolean;
82
146
  confirmationMessage?: string;
147
+ /** What the confirmation says, when a plain message is not enough. */
148
+ confirmation?: ConfirmationPresentation;
83
149
  }
84
150
  /**
85
151
  * The conditions an action checks, however they were written. `guards` pairs each
@@ -92,30 +158,53 @@ export type OperationKind = Operation['kind'];
92
158
  export declare const OPERATION_KINDS: readonly OperationKind[];
93
159
  /** Every mutation is a set, an insert or a remove against an addressed Location. */
94
160
  export type MutationOperation = SetOperation | InsertOperation | RemoveOperation;
161
+ /**
162
+ * Writes a value to an addressed position.
163
+ *
164
+ * A missing field along the path is created. A missing **collection item** is not: an
165
+ * identity selector that matches nothing reports `LOCATION_RESOLUTION_FAILED`.
166
+ */
95
167
  export interface SetOperation {
96
168
  kind: 'set';
97
169
  target: Location;
98
170
  value: Expression;
99
171
  }
172
+ /**
173
+ * Adds a member to a collection. The constructed value is deep-cloned before it is stored.
174
+ *
175
+ * A newly inserted entity instance has no previous state, so **no transition constraint is
176
+ * evaluated for it**. Govern creation with an action guard or an entity constraint.
177
+ */
100
178
  export interface InsertOperation {
101
179
  kind: 'insert';
102
- /** A location addressing a collection. */
180
+ /** A location addressing a collection. A non-array current value is treated as `[]`. */
103
181
  target: Location;
104
182
  value: Expression;
183
+ /** Defaults to `'end'`. */
105
184
  position?: 'start' | 'end';
106
185
  }
186
+ /**
187
+ * Removes one member of a collection.
188
+ *
189
+ * A selector that matches nothing is a **no-op**: no mutation, no log entry and no
190
+ * diagnostic. Removing an existing instance *is* a transition, with the proposed value
191
+ * bound to nothing.
192
+ */
107
193
  export interface RemoveOperation {
108
194
  kind: 'remove';
109
195
  target: CollectionItemLocation;
110
196
  }
111
197
  /**
112
- * Performs a set of mutations once per member of a collection. The iteration is not a
113
- * transaction of its own: it runs inside the action's transaction, so a failure in any
114
- * iteration rolls the whole action back.
198
+ * Performs a set of mutations once per member of a collection.
199
+ *
200
+ * Iteration N observes provisional writes from previous iterations. The loop executes
201
+ * inside the containing action's transaction and opens none of its own, so any failure
202
+ * rolls back the complete action — every iteration included. The collection itself is
203
+ * evaluated **once**, before the first member is mutated.
115
204
  *
116
205
  * `scopeId` introduces an iteration scope. Nested expressions refer to the current member
117
206
  * as `ref(scopeId)`, and nested locations may use it to address the canonical record the
118
- * member points at.
207
+ * member points at. Nested operations must be mutations only.
119
208
  */
120
209
  export interface ForEachOperation {
121
210
  kind: 'for-each';
@@ -163,11 +252,22 @@ export interface NativeOperation {
163
252
  resultTarget?: Location;
164
253
  declaredEffects?: NativeEffect[];
165
254
  }
255
+ /**
256
+ * An invariant over proposed state, evaluated after every governed mutation.
257
+ *
258
+ * A constraint that cannot be evaluated counts as **violated**, never as satisfied.
259
+ */
166
260
  export interface ConstraintDef extends NodeBase {
167
261
  kind: 'constraint';
168
262
  expression: Expression;
169
- /** When set, the expression is evaluated once per instance of this entity. */
263
+ /**
264
+ * When set, the expression is evaluated once per canonical instance of this entity, with
265
+ * the instance bound to `ref(entityId)` — wherever that instance is stored, including
266
+ * nested inside another entity. Without it the expression is evaluated once, in the root
267
+ * scope.
268
+ */
170
269
  entityId?: NodeId;
270
+ /** Defaults to `'error'`. A `'warning'` is advice and **never blocks a write**. */
171
271
  severity?: 'error' | 'warning';
172
272
  message?: string;
173
273
  }
@@ -182,6 +282,13 @@ export interface ConstraintDef extends NodeBase {
182
282
  *
183
283
  * `previousScopeId` and `proposedScopeId` bind those two instances for the expression.
184
284
  * When the instance is being removed, the proposed scope is bound to nothing.
285
+ *
286
+ * "Previous" means committed state as it stood immediately before the **outermost**
287
+ * transaction began — not the previous operation, and not the previous iteration.
288
+ *
289
+ * A **newly inserted** instance has no previous state and is therefore **not evaluated**.
290
+ * Govern creation with an action guard or an entity constraint. The entity must declare
291
+ * `identityFieldId`; without one this rule is silently skipped.
185
292
  */
186
293
  export interface TransitionConstraintDef extends NodeBase {
187
294
  kind: 'transition-constraint';