@cynodia/axiom 0.5.2-alpha.1 → 0.6.1-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 +2 -2
- package/docs/ACTIONS_TRANSACTIONS.md +35 -1
- package/docs/AGENT_API.md +21 -2
- package/docs/AGENT_REFERENCE.md +111 -6
- package/docs/ANTI_PATTERNS.md +61 -2
- package/docs/AUTHORITY.md +583 -0
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/EXPRESSIONS.md +1 -1
- package/docs/GRAPH_MODEL.md +2 -2
- package/docs/LOCATIONS.md +1 -1
- package/docs/PRESENTATION.md +1 -1
- package/docs/RUNTIME.md +50 -2
- package/docs/SEMANTIC_CONTRACT.md +36 -4
- package/docs/STATE.md +6 -2
- package/docs/UI.md +67 -7
- package/docs/VALIDATION.md +20 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@ Axiom represents application behavior, state, UI structure and presentation as s
|
|
|
6
6
|
semantic data executed by generic runtimes. An application is a typed graph, not source
|
|
7
7
|
files: the JavaScript and HTML that reach the browser are output, and are never edited.
|
|
8
8
|
|
|
9
|
-
**Status: experimental / alpha (0.
|
|
9
|
+
**Status: experimental / alpha (0.6.0-alpha.x).** The API may change between alpha
|
|
10
10
|
releases. The documentation in `docs/` describes this exact version.
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npm install @cynodia/axiom
|
|
15
|
+
npm install @cynodia/axiom
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Canonical mental model
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Actions and transactions
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.6.1-alpha.1. An action is behavior expressed as data, executed as a transaction.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
{
|
|
@@ -40,6 +40,22 @@ result.diagnostics[0].details // { preconditionIndex: 2, failureMode: 'insuffic
|
|
|
40
40
|
|
|
41
41
|
`actionGuards(action)` returns the conditions however they were written.
|
|
42
42
|
|
|
43
|
+
### Guards evaluate in order, and the first failure stops
|
|
44
|
+
|
|
45
|
+
Declaration order is evaluation order. The first guard that does not hold refuses the
|
|
46
|
+
invocation, and evaluation stops there: later guards are not evaluated, and exactly one
|
|
47
|
+
`PRECONDITION_FAILED` is reported, naming that guard by position and by failure mode.
|
|
48
|
+
|
|
49
|
+
This is the contract, not an implementation detail, and it has three consequences worth
|
|
50
|
+
writing an action around:
|
|
51
|
+
|
|
52
|
+
- **A guard may rely on the guards before it.** Ordering `required(x)` ahead of a guard that reads a field of `x` is how the second one is kept evaluable.
|
|
53
|
+
- **A refusal names one cause.** Failures are not aggregated. An interface that wants to show every problem at once should express them as constraints, which are evaluated over the whole proposed state, rather than as guards.
|
|
54
|
+
- **A guard that cannot be evaluated fails.** It refuses, exactly as an unevaluable constraint counts as violated — never passes.
|
|
55
|
+
|
|
56
|
+
Aggregating guard failures would be a different feature with a different diagnostic shape,
|
|
57
|
+
and is deliberately not this one.
|
|
58
|
+
|
|
43
59
|
## Lifecycle
|
|
44
60
|
|
|
45
61
|
```text
|
|
@@ -62,6 +78,10 @@ re-render
|
|
|
62
78
|
`invokeAction(id, args?)` returns `{ ok, diagnostics }` for **that invocation**. No diffing
|
|
63
79
|
of global history is needed.
|
|
64
80
|
|
|
81
|
+
An action that writes server-authoritative state runs this lifecycle **on the authority**
|
|
82
|
+
instead, unchanged; the client dispatches it and receives the result. See
|
|
83
|
+
[`AUTHORITY.md`](AUTHORITY.md).
|
|
84
|
+
|
|
65
85
|
All four failure sources are evaluated; the action does not stop at the first.
|
|
66
86
|
|
|
67
87
|
## Operations
|
|
@@ -159,6 +179,20 @@ The controlled boundary for behavior the operation vocabulary cannot express.
|
|
|
159
179
|
**Use it only where no semantic primitive exists.** A native operation is opaque to every
|
|
160
180
|
analysis Axiom offers.
|
|
161
181
|
|
|
182
|
+
## Authorization
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
authorization?: Expression
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Whether the caller may invoke this action at all, evaluated **on the authority** with the
|
|
189
|
+
caller bound to `PRINCIPAL`, before any guard and before any transaction opens. It is
|
|
190
|
+
stripped from the client IR, so a client never learns the rule and cannot satisfy it by
|
|
191
|
+
claiming to.
|
|
192
|
+
|
|
193
|
+
A guard asks whether the application's state permits this invocation. Authorization asks
|
|
194
|
+
whether this *caller* may make it. See [`AUTHORITY.md`](AUTHORITY.md#authentication-and-authorization).
|
|
195
|
+
|
|
162
196
|
## Postconditions
|
|
163
197
|
|
|
164
198
|
Evaluated after the operations, inside the transaction, against proposed state. A failure
|
package/docs/AGENT_API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent API
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.6.1-alpha.1. The machine-facing interface. Agents query semantics and apply
|
|
4
4
|
structural transformations; they never edit generated code.
|
|
5
5
|
|
|
6
6
|
```ts
|
|
@@ -121,7 +121,8 @@ reported.
|
|
|
121
121
|
```ts
|
|
122
122
|
{
|
|
123
123
|
formId, density,
|
|
124
|
-
submitActionId?, // submitActionId, or the declared submit button's own action
|
|
124
|
+
submitActionId?, // submitActionId, or the declared submit button's own action —
|
|
125
|
+
// the same resolution execution, validation and presentation use
|
|
125
126
|
submitButtonId?, // set when the form declares its submit control
|
|
126
127
|
sections: [{ nodeId, name?, headings: string[], inputIds }],
|
|
127
128
|
ungroupedInputIds, // controls belonging to no section
|
|
@@ -141,6 +142,24 @@ Presentation resolution is recomputed per call rather than cached: a transaction
|
|
|
141
142
|
the graph underneath these queries, and a stale presentation answer would be worse than a
|
|
142
143
|
slow one.
|
|
143
144
|
|
|
145
|
+
## Authority queries
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
agent.getAuthority(stateId) // 'client' | 'server'
|
|
149
|
+
agent.getActionAuthority(actionId) // where it executes — derived, not declared
|
|
150
|
+
agent.getServerActions()
|
|
151
|
+
agent.getClientWritableStates()
|
|
152
|
+
agent.getServerWritableStates()
|
|
153
|
+
agent.getServerOnlyStates() // what the client never receives
|
|
154
|
+
agent.getActionsAffectingServerState() // [{ action, stateIds }]
|
|
155
|
+
agent.getAuthorizationForAction(actionId) // the rule, or undefined
|
|
156
|
+
agent.getUnauthorizedServerActions() // server actions any caller may invoke
|
|
157
|
+
agent.getPersistenceForState(stateId)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Authority is derived from what an action writes, so these answers cannot disagree with what
|
|
161
|
+
the graph does. See [`AUTHORITY.md`](AUTHORITY.md).
|
|
162
|
+
|
|
144
163
|
## Transactions
|
|
145
164
|
|
|
146
165
|
Every change is staged on a private copy. The graph an agent or a runtime can observe is
|
package/docs/AGENT_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent reference
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.6.1-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
|
|
4
4
|
declarations before authoring or modifying an Axiom application.
|
|
5
5
|
|
|
6
6
|
Formal guarantees: [`SEMANTIC_CONTRACT.md`](SEMANTIC_CONTRACT.md). Mistakes that compile:
|
|
@@ -31,7 +31,7 @@ One canonical term per concept. These are not interchangeable.
|
|
|
31
31
|
## Graph construction
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
|
-
const graph = new ApplicationGraph(id, name); // version defaults to '0.
|
|
34
|
+
const graph = new ApplicationGraph(id, name); // version defaults to '0.6.0'
|
|
35
35
|
graph.addNode<StateDef>({ id, kind: 'state', ... }); // returns NodeId; throws if id exists
|
|
36
36
|
graph.getNode<StateDef>(id); // deep clone, or undefined
|
|
37
37
|
graph.updateNode(node); // write a modified node back
|
|
@@ -408,13 +408,22 @@ not a constraint. Enforcement belongs to guards, constraints and transition cons
|
|
|
408
408
|
|
|
409
409
|
```ts
|
|
410
410
|
const ir = compileToIR(graph); // throws GraphValidationError if invalid
|
|
411
|
-
const html = compileToHtml(graph, { title?, appearance? });
|
|
411
|
+
const html = compileToHtml(graph, { title?, appearance?, remote? });
|
|
412
412
|
const css = createThemeStylesheet(ir.theme);
|
|
413
413
|
|
|
414
|
-
const app = createAxiomRuntime({ ir, rootElement, host, nativeOperations?, inputValidation?, recordMutationValues? });
|
|
415
|
-
app.start();
|
|
414
|
+
const app = createAxiomRuntime({ ir, rootElement, host, remote?, nativeOperations?, inputValidation?, recordMutationValues? });
|
|
415
|
+
await app.start(); // render → restore → load authoritative state
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
+
`start()` renders synchronously and then loads authoritative state when a gateway is
|
|
419
|
+
configured; awaiting it means that has happened. **The gateway must be passed to
|
|
420
|
+
`createAxiomRuntime`, before `start()`.** A failed load reports `AUTHORITY_UNREACHABLE` and
|
|
421
|
+
leaves `authoritativeStateLoaded()` false — it never throws, and never looks like empty data.
|
|
422
|
+
|
|
423
|
+
`compileToHtml` wires the browser-safe gateway into the generated page whenever the IR
|
|
424
|
+
contains a remote action, so a server-authoritative application needs no client JavaScript
|
|
425
|
+
of its own. `remote: { endpoint }` points it elsewhere; `remote: false` switches it off.
|
|
426
|
+
|
|
418
427
|
`compileToIR` refuses an invalid graph. Pass `{ validate: false }` only for diagnostics.
|
|
419
428
|
|
|
420
429
|
Hosts: `createBrowserHost()` for a page, `createMemoryHost()` for headless use. The runtime
|
|
@@ -431,7 +440,11 @@ reads nothing from globals.
|
|
|
431
440
|
| `diagnostics()` / `clearDiagnostics()` | — | Running log. |
|
|
432
441
|
| `getMutationLog()` | — | Every attempted mutation with source, path and `outcome`. |
|
|
433
442
|
| `registerNativeOperation(id, fn)` | — | |
|
|
434
|
-
| `start()` / `render()` | — | Rendering is a full re-render; focus and caret are restored by node id. |
|
|
443
|
+
| `start()` / `render()` | — | `start()` is render → restore → synchronize, and returns a promise. Rendering is a full re-render; focus and caret are restored by node id. |
|
|
444
|
+
| `invokeActionAsync(id, args?)` | yes | Awaits the outcome, an authority's answer included. |
|
|
445
|
+
| `syncAuthoritativeState()` | — | Loads and applies the authoritative snapshot. Idempotent. |
|
|
446
|
+
| `authoritativeStateLoaded()` | — | Whether a snapshot has been applied. Not the same question as "is this collection empty". |
|
|
447
|
+
| `settled()` | — | Resolves when no remote invocation is outstanding — how to await an action a click or a form submit started. |
|
|
435
448
|
|
|
436
449
|
## Diagnostics
|
|
437
450
|
|
|
@@ -469,6 +482,98 @@ agent.transact((tx) => { tx.setDensity(FORM, 'compact'); }, { reason });
|
|
|
469
482
|
a native operation with undeclared effects — cannot be analyzed. **An incomplete answer
|
|
470
483
|
says so; it is never presented as exhaustive.** Detail: [`AGENT_API.md`](AGENT_API.md).
|
|
471
484
|
|
|
485
|
+
## SERVER AUTHORITY
|
|
486
|
+
|
|
487
|
+
Full model: [`AUTHORITY.md`](AUTHORITY.md). These are the invariants to know before
|
|
488
|
+
authoring an application that crosses the trust boundary.
|
|
489
|
+
|
|
490
|
+
1. **AUTHORITY** — a client cannot commit server-authoritative state, by any path.
|
|
491
|
+
2. **EXECUTION** — a server action executes against state the authority owns, on the authority.
|
|
492
|
+
3. **TRUST** — a client's validation results, derived values and claims are never authoritative.
|
|
493
|
+
4. **TRANSACTION** — one semantic action commits atomically or not at all, wherever it runs.
|
|
494
|
+
5. **CONCURRENCY** — two actions cannot both commit from incompatible snapshots.
|
|
495
|
+
6. **PROTOCOL** — a client requests semantic actions, never mutation programs.
|
|
496
|
+
7. **SERIALIZATION** — authoritative behavior is data. No closure, no arbitrary code.
|
|
497
|
+
8. **BOOTSTRAP** — a remote client is given its gateway before it starts.
|
|
498
|
+
9. **STARTUP** — `start()` renders, then synchronizes; a failed load is a diagnostic.
|
|
499
|
+
10. **FORM SUBMIT** — a declared submit button invokes with its own arguments, clicked or submitted.
|
|
500
|
+
11. **IDEMPOTENCY** — a generated request id is unique across runtime instances; records are scoped by principal.
|
|
501
|
+
12. **CHANGES** — `changes` names every observable state whose value moved, and no others.
|
|
502
|
+
13. **PORTABILITY** — `axiom.server.v1` is frozen and language-independent.
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
{ id: STATE_PRODUCTS, kind: 'state', authority: 'server' } // the authority owns it
|
|
506
|
+
{ id: STATE_AUDIT, kind: 'state', authority: 'server', serverOnly: true } // and the client never sees it
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
- `authority` defaults to `'client'`. **Every 0.5.x graph is unchanged and still runs with no server.**
|
|
510
|
+
- Authority is separate from persistence: one says who decides a value, the other where a decided value survives.
|
|
511
|
+
- **Where an action executes is derived, never declared**: an action that writes any server-authoritative state is a server action.
|
|
512
|
+
- A server action reaches the client as its id, name and parameters only — no operations, no guards, no failure modes, no authorization.
|
|
513
|
+
- A server action MUST NOT read client state. Pass the value as an action parameter; that is what a draft is for.
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
compileToIR(graph) // the client half, filtered at the boundary
|
|
517
|
+
compileToServerIR(graph) // what an authority executes: no UI, no presentation, no routes
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Authorization
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
graph.setPrincipalEntity(ENTITY_USER);
|
|
524
|
+
{ kind: 'action', authorization: binary('eq', field(ref(PRINCIPAL), F_ROLE), literal('admin')), … }
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
`PRINCIPAL` is bound to a record keyed by the principal entity's field ids, and **only where
|
|
528
|
+
an authority evaluates** — which is everywhere on the authority, not only in `authorization`:
|
|
529
|
+
guards, operation values, postconditions and constraints alike. That is how a record says who
|
|
530
|
+
caused it without a client being asked to claim an identity:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
{ fieldId: F_ORDER_PLACED_BY, value: field(ref(PRINCIPAL), F_USER_ID) }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Reading it anywhere a client evaluates is `PRINCIPAL_REFERENCE_ON_CLIENT`. A rule that cannot
|
|
537
|
+
be evaluated denies. An anonymous caller has no principal: attributes read from it are absent,
|
|
538
|
+
so any rule naming one is false. `requiresConfirmation` is interaction, not authorization.
|
|
539
|
+
|
|
540
|
+
### Running one
|
|
541
|
+
|
|
542
|
+
One graph, one process — the generated page and the authority that answers it:
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
await serveAxiomApplication({
|
|
546
|
+
serverIR: compileToServerIR(graph),
|
|
547
|
+
page: compileToHtml(graph),
|
|
548
|
+
persistence: await createSqlitePersistence({ location: 'app.db' }),
|
|
549
|
+
authenticate: (credential) => resolveUser(credential),
|
|
550
|
+
port: 3000,
|
|
551
|
+
});
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
`GET /` is the page, `POST /axiom` the semantic endpoint. No route, controller, handler, SQL
|
|
555
|
+
or client JavaScript is authored. **There is no published Axiom CLI.** The halves also run
|
|
556
|
+
separately: `serveOverHttp({ server, port })` is the bare authority, and
|
|
557
|
+
`createDirectTransport(server)` drives one in-process for tests.
|
|
558
|
+
|
|
559
|
+
A remote invocation returns `{ ok: false, pending: true }` and its outcome arrives later,
|
|
560
|
+
through the same action-outcome lifecycle a local refusal uses — so a `diagnostic` node
|
|
561
|
+
presents a server refusal exactly as it presents a local one, and the control that started it
|
|
562
|
+
renders `aria-busy` and refuses a second press until it settles.
|
|
563
|
+
|
|
564
|
+
Portable artifacts, for a runtime written in another language:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
@cynodia/axiom-server/conformance the fixture manifest
|
|
568
|
+
@cynodia/axiom-server/conformance/<name>.json one fixture, pure data
|
|
569
|
+
@cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for the IR
|
|
570
|
+
@cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Boundary diagnostics: `UNKNOWN_SERVER_ACTION` `ARGUMENT_TYPE_MISMATCH` `AUTHORIZATION_DENIED`
|
|
574
|
+
`CONCURRENCY_CONFLICT` `MALFORMED_REQUEST` `AUTHORITY_UNREACHABLE`, plus `SERVER_STATE_WRITE`
|
|
575
|
+
and `REMOTE_ACTION_UNAVAILABLE` on the client.
|
|
576
|
+
|
|
472
577
|
## Serialization
|
|
473
578
|
|
|
474
579
|
The whole graph, theme included, is JSON. `graph.serialize()` /
|
package/docs/ANTI_PATTERNS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-patterns
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
3
|
+
Axiom 0.6.1-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
|
|
4
4
|
alternative.
|
|
5
5
|
|
|
6
6
|
## 1. Field names as entity runtime keys
|
|
@@ -334,7 +334,66 @@ Renderer-generated ids are keyed by **render instance**: `data-node` is the sema
|
|
|
334
334
|
all — see anti-pattern 3 — but a test or a host that must locate an element should select on
|
|
335
335
|
`data-node` and pick the instance it means.
|
|
336
336
|
|
|
337
|
-
## 26.
|
|
337
|
+
## 26. Binding an input into server-authoritative state
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
// WRONG — rejected by validateGraph (CLIENT_WRITE_TO_SERVER_STATE), and refused at run time.
|
|
341
|
+
{ kind: 'input', binding: { location: itemFieldLocation(STATE_PRODUCTS, F_ID, …, F_STOCK) } }
|
|
342
|
+
|
|
343
|
+
// RIGHT — edit a client draft, and commit it through an action the authority executes.
|
|
344
|
+
{ kind: 'input', binding: { location: stateLocation(STATE_DRAFT_QUANTITY) } }
|
|
345
|
+
{ kind: 'button', actionId: ACTION_PLACE_ORDER, arguments: { [PARAM_QUANTITY]: ref(STATE_DRAFT_QUANTITY) } }
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Do not force every keystroke across the boundary either: a draft is client-local on purpose.
|
|
349
|
+
|
|
350
|
+
## 27. A server action reading client state
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
// WRONG — the authority does not have the client's draft. SERVER_DEPENDS_ON_CLIENT_STATE.
|
|
354
|
+
{ kind: 'action', operations: [{ kind: 'insert', target: stateLocation(STATE_ORDERS), value: ref(STATE_DRAFT) }] }
|
|
355
|
+
|
|
356
|
+
// RIGHT — the client proposes values as arguments; the authority decides.
|
|
357
|
+
{
|
|
358
|
+
kind: 'action',
|
|
359
|
+
parameters: [{ id: PARAM_QUANTITY, valueType: primitiveType('number'), required: true }],
|
|
360
|
+
operations: [{ kind: 'insert', target: stateLocation(STATE_ORDERS), value: object([…]) }],
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## 28. Treating client-side checks as authoritative
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
// WRONG — a guard the client evaluated proves nothing. So does an argument saying so.
|
|
368
|
+
invoke(ACTION_PLACE_ORDER, { quantity: 5, alreadyValidated: true })
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The authority re-evaluates guards, constraints, transition rules, argument types and
|
|
372
|
+
authorization against **its own** state, every time. An extra argument is not a parameter and
|
|
373
|
+
is refused outright. Confirmation is interaction, never authorization.
|
|
374
|
+
|
|
375
|
+
## 29. Putting a secret in the graph
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
// WRONG — a graph is a serializable artifact, and this state ships to the client.
|
|
379
|
+
{ id: STATE_API_KEY, kind: 'state', valueType: primitiveType('string'), initialValue: 'sk-…' }
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Server-only information belongs behind `authority: 'server'` with `serverOnly: true`, and a
|
|
383
|
+
credential belongs to the host, not the graph.
|
|
384
|
+
|
|
385
|
+
## 30. Reaching for a native operation to make an external call
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
// WRONG — a database write rolls back; an email does not. Nothing makes this participate
|
|
389
|
+
// in the transaction, and pretending it does is worse than not having it.
|
|
390
|
+
{ kind: 'native', implementationId: 'app.sendEmail' }
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
External effects are deliberately unsolved in 0.6. A design involving commands, a
|
|
394
|
+
transactional outbox and idempotency is future work; do not smuggle one in meanwhile.
|
|
395
|
+
|
|
396
|
+
## 31. Restating what the model already knows
|
|
338
397
|
|
|
339
398
|
```ts
|
|
340
399
|
// UNNECESSARY — required comes from the field, the label from the field's name, the
|