@cratis/pi 7.36.0 → 7.37.0
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/package/corpus/hooks/scripts/cratis-nuget-pins.txt +6 -0
- package/package/corpus/product-sync.json +5 -1
- package/package/corpus/rules/orleans.md +4 -0
- package/package/corpus/rules/profiles.md +3 -3
- package/package/corpus/skills/cratis-arc-observable-query-http/SKILL.md +17 -0
- package/package/corpus/skills/cratis-arc-observable-query-http/verification.json +12 -0
- package/package/corpus/skills/cratis-arc-react-page/SKILL.md +5 -1
- package/package/corpus/skills/cratis-arc-react-page/verification.json +3 -0
- package/package/corpus/skills/cratis-chronicle-client-kotlin/SKILL.md +2 -2
- package/package/corpus/skills/cratis-components-common-controls/SKILL.md +149 -0
- package/package/corpus/skills/cratis-components-common-controls/verification.json +15 -0
- package/package/corpus/skills/cratis-components-data-surfaces/SKILL.md +185 -0
- package/package/corpus/skills/cratis-components-data-surfaces/verification.json +16 -0
- package/package/corpus/skills/cratis-fundamentals-telemetry/SKILL.md +125 -0
- package/package/corpus/skills/cratis-fundamentals-telemetry/verification.json +15 -0
- package/package/corpus/skills/cratis-orleans-jobs/SKILL.md +115 -0
- package/package/corpus/skills/cratis-orleans-jobs/references/state-machines.md +64 -0
- package/package/corpus/skills/cratis-orleans-jobs/verification.json +20 -0
- package/package/corpus/skills/cratis-screenplay-model-authoring/references/screen-authoring-workflow.md +6 -4
- package/package/corpus/skills/cratis-screenplay-read-surface/references/guarded-actions.md +3 -1
- package/package/corpus/skills/cratis-screenplay-render-and-gap-fill/SKILL.md +3 -2
- package/package/corpus/skills/cratis-screenplay-ui-composition/references/screen-composition-contract.md +7 -5
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/SKILL.md +4 -4
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/sandbox-and-specrunner.md +26 -0
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/verification.json +3 -0
- package/package/profile-catalog.json +6 -2
- package/package.json +1 -1
|
@@ -8,5 +8,11 @@ Cratis.Arc.Chronicle 22.75.0
|
|
|
8
8
|
Cratis.Chronicle 19.50.1
|
|
9
9
|
Cratis.Chronicle.CodeAnalysis 19.50.1
|
|
10
10
|
Cratis.Fundamentals 7.23.0
|
|
11
|
+
Cratis.Metrics.Roslyn 7.23.0
|
|
12
|
+
Cratis.OpenTelemetry 7.23.0
|
|
13
|
+
Cratis.Orleans 1.16.3
|
|
14
|
+
Cratis.Orleans.Chronicle 1.16.3
|
|
15
|
+
Cratis.Orleans.Storage.MongoDB 1.16.3
|
|
16
|
+
Cratis.Orleans.Storage.Sql 1.16.3
|
|
11
17
|
Cratis.Specifications 4.1.1
|
|
12
18
|
Cratis.Specifications.XUnit 4.1.1
|
|
@@ -230,7 +230,8 @@
|
|
|
230
230
|
},
|
|
231
231
|
"covers": {
|
|
232
232
|
"skills": [
|
|
233
|
-
"cratis-chronicle-hosting"
|
|
233
|
+
"cratis-chronicle-hosting",
|
|
234
|
+
"cratis-orleans-jobs"
|
|
234
235
|
],
|
|
235
236
|
"rules": [
|
|
236
237
|
"orleans.md"
|
|
@@ -384,6 +385,7 @@
|
|
|
384
385
|
"cratis-code-review",
|
|
385
386
|
"cratis-engineering-csharp-conventions",
|
|
386
387
|
"cratis-fundamentals-concept",
|
|
388
|
+
"cratis-fundamentals-telemetry",
|
|
387
389
|
"cratis-fundamentals-type-discovery"
|
|
388
390
|
],
|
|
389
391
|
"rules": [
|
|
@@ -411,6 +413,8 @@
|
|
|
411
413
|
"skills": [
|
|
412
414
|
"cratis-arc-react-page",
|
|
413
415
|
"cratis-components-accessibility",
|
|
416
|
+
"cratis-components-common-controls",
|
|
417
|
+
"cratis-components-data-surfaces",
|
|
414
418
|
"cratis-components-schema-editor",
|
|
415
419
|
"cratis-components-stepper-command-dialog",
|
|
416
420
|
"cratis-components-styling",
|
|
@@ -43,6 +43,10 @@ in its resilient storage pipeline.
|
|
|
43
43
|
|
|
44
44
|
## Co-host a silo with Cratis.Orleans
|
|
45
45
|
|
|
46
|
+
For job/step authoring, stage barriers, recovery, storage scopes and state-machine
|
|
47
|
+
APIs, load **cratis-orleans-jobs** from `cratis/engineering/csharp`. These APIs are
|
|
48
|
+
framework/service guidance; they do not impose application slice architecture.
|
|
49
|
+
|
|
46
50
|
For service authors, `Cratis.Orleans` supplies hosting, grain serializers, and the
|
|
47
51
|
shared job system without requiring Chronicle. With `Cratis.Orleans` and
|
|
48
52
|
`Cratis.Orleans.Storage.MongoDB` 1.16.3, this host uses durable MongoDB membership
|
|
@@ -73,8 +73,8 @@ profile includes neither Screenplay nor Stage.
|
|
|
73
73
|
|---|---|---|
|
|
74
74
|
| `cratis/arc` | Arc CQRS framework | `cratis/arc/csharp`, `cratis/arc/java`, `cratis/arc/kotlin` |
|
|
75
75
|
| `cratis/chronicle` | Chronicle event sourcing engine (does not compose `cratis/chronicle/mcp`) | `cratis/chronicle/compliance`, `cratis/chronicle/csharp`, `cratis/chronicle/elixir`, `cratis/chronicle/java`, `cratis/chronicle/kotlin`, `cratis/chronicle/multi-tenancy`, `cratis/chronicle/typescript`, `cratis/chronicle/web-workbench` |
|
|
76
|
-
| `cratis/components` | React component library | (no child profiles) |
|
|
77
|
-
| `cratis/fundamentals` | Core primitives (`
|
|
76
|
+
| `cratis/components` | React component library, including `cratis-components-common-controls` and `cratis-components-data-surfaces` | (no child profiles) |
|
|
77
|
+
| `cratis/fundamentals` | Core primitives and generated .NET telemetry (`cratis-fundamentals-telemetry`) | (no child profiles) |
|
|
78
78
|
| `cratis/arc/go` | Experimental Arc.Go HTTP CQRS (root module) | `cratis/language/go`, `cratis/fundamentals/go` |
|
|
79
79
|
| `cratis/chronicle/client-go` | Experimental standalone Chronicle.Go client | `cratis/language/go`, `cratis/fundamentals/go` |
|
|
80
80
|
| `cratis/fundamentals/go` | Fundamentals.Go named scalar concepts and codecs | `cratis/language/go` |
|
|
@@ -86,7 +86,7 @@ profile includes neither Screenplay nor Stage.
|
|
|
86
86
|
| Profile ID | Description | Automatically Includes |
|
|
87
87
|
|---|---|---|
|
|
88
88
|
| `cratis/engineering` | Engineering conventions and workflows | `cratis/engineering/core`, `cratis/engineering/csharp`, `cratis/engineering/elixir`, `cratis/engineering/go`, `cratis/engineering/kotlin`, `cratis/engineering/react`, `cratis/engineering/typescript` |
|
|
89
|
-
| `cratis/engineering/csharp` | C# engineering conventions | `cratis/engineering/core` |
|
|
89
|
+
| `cratis/engineering/csharp` | C# engineering conventions and framework Orleans jobs (`cratis-orleans-jobs`, alongside `orleans.md`) | `cratis/engineering/core` |
|
|
90
90
|
| `cratis/engineering/go` | Go framework parity and immutable module releases | `cratis/engineering/core`, `cratis/language/go` |
|
|
91
91
|
| `cratis/engineering/typescript` | TypeScript engineering conventions | `cratis/engineering/core` |
|
|
92
92
|
| `cratis/engineering/react` | React engineering conventions | `cratis/engineering/core` |
|
|
@@ -136,6 +136,23 @@ Sources at v22.75.0: `Arc.Core/Queries/ObservableQueryHttp.cs`,
|
|
|
136
136
|
`ObservableQueryHandler.cs` and
|
|
137
137
|
[MongoDB observations](https://github.com/Cratis/Arc/blob/v22.75.0/Documentation/backend/csharp/mongodb/observing-collections.md).
|
|
138
138
|
|
|
139
|
+
### Local `cratis` template ports
|
|
140
|
+
|
|
141
|
+
Templates `v1.7.4` still publishes MongoDB on host port `27017` and configures
|
|
142
|
+
`mongodb://localhost:27017` for the app (`Templates/Cratis/docker-compose.yml:3–13`,
|
|
143
|
+
`appsettings.json:35–37`). Its Vite dev server runs on `9000` and proxies `/api`,
|
|
144
|
+
`/.cratis` and `/swagger` to `http://localhost:5000`
|
|
145
|
+
(`Templates/Cratis/.frontend/vite.config.ts:24–39`). A backend started on another
|
|
146
|
+
port needs matching proxy targets; a healthy Chronicle container alone does not
|
|
147
|
+
prove the browser can reach the app or its MongoDB read models.
|
|
148
|
+
|
|
149
|
+
The bundled development MongoDB replica set advertises `localhost:27017`
|
|
150
|
+
(Chronicle `v19.34.0`, `Docker/entrypoint.sh:23–25`). For that setup, merely changing
|
|
151
|
+
Docker's host mapping to another port does not change the advertised member
|
|
152
|
+
address. Keep `27017` available, or deliberately reconfigure the replica set and
|
|
153
|
+
application connection together. These are local template defaults, not Arc
|
|
154
|
+
requirements or production topology advice.
|
|
155
|
+
|
|
139
156
|
## What comes back
|
|
140
157
|
|
|
141
158
|
Every transport carries the same `QueryResult` shape:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "cratis-arc-observable-query-http",
|
|
3
|
+
"input": "Our local cratis template query is stuck Loading after changing the MongoDB and backend host ports. Which connections should we check?",
|
|
4
|
+
"fixtures": [],
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "kind": "skill-contains", "value": "Templates `v1.7.4`" },
|
|
7
|
+
{ "kind": "skill-contains", "value": "mongodb://localhost:27017" },
|
|
8
|
+
{ "kind": "skill-contains", "value": "http://localhost:5000" },
|
|
9
|
+
{ "kind": "skill-contains", "value": "does not change the advertised member" },
|
|
10
|
+
{ "kind": "skill-contains", "value": "local template defaults, not Arc" }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -71,7 +71,11 @@ must move focus into itself.
|
|
|
71
71
|
`@cratis/components`) above every Cratis component — it carries the locale,
|
|
72
72
|
the Components-owned labels. Enable `toaster` (or mount one `<Toaster />`)
|
|
73
73
|
for notifications; without it even `toastCommandResult` displays nothing.
|
|
74
|
-
Mount exactly one toaster, not both.
|
|
74
|
+
Mount exactly one toaster, not both. The `cratis` template already mounts
|
|
75
|
+
`<CratisComponentsProvider toaster>` **inside** `<Arc>` at Templates `v1.7.4`
|
|
76
|
+
(`Templates/Cratis/.frontend/main.tsx:12–19`); the missing-toaster/outside-Arc
|
|
77
|
+
workaround for `v1.7.2` no longer applies to that template version. Check an
|
|
78
|
+
older application's entry point rather than adding a second toaster.
|
|
75
79
|
- The app must import the stylesheets explicitly; components do not import
|
|
76
80
|
their own CSS:
|
|
77
81
|
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
{ "kind": "skill-contains", "value": "DataPage has no snapshot refresh handle" },
|
|
14
14
|
{ "kind": "skill-contains", "value": "Mount exactly one toaster" },
|
|
15
|
+
{ "kind": "skill-contains", "value": "<CratisComponentsProvider toaster>" },
|
|
16
|
+
{ "kind": "skill-contains", "value": "Templates `v1.7.4`" },
|
|
17
|
+
{ "kind": "skill-contains", "value": "rather than adding a second toaster" },
|
|
15
18
|
{ "kind": "skill-contains", "value": "4.26.4" },
|
|
16
19
|
{ "kind": "skill-contains", "value": "checkbox multi-selection", "file": "references/data-tables.md" },
|
|
17
20
|
{ "kind": "skill-contains", "value": "DataPage never invokes it or refetches", "file": "references/data-page.md" }
|
|
@@ -247,8 +247,8 @@ store.eventLog.transactional.appendMany("<id>", listOf(<OtherEvent>()))
|
|
|
247
247
|
unitOfWork.commit()
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
-
`transactional` appends return
|
|
251
|
-
the unit of work (
|
|
250
|
+
In a unit of work, `transactional` appends return a placeholder `AppendResult`
|
|
251
|
+
(sequence `-1`); real results are on the unit of work after `commit()`.
|
|
252
252
|
|
|
253
253
|
## Observing
|
|
254
254
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-components-common-controls
|
|
3
|
+
description: Choose and bind Cratis Components ComboBox, ToggleGroup, Tabs, TagGroup and Breadcrumbs with controlled values, query states, keyboard semantics and localizable names. Use for entity pickers, exclusive choices, panel tabs, tag inputs or navigation trails. Do not use for Arc generated command fields, data tables, canvas palettes or theming.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
8
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
9
|
+
|
|
10
|
+
# Common controlled controls
|
|
11
|
+
|
|
12
|
+
## Verified product sources
|
|
13
|
+
|
|
14
|
+
All claims and prop shapes are from **Cratis/Components v4.26.4**, source rather than unversioned examples.
|
|
15
|
+
|
|
16
|
+
| Package | Version | Sources |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `@cratis/components` | `4.26.4` | `Source/Common/ComboBox.tsx:44-161,223-295`; `ToggleGroup.tsx:21-87,109-145`; `Tabs.tsx:23-79,91-132`; `TagGroup.tsx:49-145,155-260`; `Breadcrumbs.tsx:23-61,81-126` |
|
|
19
|
+
| React | `^19.0.0` peer | `Source/package.json:319-340` |
|
|
20
|
+
|
|
21
|
+
Imports are exported by `Source/Common/index.ts:11-15`; `Source/package.json:95-99` exposes the `Common` subpath. Use **cratis-components-styling** for the provider and CSS setup and **cratis-components-accessibility** for naming/focus. These controls do not replace Arc's generated `InputTextField`/other command fields; use **cratis-arc-react-page** for those bindings.
|
|
22
|
+
|
|
23
|
+
## Choose the semantic control first
|
|
24
|
+
|
|
25
|
+
| Need | Control | Controlled contract |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Pick one option, with search | `ComboBox` | `options: { key, label, description?, disabled? }[]`, `value: string \| null`, `onChange(key)` |
|
|
28
|
+
| Choose one value from a small set | `ToggleGroup` | `options: { value, label, icon?, disabled? }[]`, `value: string`, `onChange(value)` |
|
|
29
|
+
| Reveal a corresponding content panel | `Tabs` | `tabs: { id, label, content, disabled? }[]`, `value: string`, `onChange(id)` |
|
|
30
|
+
| Add/remove string values | `TagGroup` | `value: string[]`, `onChange(nextValues)` |
|
|
31
|
+
| Show where the page sits in navigation | `Breadcrumbs` | `items: { label, href?, onNavigate? }[]`, outermost first |
|
|
32
|
+
|
|
33
|
+
`ToggleGroup` is a single-selection value choice with radio-group semantics and one Tab stop. `Tabs` supplies real panels and tab/panel relationships. Do not use panel-less tabs as a fancy radio group (`ToggleGroup.tsx:78-87`, `Tabs.tsx:70-79`).
|
|
34
|
+
|
|
35
|
+
## 1. Bind a picker, not its display text
|
|
36
|
+
|
|
37
|
+
A controlled presentation component; the caller owns the selected key and supplies approved/localized text. `inputValue` is a separate search draft, not the selected value.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
41
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
42
|
+
|
|
43
|
+
import { ComboBox, type ComboBoxOption } from '@cratis/components/Common';
|
|
44
|
+
|
|
45
|
+
interface AccountPickerProps {
|
|
46
|
+
options: ComboBoxOption[];
|
|
47
|
+
value: string | null;
|
|
48
|
+
onChange: (key: string | null) => void;
|
|
49
|
+
label: string;
|
|
50
|
+
loading: boolean;
|
|
51
|
+
loadingMessage: string;
|
|
52
|
+
emptyMessage: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const AccountPicker = (props: AccountPickerProps) => (
|
|
56
|
+
<ComboBox
|
|
57
|
+
options={props.options}
|
|
58
|
+
value={props.value}
|
|
59
|
+
onChange={props.onChange}
|
|
60
|
+
aria-label={props.label}
|
|
61
|
+
loading={props.loading}
|
|
62
|
+
loadingMessage={props.loadingMessage}
|
|
63
|
+
emptyMessage={props.emptyMessage}
|
|
64
|
+
openOnFocus
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The host loads options; ComboBox never fetches them. For server-filtered results, supply `inputValue`, `onInputChange` and `filter='none'`; the caller debounces/cancels requests and discards stale answers. Local `filter` defaults to `'contains'`; `'startsWith'` is also supported. Explicit open/focus shows all options rather than filtering to the selected label (`ComboBox.tsx:170-177,223-266`).
|
|
70
|
+
|
|
71
|
+
Useful props (`:106-161`):
|
|
72
|
+
|
|
73
|
+
- `failure: ReactNode` replaces options with failure content. `loading` shows `loadingMessage`; `emptyMessage` is for a genuine empty result. Do not map denied/failed loads to an empty list without an explanation.
|
|
74
|
+
- `optionLayout='inline' | 'stacked'` controls secondary `description` placement.
|
|
75
|
+
- `action={{ label, onAction }}` offers a footer action receiving the current input text. It **does not select or create an option for you**. The handler may return a promise, but the component fires it without awaiting; the caller owns busy/error handling (`:268-287`).
|
|
76
|
+
- `required`, `invalid`, `errorMessage`, `description`, `disabled`, `readOnly`, `name` and `id` cover field state. `id` belongs to the input for `<label htmlFor>`. `name` carries the selected key, not search text (`:133-156`).
|
|
77
|
+
|
|
78
|
+
## 2. Choose a value or a panel
|
|
79
|
+
|
|
80
|
+
JSX binding excerpts inside an existing view. They assume caller-owned string values, change callbacks, and localized `labels` with the named properties:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { ToggleGroup, Tabs } from '@cratis/components/Common';
|
|
84
|
+
|
|
85
|
+
<ToggleGroup
|
|
86
|
+
aria-label={labels.viewMode}
|
|
87
|
+
value={viewMode}
|
|
88
|
+
onChange={changeViewMode}
|
|
89
|
+
options={[
|
|
90
|
+
{ value: 'compact', label: labels.compact },
|
|
91
|
+
{ value: 'expanded', label: labels.expanded }
|
|
92
|
+
]}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<Tabs
|
|
96
|
+
aria-label={labels.details}
|
|
97
|
+
value={selectedTab}
|
|
98
|
+
onChange={changeTab}
|
|
99
|
+
tabs={[
|
|
100
|
+
{ id: 'overview', label: labels.overview, content: overview },
|
|
101
|
+
{ id: 'history', label: labels.history, content: history }
|
|
102
|
+
]}
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`ToggleGroup.onChange` receives a string, not a DOM event; selecting the already-selected value is ignored. `Tabs.onChange` also receives an ID string. Both offer `disabled`, accessible labeling, `className` and typed `pt` parts. ToggleGroup additionally exposes `invalid` and `aria-describedby` (`ToggleGroup.tsx:52-76,116-121`; `Tabs.tsx:51-68`). Do not infer route navigation from changing a tab; its job is revealing content.
|
|
107
|
+
|
|
108
|
+
## 3. Add tags without confusing display with read-only
|
|
109
|
+
|
|
110
|
+
JSX excerpt in a view with caller-owned `tags: string[]`, `changeTags`, and localized labels:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { TagGroup } from '@cratis/components/Common';
|
|
114
|
+
|
|
115
|
+
<TagGroup
|
|
116
|
+
aria-label={labels.tags}
|
|
117
|
+
value={tags}
|
|
118
|
+
onChange={changeTags}
|
|
119
|
+
separators={[',', ';']}
|
|
120
|
+
removeLabel={tag => labels.removeTag.replace('{tag}', tag)}
|
|
121
|
+
/>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
TagGroup defaults to `editable=true`, comma separators and `allowDuplicates=false`. Enter or separators commit trimmed nonempty values; paste splits on separators. Empty-entry Backspace removes the last tag (`TagGroup.tsx:94-109,127-145,190-260`).
|
|
125
|
+
|
|
126
|
+
**`editable=false` only removes the adding input; existing tags remain removable.** Use `disabled` when removal must also be unavailable, or a display-only surface for noninteractive tags. Supply `removeLabel(value)` for localized accessible names. `removeIcon` overrides that group's glyph (ahead of provider `icons.remove`) without changing the button's name or keyboard behavior (`TagGroup.tsx:54-91,155-190`). Prefer unique values: removal is value-keyed, so this is not an identity-preserving collection editor (`:139-140`).
|
|
127
|
+
|
|
128
|
+
## 4. Build the breadcrumb trail from actual navigation
|
|
129
|
+
|
|
130
|
+
JSX excerpt in a view whose `labels` are localized:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { Breadcrumbs } from '@cratis/components/Common';
|
|
134
|
+
|
|
135
|
+
<Breadcrumbs
|
|
136
|
+
aria-label={labels.location}
|
|
137
|
+
items={[
|
|
138
|
+
{ label: labels.home, href: '/' },
|
|
139
|
+
{ label: labels.accounts, href: '/accounts' },
|
|
140
|
+
{ label: labels.currentAccount }
|
|
141
|
+
]}
|
|
142
|
+
/>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Earlier entries can supply `href` or router-owned `onNavigate`. The last item is current: its destination/callback is suppressed and React Aria supplies `aria-current='page'`. Separators are decorative; `separator` accepts any React node (`Breadcrumbs.tsx:97-126`). Supply a distinct landmark name if a page has multiple trails. Breadcrumbs do not discover routes or enforce access.
|
|
146
|
+
|
|
147
|
+
## Verify the binding
|
|
148
|
+
|
|
149
|
+
Test caller state changes using the real payload shapes (key/string/string array), not invented `{ value }` events. Exercise null selection, query failure, disabled options, panel content, tag removal and the final breadcrumb. Browser-check keyboard/focus and accessible names. `pt` keys belong to each exported `*Parts` interface; style those parts rather than overriding internal React Aria markup. Do not add undocumented props to work around a failed interaction.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "cratis-components-common-controls",
|
|
3
|
+
"input": "Choose between a value toggle and panel tabs, wire a nullable entity picker and disable tag editing correctly.",
|
|
4
|
+
"fixtures": [],
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "kind": "skill-contains", "value": "@cratis/components/Common" },
|
|
7
|
+
{ "kind": "skill-contains", "value": "ComboBoxOption" },
|
|
8
|
+
{ "kind": "skill-contains", "value": "inputValue" },
|
|
9
|
+
{ "kind": "skill-contains", "value": "radio-group semantics" },
|
|
10
|
+
{ "kind": "skill-contains", "value": "content: overview" },
|
|
11
|
+
{ "kind": "skill-contains", "value": "existing tags remain removable" },
|
|
12
|
+
{ "kind": "skill-contains", "value": "removeLabel" },
|
|
13
|
+
{ "kind": "skill-contains", "value": "aria-current='page'" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-components-data-surfaces
|
|
3
|
+
description: Compose Cratis Components PivotViewer exploration, TimeMachine historical snapshots, Canvas pan/zoom boards and public Chat conversation/sidebar surfaces. Use when a table is not the right presentation, when supplying Pixi-backed content, or when binding host-owned chat data and actions. Do not use for DataPage lists, backend event replay, chat persistence, canvas tool palettes or Scene authoring.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
8
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
9
|
+
|
|
10
|
+
# Data beyond a table
|
|
11
|
+
|
|
12
|
+
## Verified product sources
|
|
13
|
+
|
|
14
|
+
All API and behavior claims below use **Cratis/Components v4.26.4**.
|
|
15
|
+
|
|
16
|
+
| Package | Version | Source |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `@cratis/components` | `4.26.4` | `Source/PivotViewer/types.ts:67-112,145-187`; `Source/TimeMachine/{TimeMachine.tsx:19-65,types.ts:8-34}`; `Source/Canvas/{Canvas.tsx:38-104,CanvasItem.tsx:8-38,CanvasHandle.ts:7-21}`; `Source/Chat/{index.ts:4-40,ChatConversation.tsx:78-180,ChatSidebar.tsx:86-203}` |
|
|
19
|
+
| `pixi.js` | `^8.20.0` optional peer | `Source/package.json:319-340`; required when using Canvas or PivotViewer |
|
|
20
|
+
| React | `^19.0.0` peer | `Source/package.json:319-340` |
|
|
21
|
+
|
|
22
|
+
Subpath exports: `Source/package.json:60-69,155-159,178-182`; each area's `index.ts` exports the types used below. Load **cratis-components-styling** for CSS/provider setup and **cratis-components-accessibility** for localized names. For tabular queries use **cratis-arc-react-page**; for a board tool palette use **cratis-components-toolbar**.
|
|
23
|
+
|
|
24
|
+
These are **presentation contracts**. The host owns loading, access control, data acquisition and persistence. A historical slider is not event replay; a chat send callback is not a durable write.
|
|
25
|
+
|
|
26
|
+
## Choose the surface
|
|
27
|
+
|
|
28
|
+
| Surface | Fit | Avoid assuming |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `PivotViewer` | Faceted card exploration of an already-loaded collection | Server paging, JSX card rendering or controlled selection |
|
|
31
|
+
| `TimeMachine` | Supplied historical snapshots and event descriptions | Fetching history, rebuilding a read model or a controlled current index |
|
|
32
|
+
| `Canvas` | A pan/zoom board combining DOM items and optional Pixi objects | That moving the camera persists item positions |
|
|
33
|
+
| `ChatConversation` / `ChatSidebar` | Host-owned messages, topic selection and actions | Automatic message persistence, authorization or topic naming |
|
|
34
|
+
|
|
35
|
+
Keep Canvas/PivotViewer behind their subpath imports so ordinary pages do not acquire a Pixi import edge. Install/deduplicate the optional `pixi.js` peer for these areas, including a DOM-only Canvas: `Canvas.tsx:176` initializes Pixi through `usePixiApplication`. Do not rely on the word "optional" to omit it when importing those areas.
|
|
36
|
+
|
|
37
|
+
## 1. PivotViewer: data and text cards, not React cards
|
|
38
|
+
|
|
39
|
+
`data`, `dimensions` and `cardRenderer` are required. Each dimension has `key`, `label`, `getValue`; optional `formatValue` and `sort` control group presentation. **`cardRenderer` returns structured text, not JSX**. `detailRenderer(item, onClose)` may return a React node (`PivotViewer/types.ts:67-78,145-163`).
|
|
40
|
+
|
|
41
|
+
This component accepts loaded domain data; its labels come from the host's localization layer:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
45
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
46
|
+
|
|
47
|
+
import { PivotViewer } from '@cratis/components/PivotViewer';
|
|
48
|
+
|
|
49
|
+
interface CatalogItem {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
category: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CatalogExplorerProps {
|
|
56
|
+
items: CatalogItem[];
|
|
57
|
+
categoryLabel: string;
|
|
58
|
+
loadingLabel: string;
|
|
59
|
+
isLoading: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const CatalogExplorer = (props: CatalogExplorerProps) => (
|
|
63
|
+
<PivotViewer<CatalogItem>
|
|
64
|
+
data={props.items}
|
|
65
|
+
dimensions={[{ key: 'category', label: props.categoryLabel, getValue: item => item.category }]}
|
|
66
|
+
getItemId={item => item.id}
|
|
67
|
+
cardRenderer={item => ({ title: item.name, values: [item.category] })}
|
|
68
|
+
searchFields={[item => item.name]}
|
|
69
|
+
isLoading={props.isLoading}
|
|
70
|
+
loadingLabel={props.loadingLabel}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Other substantial props (`types.ts:93-112,145-185`):
|
|
76
|
+
|
|
77
|
+
- `filters` accepts keyed facets with `label` and `getValue`. Optional `options` are `{ key, label, value, count }`; otherwise they are computed from data. Numeric `type='number'` supports a range/histogram with `buckets`; `renderEditor` replaces a facet editor.
|
|
78
|
+
- `defaultDimensionKey` initializes grouping. Group/filter/selection state is internal; there is no `selectedItem`/`onSelectedItemChange` prop at this tag (`PivotViewer.tsx:54-112`).
|
|
79
|
+
- `searchFields` are property accessors. Use direct property paths, not computed expressions: the implementation extracts paths from accessor source (`types.ts:23-29`, `utils/search.ts`).
|
|
80
|
+
- `emptyContent`, `isLoading`, `loadingLabel`, `labels` and `colors` control presentation. Use the exported `PivotViewerLabels` shape for localization; do not put DOM markup in a text-card result.
|
|
81
|
+
|
|
82
|
+
Bound the dataset in the host. A worker and virtualized rendering do not make loading an unbounded collection a safe query shape (`PivotViewer.tsx:152-165`). Test actual browser rendering, resizing, zoom and keyboard behavior; a server-rendered shell does not exercise Pixi.
|
|
83
|
+
|
|
84
|
+
## 2. TimeMachine: prepare history before rendering
|
|
85
|
+
|
|
86
|
+
`Version` is `{ id: string, timestamp: Date, label: string, content: ReactNode, events?: Event[] }`. `Event` is `{ sequenceNumber: number, type: string, occurred: Date, content: Record<string, unknown> }`; it is a presentation DTO, not a Chronicle client object (`TimeMachine/types.ts:8-34`). Supply versions in the intended history order.
|
|
87
|
+
|
|
88
|
+
Binding excerpt for an existing view with `versions: Version[]`, a caller-owned `onVersionSelected(index)` callback and translated `timelineLabels: Partial<TimeMachineLabels>`:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { TimeMachine } from '@cratis/components/TimeMachine';
|
|
92
|
+
|
|
93
|
+
<TimeMachine
|
|
94
|
+
versions={versions}
|
|
95
|
+
currentVersionIndex={0}
|
|
96
|
+
onVersionChange={onVersionSelected}
|
|
97
|
+
labels={timelineLabels}
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**`currentVersionIndex` is initial-only**. Later prop changes do not replace internal selection. `onVersionChange(index)` reports timeline/wheel/previous/next selection; `scrollSensitivity` defaults to 50. The component offers read-model and event views over supplied data (`TimeMachine.tsx:19-65,73-125,133-145`). Do not promise an externally controlled scrubber or backend replay.
|
|
102
|
+
|
|
103
|
+
Load only authorized, redacted history. The presence of an event payload in this display is not permission to reveal it, and snapshot content is host-supplied React content. Exercise both modes and localization with the real DTOs.
|
|
104
|
+
|
|
105
|
+
## 3. Canvas: distinguish camera, DOM items and Pixi data
|
|
106
|
+
|
|
107
|
+
DOM content belongs in `Canvas` children. Position measured items with `CanvasItem x/y`, optional stable `id`, and optional `zIndex` on the item root (nested z-index cannot escape its stacking context). The item registry feeds minimap measurements (`CanvasItem.tsx:8-42,65-95`).
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
111
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
112
|
+
|
|
113
|
+
import { Canvas, CanvasItem } from '@cratis/components/Canvas';
|
|
114
|
+
|
|
115
|
+
export const Board = () => (
|
|
116
|
+
<Canvas style={{ height: '32rem' }} showMinimap minimapWorldWidth={1600} minimapWorldHeight={1200}>
|
|
117
|
+
<CanvasItem id='first-card' x={100} y={80}>
|
|
118
|
+
<article>First card</article>
|
|
119
|
+
</CanvasItem>
|
|
120
|
+
</Canvas>
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The literal card text is illustrative; use the host's approved/localized content. Give the board visible dimensions. For Pixi items, supply `items: T[]` extending `CanvasItemData` and `renderItem(item): PIXI.Container`; `onItemPointerDown` reports item activation, `onReady` receives `CanvasContext` (`Canvas.tsx:38-50`). Do not return React nodes from `renderItem`.
|
|
125
|
+
|
|
126
|
+
Camera/controls contract (`Canvas.tsx:51-104,119-160`, `CanvasHandle.ts:7-21`):
|
|
127
|
+
|
|
128
|
+
- `initialZoom` and `initialPan` are initial values, not a controlled camera. `onTransformChange(zoom, pan)` reports movement. Use `onHandleReady` and `CanvasHandle.smoothPanToWorld`/`smoothPanZoomToWorld` for navigation; `getContainerRect` and `getItemBounds` measure it.
|
|
129
|
+
- `minZoom`/`maxZoom` default to 0.1/5. `showControls` defaults true; `showMinimap` false. `minimapItems` can replace registry-derived rectangles.
|
|
130
|
+
- `controlsPlacement` is `'bottom-left' | 'bottom-right'`; `controlsLabels` and `controlsIcons` are typed override objects. `controlsFollowViewportInsets` defaults true, stepping aside by `--canvas-viewport-left/right`. Set false when the host already offsets the whole canvas, or the inset is applied twice.
|
|
131
|
+
- `readOnly` absorbs content interaction but leaves pan/zoom available. `backgroundDragPans=false` lets a product own background drag selection while wheel/middle-button/touch panning remain available.
|
|
132
|
+
|
|
133
|
+
Persist item edits in the host, not in camera callbacks. Browser-test gestures and dimensions. For jsdom tests provide a `ResizeObserver` substitute where required; Pixi integration still needs a real browser signal.
|
|
134
|
+
|
|
135
|
+
## 4. Public Chat: bind the host's messages and outcomes
|
|
136
|
+
|
|
137
|
+
The public `@cratis/components/Chat` exports **`ChatConversation`**, **`ChatSidebar`**, **`ChatSidebarForObservableQueries`**, **`ChatTopicList`** and their types. It does **not** export the Kit `Chat` component or expose a `Chat/Kit` package subpath (`Source/Chat/index.ts:4-40`, `Source/package.json`). **`Chat` is exported through `@cratis/components/Canvas`** via the Kit barrel (`Source/Canvas/index.ts:37-40`, `Source/Chat/Kit/index.ts:4-26`). Prefer the non-spatial Chat subpath for ordinary host-data conversations; importing Canvas brings its Pixi edge.
|
|
138
|
+
|
|
139
|
+
The Kit `Chat` has a different contract: required `messages`, `onSend(text)` and `onClose`; optional `variant`, `typingAuthors`, `currentUserId`, `onReact(messageId, emoji)`, `onAct(messageId)`, `canAct` and nested `labels`. Its messages use the Kit `ChatMessage` shape, not the public Chat subpath's message type. Reactions need both `currentUserId` and `onReact`. An optional `id` additionally publishes `ChatMessageAdded` over an available Arc messenger at send time, not after persistence succeeds (`Source/Chat/Kit/Chat.tsx:58-135`, `ChatMessage.ts`). Never count that notification as durable delivery, or mix `onSend` with the public conversation's `onSendMessage`.
|
|
140
|
+
|
|
141
|
+
The public `ChatMessage` has `id`, `topicId`, `authorId`, `body`, `timestamp: Date`, optional `mentions` and `metadata`. Identifiers use `ChatIdentifier`; resolve authors through `authorOf`, not an invented `authorName` property (`ChatMessage.ts:17-37`, `ChatConversation.tsx:78-127`).
|
|
142
|
+
|
|
143
|
+
Complete presentation wrapper using the real public types:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
147
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
148
|
+
|
|
149
|
+
import {
|
|
150
|
+
ChatConversation, ChatStatus,
|
|
151
|
+
type ChatConversationLabels, type ChatMention, type ChatMessage
|
|
152
|
+
} from '@cratis/components/Chat';
|
|
153
|
+
|
|
154
|
+
interface ConversationProps {
|
|
155
|
+
messages: ChatMessage[];
|
|
156
|
+
status: ChatStatus;
|
|
157
|
+
labels: ChatConversationLabels;
|
|
158
|
+
send: (body: string, mentions: ChatMention[]) => void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const Conversation = (props: ConversationProps) => (
|
|
162
|
+
<ChatConversation
|
|
163
|
+
messages={props.messages}
|
|
164
|
+
status={props.status}
|
|
165
|
+
labels={props.labels}
|
|
166
|
+
onSendMessage={props.send}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`onSendMessage` reports the trimmed body and mentions; persistence is the host's responsibility. Supply fresh authorized data after command success. Handle failure visibly rather than adding a fabricated message that looks saved.
|
|
172
|
+
|
|
173
|
+
`ChatStatus.Ready/Loading/Failed/Unauthorized` distinguish query outcomes. Existing messages remain during loading/failure; unauthorized replaces them and disables the composer (`ChatStatus.ts:5-14`, `ChatConversation.tsx:85`). This is UI behavior, not backend enforcement.
|
|
174
|
+
|
|
175
|
+
Additional public contracts:
|
|
176
|
+
|
|
177
|
+
- `actions` are typed `ChatMessageAction<TMessage>[]`; `renderMessageExtra(message)` supplies reaction/failure/detail content under the body. `mentionCandidates` or `resolveMentionCandidates` supplies available mentions (`ChatConversation.tsx:121-180`).
|
|
178
|
+
- `ChatSidebar` requires `open`, `onClose`, `topics`, `messages`, and `onSendMessage(topicId, body, mentions)`. `topicActions` is separate from inherited message `actions`; `renderHeaderActions(openTopic)` belongs to the sidebar (`ChatSidebar.tsx:86-163`).
|
|
179
|
+
- `selectedTopicId` is controlled when provided: ID opens a topic, null shows the list, omission uses internal selection. `onTopicSelected` reports selection. `topicsStatus` and `messagesStatus` are separate; do not show one topic's cached messages while another topic is loading (`:123-142`).
|
|
180
|
+
- `onStartTopic` returns a created identifier or undefined, optionally in a promise. `onRequestTopicName` asks the host to name an unnamed topic after its first message. Neither creates/saves/names anything on its own (`:144-180`).
|
|
181
|
+
- The sidebar defaults non-modal, right edge, `24rem`; `position`, `width`, `modal`, `labels`, and typed `pt` customize it (`:182-203`).
|
|
182
|
+
|
|
183
|
+
## Verify the surface, not only the props
|
|
184
|
+
|
|
185
|
+
Test host orchestration separately from Components: loading/failed/denied vs empty, stable IDs, data ordering, and persistence errors. Browser-check interaction/focus and all visible/localized labels. Do not infer remote fetch, tenant isolation, history reconstruction or message delivery from a rendered surface or a passing type check.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "cratis-components-data-surfaces",
|
|
3
|
+
"input": "Present loaded faceted data, history snapshots, a pan/zoom board and host-owned chat without inventing persistence.",
|
|
4
|
+
"fixtures": [],
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "kind": "skill-contains", "value": "cardRenderer` returns structured text, not JSX" },
|
|
7
|
+
{ "kind": "skill-contains", "value": "currentVersionIndex` is initial-only" },
|
|
8
|
+
{ "kind": "skill-contains", "value": "CanvasItem" },
|
|
9
|
+
{ "kind": "skill-contains", "value": "controlsFollowViewportInsets" },
|
|
10
|
+
{ "kind": "skill-contains", "value": "pixi.js" },
|
|
11
|
+
{ "kind": "skill-contains", "value": "Chat` is exported through `@cratis/components/Canvas`" },
|
|
12
|
+
{ "kind": "skill-contains", "value": "onSendMessage" },
|
|
13
|
+
{ "kind": "skill-contains", "value": "renderHeaderActions(openTopic)" },
|
|
14
|
+
{ "kind": "skill-contains", "value": "not backend enforcement" }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-fundamentals-telemetry
|
|
3
|
+
description: Instrument a .NET service with Cratis.Metrics.Roslyn generated counters, gauges, histograms and spans, and subscribe/export them through Cratis.OpenTelemetry. Use for partial instrumentation declarations, missing telemetry, host setup or metric-name upgrades. Do not use for operating a collector or for application audit records.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
8
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
9
|
+
|
|
10
|
+
# Generated metrics and traces
|
|
11
|
+
|
|
12
|
+
Use this when a service needs measurements and spans, not business facts. Keep event sourcing and audit records separate from telemetry.
|
|
13
|
+
|
|
14
|
+
## Verified product sources
|
|
15
|
+
|
|
16
|
+
All source locators below are in **Cratis/Fundamentals v7.23.0**. Read the tag, not a sibling checkout's HEAD.
|
|
17
|
+
|
|
18
|
+
| Package | Version | Source |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `Cratis.Fundamentals` | `7.23.0` | `Source/DotNET/Fundamentals/Metrics/{Counter,Gauge,Histogram}Attribute.cs`, `Traces/SpanAttribute.cs`, `Diagnostics/TagAttribute.cs` |
|
|
21
|
+
| `Cratis.Metrics.Roslyn` | `7.23.0` | `Source/DotNET/Metrics.Roslyn/MetricsSourceGenerator.cs:42-130,174-238,285-337,354-406`; `Templates/Metrics.hbs:11-123` |
|
|
22
|
+
| `Cratis.OpenTelemetry` | `7.23.0` | `Source/DotNET/OpenTelemetry/OpenTelemetryExtensions.cs:29-180,215-315`; `InstrumentationExtensions.cs:17-31`; `CratisOpenTelemetryOptions.cs:19-34` |
|
|
23
|
+
|
|
24
|
+
Registration evidence: `Source/DotNET/Fundamentals/DependencyInjection/ServiceCollectionExtensions.cs:27-54`. Unkeyed names: `Metrics/UnkeyedMeter.cs:17-23`, `Traces/UnkeyedActivitySource.cs:17-23` under the same Fundamentals source root. Upgrade context: `Documentation/csharp/metrics/roslyn.md` (Exported Metric Names) and `Documentation/csharp/opentelemetry.md`.
|
|
25
|
+
|
|
26
|
+
## 1. Declare instrumentation beside its owner
|
|
27
|
+
|
|
28
|
+
Reference `Cratis.Fundamentals` for runtime types and `Cratis.Metrics.Roslyn` for generation. Use a **static partial class in a namespace**, containing **static partial methods**. The generator's candidate filter requires static classes even for metrics; a merely partial instance class is not enough (`MetricsSourceGenerator.cs:42-53,84-93`). Prefer extension declarations with short, unqualified attribute/type names, as below.
|
|
29
|
+
|
|
30
|
+
This declaration file is complete; it assumes the application's existing `ReportWorker` class in `MyApplication.Reports`:
|
|
31
|
+
|
|
32
|
+
```csharp
|
|
33
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
34
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
35
|
+
|
|
36
|
+
using System.Diagnostics;
|
|
37
|
+
using Cratis.Diagnostics;
|
|
38
|
+
using Cratis.Metrics;
|
|
39
|
+
using Cratis.Traces;
|
|
40
|
+
|
|
41
|
+
namespace MyApplication.Reports;
|
|
42
|
+
|
|
43
|
+
public static partial class ReportInstrumentation
|
|
44
|
+
{
|
|
45
|
+
[Counter<int>("reports.completed", "Completed reports")]
|
|
46
|
+
internal static partial void Completed(this IMeter<ReportWorker> meter,
|
|
47
|
+
[Tag("reports.outcome")] string outcome);
|
|
48
|
+
|
|
49
|
+
[Gauge<int>("reports.pending", "Pending reports")]
|
|
50
|
+
internal static partial void Pending(this IMeter<ReportWorker> meter, int count);
|
|
51
|
+
|
|
52
|
+
[Histogram<double>("reports.duration", "Report duration", "s")]
|
|
53
|
+
internal static partial void Duration(this IMeter<ReportWorker> meter, double seconds);
|
|
54
|
+
|
|
55
|
+
[Span("reports.build", ActivityKind.Internal)]
|
|
56
|
+
internal static partial IActivityScope<ReportWorker> Build(
|
|
57
|
+
this IActivitySource<ReportWorker> source,
|
|
58
|
+
[Tag("reports.format")] string format);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- The first metric argument is `IMeter<T>` or `IMeterScope<T>`. A counter without a measurement increments by one. The first remaining parameter, in any position, whose type exactly matches the attribute's numeric type is the measurement, not a tag; avoid a second parameter of that type. Gauges and histograms require that measurement (`MetricsSourceGenerator.cs:285-337`).
|
|
63
|
+
- Remaining parameters are tags. Metric keys default to parameter names; span keys default to snake_case. `[Tag("key")]` from `Cratis.Diagnostics` overrides either (`:174-185,403`).
|
|
64
|
+
- `IMeterScope<T>` merges scope tags with method tags (`Templates/Metrics.hbs:34-38,67-71,96-100`). Do not put credentials, personal payloads or unbounded entity IDs in metric labels; choose bounded categories.
|
|
65
|
+
- Name, description and optional UCUM unit are distinct. A duration in seconds uses `"s"` and a measurement in seconds, not milliseconds (`HistogramAttribute.cs:14-29`, template `:109`).
|
|
66
|
+
- A span returns `IActivityScope<T>` and takes `IActivitySource<T>` first. Assign the returned scope with `using`; `CRT0001` checks disposal (`ActivityScopeUsingAnalyzer.cs:20`, `Templates/Metrics.hbs:118-122`).
|
|
67
|
+
|
|
68
|
+
Call-site excerpt inside the existing worker, with injected `IMeter<ReportWorker> meter` and `IActivitySource<ReportWorker> source`:
|
|
69
|
+
|
|
70
|
+
```csharp
|
|
71
|
+
using var activity = source.Build("pdf");
|
|
72
|
+
meter.Pending(3);
|
|
73
|
+
meter.Completed("succeeded");
|
|
74
|
+
meter.Duration(0.25);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This shows recording, not report execution. Both `AddBindingsByConvention()` and `AddSelfBindings()` register unkeyed `IMeter<>` and `IActivitySource<>` as singletons. For a plain host, use the normal type-discovery/convention setup from **cratis-fundamentals-type-discovery**; a generated method alone does not register its dependencies.
|
|
78
|
+
|
|
79
|
+
## 2. Subscribe the actual instrumentation name
|
|
80
|
+
|
|
81
|
+
**Unkeyed instruments use `typeof(T).FullName`**, not the metric name and not the host service name. `AddCratisInstrumentation()` subscribes only to `Cratis.*`. An application namespace such as `MyApplication.Reports.ReportWorker` needs an explicit meter/source subscription. This is the first check when generation works but nothing is exported.
|
|
82
|
+
|
|
83
|
+
Reference `Cratis.OpenTelemetry` explicitly for SDK/exporter setup. Host-setup excerpt for an existing `IHostApplicationBuilder builder` (including a `WebApplicationBuilder`), before building the host:
|
|
84
|
+
|
|
85
|
+
```csharp
|
|
86
|
+
using Cratis.OpenTelemetry;
|
|
87
|
+
using OpenTelemetry.Metrics;
|
|
88
|
+
using OpenTelemetry.Trace;
|
|
89
|
+
using MyApplication.Reports;
|
|
90
|
+
|
|
91
|
+
builder.AddCratisOpenTelemetry(options =>
|
|
92
|
+
{
|
|
93
|
+
options.ServiceName = "report-service";
|
|
94
|
+
options.ConfigureMetrics = metrics => metrics.AddMeter(typeof(ReportWorker).FullName!);
|
|
95
|
+
options.ConfigureTracing = tracing => tracing.AddSource(typeof(ReportWorker).FullName!);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`AddCratisOpenTelemetry` extends `IHostApplicationBuilder`. When the host already owns an OpenTelemetry builder, use `services.AddOpenTelemetry().WithCratis(configuration, configure)` instead; `WithCratis` extends `OpenTelemetryBuilder`. Do not add both setups. If the application only wants subscription helpers, `AddCratisInstrumentation()` extends both `TracerProviderBuilder` and `MeterProviderBuilder`, not the service collection (`OpenTelemetryExtensions.cs:29-71`, `InstrumentationExtensions.cs:24-31`).
|
|
100
|
+
|
|
101
|
+
Shared setup adds Cratis sources/meters, ASP.NET Core and HTTP client instrumentation, runtime metrics, and scoped/formatted logs. Application callbacks run before DI is built (`OpenTelemetryExtensions.cs:77-180`).
|
|
102
|
+
|
|
103
|
+
## 3. Configure export, without baking endpoints or secrets into code
|
|
104
|
+
|
|
105
|
+
At this tag, OTLP export is **opt-in**: no configured endpoint means no OTLP exporter. Environment values take precedence over configuration keys. Relevant standard keys (`OpenTelemetryExtensions.cs:215-315`):
|
|
106
|
+
|
|
107
|
+
| Key | Effect |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES` | Resource values override application defaults |
|
|
110
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Shared absolute HTTP/HTTPS endpoint |
|
|
111
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `_METRICS_ENDPOINT`, `_LOGS_ENDPOINT` | Signal-specific endpoints override the shared endpoint |
|
|
112
|
+
| `OTEL_EXPORTER_OTLP_PROTOCOL` (or signal-specific equivalent) | `http/protobuf` selects HTTP; otherwise gRPC |
|
|
113
|
+
| `OTEL_TRACES_EXPORTER`, `OTEL_METRICS_EXPORTER`, `OTEL_LOGS_EXPORTER` | If supplied, must include `otlp` for that signal to export |
|
|
114
|
+
| `OTEL_SDK_DISABLED=true` | Disables providers |
|
|
115
|
+
| `OTEL_METRIC_EXPORT_INTERVAL`, `OTEL_METRIC_EXPORT_TIMEOUT` | Metric reader timing in milliseconds |
|
|
116
|
+
| `OTEL_TRACES_SAMPLER`, `OTEL_TRACES_SAMPLER_ARG` | Sampler and ratio configuration |
|
|
117
|
+
|
|
118
|
+
For HTTP/protobuf, the shared endpoint gains `/v1/traces`, `/v1/metrics` or `/v1/logs`; signal-specific endpoints are used as supplied. An enabled invalid endpoint throws `InvalidOpenTelemetryEndpoint`. Treat exporter headers as secrets; obtain them through the approved environment/secret mechanism, never a snippet or committed configuration.
|
|
119
|
+
|
|
120
|
+
## Upgrade and prove it
|
|
121
|
+
|
|
122
|
+
- Fundamentals stopped bringing the OpenTelemetry SDK transitively in **7.20.2**; add the explicit telemetry package when migrating a consumer that relied on that dependency (`Documentation/csharp/opentelemetry.md`).
|
|
123
|
+
- Regeneration after the **7.20.0** unit correction can rename exported Prometheus metrics: descriptions are now HELP text, not units. Rebuild the consumer and update dashboards/alerts that used the old description-suffixed names (`metrics/roslyn.md`, template `:48-50,80,109`).
|
|
124
|
+
- Build with analyzers and warnings as errors. Inspect generated implementations for correct measurements/tags and scope disposal. Do not edit generated files.
|
|
125
|
+
- Exercise one code path with a listener or the configured collector: observe the counter increment, histogram unit/measurement and completed span. A build or `skill-contains` assertion is not proof of export. Check namespace subscriptions, SDK disablement, sampler and endpoint independently before diagnosing the collector.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "cratis-fundamentals-telemetry",
|
|
3
|
+
"input": "Generate service metrics and spans and diagnose why an application's unkeyed meter is not exported.",
|
|
4
|
+
"fixtures": [],
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "kind": "skill-contains", "value": "static partial class in a namespace" },
|
|
7
|
+
{ "kind": "skill-contains", "value": "Histogram<double>" },
|
|
8
|
+
{ "kind": "skill-contains", "value": "IActivityScope<ReportWorker>" },
|
|
9
|
+
{ "kind": "skill-contains", "value": "CRT0001" },
|
|
10
|
+
{ "kind": "skill-contains", "value": "typeof(T).FullName" },
|
|
11
|
+
{ "kind": "skill-contains", "value": "AddCratisOpenTelemetry" },
|
|
12
|
+
{ "kind": "skill-contains", "value": "no configured endpoint means no OTLP exporter" },
|
|
13
|
+
{ "kind": "skill-contains", "value": "descriptions are now HELP text, not units" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-orleans-jobs
|
|
3
|
+
description: Build recoverable work with Cratis.Orleans jobs and job-step grains, stage barriers, bounded startup and cleanup, storage scopes and state machines. Use for service or kernel job authors, job recovery, SQL schema setup, or grain lifecycle transitions. Framework guidance; do not use for operating live Chronicle jobs or for application command validation.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
8
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
9
|
+
|
|
10
|
+
# Recoverable Orleans jobs
|
|
11
|
+
|
|
12
|
+
For services and kernel contributors using Orleans. This is library-consumer guidance, not the application vertical-slice architecture. For live Chronicle operations use **cratis-chronicle-cli-operations** instead. Co-hosting conventions are in [orleans.md](../../rules/orleans.md).
|
|
13
|
+
|
|
14
|
+
## Verified product sources
|
|
15
|
+
|
|
16
|
+
All Orleans claims below are source reads from **Cratis/Orleans v1.16.3**.
|
|
17
|
+
|
|
18
|
+
| Packages | Version | Sources |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `Cratis.Orleans`, `Cratis.Orleans.Storage` | `1.16.3` | `Source/Orleans/Jobs/{Job.cs,Job.Steps.cs,Job.Stages.cs,JobStep.cs,IJobsManager.cs,JobsOptions.cs}`; `Source/Orleans.Storage/Jobs/*` |
|
|
21
|
+
| `Cratis.Orleans.Storage.MongoDB`, `Cratis.Orleans.Storage.Sql` | `1.16.3` | `Source/Orleans.Storage.MongoDB/{MongoDBJobsStorageOptions.cs,DatabaseNames.cs}`; `Source/Orleans.Storage.Sql/{SqlJobsStorage.cs,SqlJobsStorageOptions.cs,Jobs/Migrations/v1_0_0.cs}` |
|
|
22
|
+
| `Cratis.Orleans.Chronicle` | `1.16.3` | `Source/Orleans.Chronicle/StateMachines/EventSourcedStateMachine.cs:19-40,77-94,128-176`; [state-machine reference](references/state-machines.md) |
|
|
23
|
+
|
|
24
|
+
The API snippets cite their exact files and lines below. Product docs `Documentation/concepts/{jobs,storage-scopes,state-machines,event-sourced-state-machines}.md` supply the task vocabulary; source resolves contradictions.
|
|
25
|
+
|
|
26
|
+
## 1. Choose a job, not a detached task
|
|
27
|
+
|
|
28
|
+
A job plans recoverable work; each step is a grain and does the work. Use it for work that must outlive a request. It is **at-least-once**, not exactly-once: a step interrupted before a durable checkpoint can perform the same work again. Protect external effects with receiver-owned idempotency/deduplication; a checkpoint does not atomically commit an external write (`JobStep.cs:47-56`, `JobsOptions.cs:34-59`).
|
|
29
|
+
|
|
30
|
+
- Job: derive `Job<TRequest, TJobState>`; the request is a class implementing `IJobRequest`, the state derives `JobState` (`Job.cs:23-25`). Implement `PrepareSteps(TRequest)` returning `Task<IImmutableList<JobStepDetails>>` (`Job.Steps.cs:63-87`).
|
|
31
|
+
- Step: derive `JobStep<TRequest, TResult, TState>`; state derives `JobStepState`. Its constructor receives `[PersistentState(nameof(JobStepState), WellKnownGrainStorageProviders.JobSteps)] IPersistentState<TState>`, `IJobStepThrottle`, and `ILogger<JobStep<TRequest, TResult, TState>>` (`JobStep.cs:27-32`).
|
|
32
|
+
- Implement `PrepareStep(TRequest)` → `Task<Result<PrepareJobStepError>>`; `InitializeState(TRequest)` → `ValueTask`, called after preparation succeeds; and `PerformStep(TState, CancellationToken)` → `Task<Catch<JobStepResult>>` (`JobStep.cs:350-387`). `TResult` is the step's result type; the work method returns the framework's `JobStepResult` envelope, not `TResult` directly.
|
|
33
|
+
- Give the concrete job and step unique `[GrainType("product-specific-name")]` names. Use serializable request/state/result types. Do not reuse a class-derived grain name across assemblies (`Jobs/JobsManager.cs:31`, `Documentation/how-to/host-the-silo.md`, and `orleans.md`).
|
|
34
|
+
|
|
35
|
+
`PerformStep` runs **outside the activation context** with a state snapshot obtained through the grain reference. Mutating that snapshot does not persist it; do not access/write grain `State` directly from background work (`JobStep.cs:375,390-450`). Keep progress/state coordination on grain calls. Since 1.16.2 the framework owns stop reporting; do not report cancellation again from a `PerformWork` override (`:390-450`, `Documentation/concepts/jobs.md:117-120`).
|
|
36
|
+
|
|
37
|
+
## 2. Plan dependencies as stages
|
|
38
|
+
|
|
39
|
+
Default steps all use `JobStepStage.First`. Prefer independent fan-out. Use stages only when correctness requires earlier work to finish (`Job.Stages.cs:20-44`).
|
|
40
|
+
|
|
41
|
+
Planning excerpt inside an existing `Job<RecoveryRequest, RecoveryState>`. It assumes `RecoveryRequest : IJobRequest`, `RecoveryState : JobState`, and the two existing grain interfaces `IRepairIndex` and `IRebuildCatalog`, each deriving a typed `IJobStep<,,>`:
|
|
42
|
+
|
|
43
|
+
```csharp
|
|
44
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
45
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
46
|
+
|
|
47
|
+
protected override Task<IImmutableList<JobStepDetails>> PrepareSteps(RecoveryRequest request) =>
|
|
48
|
+
Task.FromResult<IImmutableList<JobStepDetails>>(
|
|
49
|
+
[
|
|
50
|
+
CreateStep<IRepairIndex>(request, stage: 0),
|
|
51
|
+
CreateStep<IRebuildCatalog>(request, stage: 10)
|
|
52
|
+
]);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Import `System.Collections.Immutable` and `Cratis.Orleans.Jobs` in that job file. `CreateStep<TStep>` takes the **grain interface**, not the implementation (`Job.Steps.cs:69-79`). Its stage overload accepts `JobStepStage`, with implicit integer conversion (`Source/Orleans.Storage/Jobs/Stages/JobStepStage.cs`).
|
|
56
|
+
|
|
57
|
+
Every step in a stage reaches an outcome before later stages start; steps within a stage run in parallel. All steps are prepared up front, so later-stage preparation cannot read the effects of earlier-stage execution. Read such dependencies in `PerformStep` (`Job.Stages.cs:27-33`).
|
|
58
|
+
|
|
59
|
+
A failed stage stops the pipeline by default: later steps become `Unreachable`, and the job completes with failures. A best-effort stage may override `GetFailureBehaviorFor(JobStepStage)` to return `JobStageFailureBehavior.ContinueWithNextStage` (`Job.Stages.cs:40-44,152-194`, namespace `Cratis.Orleans.Jobs.Stages`). Do not silently continue when a later stage relies on the failed output.
|
|
60
|
+
|
|
61
|
+
## 3. Start and resume through the scoped manager
|
|
62
|
+
|
|
63
|
+
`IGrainFactory.GetJobsManager(scope, namespace)` resolves the manager (`JobsManagerExtensions.cs:18-19`). `IJobsManager.Start<TJob,TRequest>(request)` returns `Result<JobId, StartJobError>`; inspect its error before telling a caller that a job started (`IJobsManager.cs:37-52`). Start success is not completed work; observe the job's status/outcomes.
|
|
64
|
+
|
|
65
|
+
`IJobsManager.Resume(JobId)` returns **`Task<bool>`**. False means nothing took the job; do not wait for a completion callback as though resumption succeeded. True can mean resumed, already running, or finalized as completed (`IJobsManager.cs:54-64`). `Rehydrate()` completes after loading/preparing interrupted jobs, **before** its bounded background drain has resumed them all (`:15-28`).
|
|
66
|
+
|
|
67
|
+
`StartStepsInBackground` defaults to false. Overriding it to true makes Start return once the job is durable, moving step preparation/start to the grain's next turn. Later startup errors belong to the job, not the Start response (`Job.cs:74-87,395-427`).
|
|
68
|
+
|
|
69
|
+
## 4. Know what storage retains
|
|
70
|
+
|
|
71
|
+
Successful jobs clear themselves by default; override `KeepAfterCompleted` to retain job state. Jobs completed with failures default to `KeepAfterCompletedWithFailures = true`, but retention cleanup still applies (`Job.cs:58-71`, `JobsOptions.cs:80-97`).
|
|
72
|
+
|
|
73
|
+
**Keeping a job does not keep successful step state.** The step provider deletes successful/removing state, moves failed state to failed-step storage, and saves stopped/scheduled/running/unreachable state (`JobStepGrainStorageProvider.cs:64-84`). SQL keeps failed step rows with a failure status rather than a separate collection (`Source/Orleans.Storage.Sql/Jobs/JobStepStorage.cs`, `MoveToFailed`). Copy results the job must retain through `OnStepCompletedOrStopped(JobStepId, JobStepResult)` into job state (`Job.Steps.cs:49-55,110-117`); do not read deleted successful step state later.
|
|
74
|
+
|
|
75
|
+
Deliver external callbacks to a method on the **job's grain interface**, update job state there and persist it with `WriteState()`. Preserve the returned `Catch` failure; do not acknowledge input whose write failed (`Job.State.cs:13-25`, `WriteState`; `Documentation/concepts/jobs.md:122-164`). Step state is not a callback inbox: a concurrent callback can race the final write, and successful completion deletes it. A running step's snapshot will not see later job/step writes automatically.
|
|
76
|
+
|
|
77
|
+
## 5. Register storage and budget concurrency
|
|
78
|
+
|
|
79
|
+
`IJobsStorage.GetFor(scope, namespace)` returns `JobsStorage`. Scope and namespace are opaque engine coordinates carried in grain keys; the host/provider owns their mapping (`Source/Orleans.Storage/Jobs/IJobsStorage.cs`, `Job.cs:120-125`). They are storage boundaries, not authorization checks.
|
|
80
|
+
|
|
81
|
+
Choose one storage registration alongside `AddCratisOrleans`:
|
|
82
|
+
|
|
83
|
+
| Registration | Package / namespace | Use |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `AddCratisOrleansMongoDBJobsStorage` | `.Storage.MongoDB` / `Cratis.Orleans.Setup` | Durable MongoDB; database-name resolver can override the default `{scope}+jobs[+{namespace}]` |
|
|
86
|
+
| `AddCratisOrleansSqlJobsStorage` | `.Storage.Sql` / `Cratis.Orleans.Setup` | Durable EF Core; `OptionsResolver(scope, namespace)` selects typed `DbContextOptions<JobsDbContext>` |
|
|
87
|
+
| `AddCratisOrleansInMemoryJobsStorage` | `.Storage` / `Cratis.Orleans.Storage` | Specs/development only; process restart loses state |
|
|
88
|
+
|
|
89
|
+
Evidence: each package's `*JobsStorageServiceCollectionExtensions.cs`; MongoDB `DatabaseNames.cs` and `MongoDBJobsStorageOptions.cs`; SQL `SqlJobsStorageOptions.cs:15-27`; in-memory `InMemory/InMemoryJobsStorage.cs`.
|
|
90
|
+
|
|
91
|
+
### SQL schema contradiction resolved
|
|
92
|
+
|
|
93
|
+
The product's SQL setup docs say to create the schema yourself. **At v1.16.3 the provider applies migrations automatically**: `SqlJobsStorage.GetFor` calls `EnsureSchema`, which calls `dbContext.Database.Migrate()` before constructing storage (`Source/Orleans.Storage.Sql/SqlJobsStorage.cs:33-49,58-74`). The migration is `Jobs/Migrations/v1_0_0.cs`; do not substitute `EnsureCreated` or hand-create those tables. The connection needs permissions to apply the migration, and scope-to-database mapping must be deliberate. Storage is cached per scope/namespace; `IJobsStorage.Reset()` clears the cache and the next resolution migrates again (`SqlJobsStorage.cs:52-56`). This resolves the documentation conflict from source, not from a release-note inference.
|
|
94
|
+
|
|
95
|
+
Configure `JobsOptions` through the host's options registration. Its properties are **init-only**: supply a constructed instance via the options mechanism rather than assigning them in a `Configure(options => options.Property = …)` callback (`JobsOptions.cs`).
|
|
96
|
+
|
|
97
|
+
| Option | Default | Purpose |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `MaxParallelSteps` | `max(1, ProcessorCount - 1)` when unset | Actual execution throttle |
|
|
100
|
+
| `MaxConcurrentStepStarts` | `16` | Startup/resume calls per job, not the execution limit |
|
|
101
|
+
| `MaxConcurrentRehydration` | `8` | Jobs resumed concurrently by a manager |
|
|
102
|
+
| `MaxConcurrentCleanup` | `4` | Dead-job deletions |
|
|
103
|
+
| `StepCheckpointBatchInterval` | `100` | Batches between durable progress checkpoints |
|
|
104
|
+
| `StepCheckpointFlushInterval` | `5 seconds` | Timed checkpoint flush; nonpositive disables it |
|
|
105
|
+
| `DeadJobThreshold`, `CleanupCadence` | `1 hour` each | Preparation-with-no-steps scavenging |
|
|
106
|
+
| `TerminalJobRetention` | `7 days` | Terminal history retention; `Timeout.InfiniteTimeSpan` retains indefinitely |
|
|
107
|
+
| `MaxTerminalJobsPerCleanup` | `100` | Bounded oldest-first cleanup page |
|
|
108
|
+
|
|
109
|
+
Source: `JobsOptions.cs:17-109`. The three concurrency options for starts/rehydration/cleanup treat values below one as one. Retention does not delete preparing/running/stopped/removing jobs. Larger checkpoint intervals mean more post-crash redelivery, not higher delivery guarantees.
|
|
110
|
+
|
|
111
|
+
## State machines and verification
|
|
112
|
+
|
|
113
|
+
Use [state-machines.md](references/state-machines.md) for `StateMachine<TStoredState>` and `EventSourcedStateMachine<TModel>`: jobs are recoverable work, state machines are lifecycle behavior. Neither makes external effects exactly-once.
|
|
114
|
+
|
|
115
|
+
Verify a new job with specs for planning, idempotent redelivery, stop/resume (including false), failed barriers/Unreachable, retention and persistence failures. Exercise the selected durable provider for restart recovery and SQL migration permissions. A build alone cannot establish crash recovery. Do not operate a live store merely to validate a code example.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Grain lifecycle state machines
|
|
5
|
+
|
|
6
|
+
Verified from **Cratis/Orleans v1.16.3**. Use explicit states when entering/leaving a status has behavior; an enum is enough for a status with no behavior. Do not put long-running work on a grain turn just because `OnEnter` is available.
|
|
7
|
+
|
|
8
|
+
## Stored lifecycle
|
|
9
|
+
|
|
10
|
+
`StateMachine<TStoredState>` derives from a stateful grain. Override `InitialState` and `CreateStates()`; states derive `State<TStoredState>`. `AllowedTransitions` is an immutable list of target types, or override `CanTransitionTo<TTargetState>(storedState)` for a guard (`Source/Orleans/StateMachines/StateMachine.cs:63-67,120-192`; `State.cs:17-34`).
|
|
11
|
+
|
|
12
|
+
Declaration excerpt for an existing grain file with `ProcessState : StateMachineState`, the existing `IProcess` grain interface, and a registered `processes` grain storage provider:
|
|
13
|
+
|
|
14
|
+
```csharp
|
|
15
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
16
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
17
|
+
|
|
18
|
+
using System;
|
|
19
|
+
using System.Collections.Immutable;
|
|
20
|
+
using System.Threading.Tasks;
|
|
21
|
+
using Cratis.Orleans.StateMachines;
|
|
22
|
+
using Orleans;
|
|
23
|
+
using Orleans.Providers;
|
|
24
|
+
|
|
25
|
+
[GrainType("my-product-process")]
|
|
26
|
+
[StorageProvider(ProviderName = "processes")]
|
|
27
|
+
public class Process : StateMachine<ProcessState>, IProcess
|
|
28
|
+
{
|
|
29
|
+
protected override Type InitialState => typeof(Pending);
|
|
30
|
+
|
|
31
|
+
public override IImmutableList<IState<ProcessState>> CreateStates() =>
|
|
32
|
+
[new Pending(), new Finished()];
|
|
33
|
+
|
|
34
|
+
public Task Finish() => TransitionTo<Finished>();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public class Pending : State<ProcessState>
|
|
38
|
+
{
|
|
39
|
+
protected override IImmutableList<Type> AllowedTransitions => [typeof(Finished)];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public class Finished : State<ProcessState>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Split types into their house-style files in the consumer; the block collects declarations to show the API. Do not copy the `processes` provider name into Chronicle's kernel; use its registered constants.
|
|
46
|
+
|
|
47
|
+
A refused transition is a **no-op**, not an exception or a successful change. An unknown target throws `UnknownStateTypeInStateMachine`. The machine runs leaving hooks, `OnLeave`, entering hooks, `OnEnter`, then persistence (`StateMachine.cs:162-169,222-290`). Transitions requested while entering are scheduled; transitions while leaving throw `TransitioningDuringOnLeaveIsNotSupported`.
|
|
48
|
+
|
|
49
|
+
When stored state derives `StateMachineState`, `CurrentState` stores the state's full type name; activation resolves it, enters it again, and can repeat enter effects. Renames need a stored-value migration; an unknown recorded name throws `UnknownCurrentState` (`StateMachine.cs:183-191,261-264`). A self-transition with the **same state instance** skips persistence: return a new stored-state instance when a self-transition must write (`:266-272`).
|
|
50
|
+
|
|
51
|
+
For externally owned storage, the protected constructor accepts `Orleans.Core.IStorage<TStoredState>` and reads it on activation before the activation hook. Override `ResolveActivationState()` to derive lifecycle from authoritative state instead of persisting a second status (`:39-51,68-95,172-192`).
|
|
52
|
+
|
|
53
|
+
## Lifecycle following Chronicle facts
|
|
54
|
+
|
|
55
|
+
`EventSourcedStateMachine<TModel>` from `Cratis.Orleans.Chronicle.StateMachines` uses a Chronicle read model as authoritative state. Its interface derives `IEventSourcedStateMachine` (string-keyed). There is **no grain storage provider** to configure and writes are no-ops: `ReadModelStorage<TModel>` reads Chronicle; `ResolveState(TModel?)` derives activation state (`Source/Orleans.Chronicle/StateMachines/EventSourcedStateMachine.cs:19-40,77-94,155-176`; `ReadModelStorage.cs:68-95`).
|
|
56
|
+
|
|
57
|
+
- Define states with `CreateStates()` as above, over `TModel`.
|
|
58
|
+
- Define event transitions in `DefineTransitions(IStateMachineTransitions<TModel>)`, using `.On<TEvent>().When(model => predicate).TransitionTo<TState>()`. First applicable allowed transition wins. Make `ResolveState` agree with these transitions after restart (`EventSourcedStateMachine.cs:142-176`, `StateMachineTransitions.cs`).
|
|
59
|
+
- Use model-bound projection attributes, an already-discovered projection, or `DefineProjection(IProjectionBuilderFor<TModel>)`, not competing projections. `DefineProjection` and `DefineTransitions` run at startup **without a constructor**: use only the builder, never injected services or fields (`EventSourcedStateMachine.cs:34-37`).
|
|
60
|
+
- Register `AddEventSourcedStateMachines()` on `ISiloBuilder` or `IServiceCollection`, beside the application's Chronicle client registration; namespace `Cratis.Orleans.Chronicle.Hosting` (`Hosting/EventSourcedStateMachinesServiceCollectionExtensions.cs:30-55`).
|
|
61
|
+
- Grain keys include event store, namespace and event-source ID. Use `IGrainFactory.GetEventSourcedStateMachine<TInterface>(eventStore, eventSourceId)` from `Cratis.Orleans.Chronicle.StateMachines` so tenant coordinates come from the supplied event store (`StateMachines/EventSourcedStateMachineGrainFactoryExtensions.cs:21-52`).
|
|
62
|
+
- Prefer passive models. For events consumed by a materialized projection, the reactor waits for its sequence position before refreshing; `ModelCatchUpTimeout` defaults to 30 seconds and expiry fails with `ModelDidNotCatchUp` (`EventSourcedStateMachineReactor.cs:58-89`; `EventSourcedStateMachinesOptions.cs:26`).
|
|
63
|
+
|
|
64
|
+
This machine **follows facts**, it does not arbitrate whether a command may append one. Keep command rejection and append-time invariants at their owning boundary. Enter/model-change effects may repeat on activation, redelivery or replay: design them idempotently (`Documentation/concepts/event-sourced-state-machines.md:240-247`). Test guards, no-op refusals, reactivation and persistence failures separately from job recovery.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "cratis-orleans-jobs",
|
|
3
|
+
"input": "Author a staged recoverable job with SQL storage and distinguish lifecycle state machines from job execution.",
|
|
4
|
+
"fixtures": [],
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "kind": "skill-contains", "value": "at-least-once" },
|
|
7
|
+
{ "kind": "skill-contains", "value": "Task<Catch<JobStepResult>>" },
|
|
8
|
+
{ "kind": "skill-contains", "value": "CreateStep<IRepairIndex>(request, stage: 0)" },
|
|
9
|
+
{ "kind": "skill-contains", "value": "Unreachable" },
|
|
10
|
+
{ "kind": "skill-contains", "value": "Task<bool>" },
|
|
11
|
+
{ "kind": "skill-contains", "value": "Keeping a job does not keep successful step state" },
|
|
12
|
+
{ "kind": "skill-contains", "value": "dbContext.Database.Migrate()" },
|
|
13
|
+
{ "kind": "skill-contains", "value": "MaxConcurrentStepStarts" },
|
|
14
|
+
{ "kind": "skill-contains", "value": "StateMachine<TStoredState>" },
|
|
15
|
+
{ "kind": "skill-contains", "value": "EventSourcedStateMachine<TModel>" },
|
|
16
|
+
{ "kind": "skill-contains", "file": "references/state-machines.md", "value": "A refused transition is a **no-op**" },
|
|
17
|
+
{ "kind": "skill-contains", "file": "references/state-machines.md", "value": "without a constructor" },
|
|
18
|
+
{ "kind": "skill-contains", "file": "references/state-machines.md", "value": "same state instance" }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -168,10 +168,12 @@ Historical verified public vector and executable checks:
|
|
|
168
168
|
- render (`cratis render <corpus-folder> --name ScreenComposition --destination
|
|
169
169
|
<dir> -o json`) with CLI 3.43.0 publishes the corpus; the browser acceptance
|
|
170
170
|
run on CLI 3.40.7 passed 51 assertions (Cratis/Screenplay#605);
|
|
171
|
-
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
- guarded Close and double-click: refused with `STAGE-SCENE-ACTION-001` and
|
|
172
|
+
`STAGE-SCENE-INTERACTION-001` on this historical vector, rendered and run since
|
|
173
|
+
Stage 4.65 (live browser run on CLI 3.53.1 with `cratis/stage:4.73.1`,
|
|
174
|
+
Cratis/Screenplay#671); the codes remain for a guard Stage cannot evaluate. Studio
|
|
175
|
+
production Play proof is a separate vector (Cratis/Screenplay#173). UI-profile
|
|
176
|
+
rejection and render-then-recover shipped in Cratis/cli#301.
|
|
175
177
|
|
|
176
178
|
## Replay the workflow in CI without a Screenplay checkout
|
|
177
179
|
|
|
@@ -77,7 +77,9 @@ second command. Screens remain outside portable ESM execution. A downstream
|
|
|
77
77
|
renderer must explicitly admit guarded selection or report unsupported behavior,
|
|
78
78
|
not approximate it as a plain action. Checked at historical CLI 3.41.0: its bundled
|
|
79
79
|
screen renderer reported unsupported guarded actions with STAGE-SCENE-ACTION-001; source compilation alone
|
|
80
|
-
cannot establish usable controls.
|
|
80
|
+
cannot establish usable controls. Since Stage 4.65 a guard comparing `item.<field>` with a literal is
|
|
81
|
+
rendered and evaluated at runtime (public CLI 3.53.1 with `cratis/stage:4.73.1`, Cratis/Screenplay#671);
|
|
82
|
+
STAGE-SCENE-ACTION-001 remains for a guard Stage cannot evaluate.
|
|
81
83
|
|
|
82
84
|
Authority: Screenplay `v4.125.0:Documentation/screenplay/screens.md`;
|
|
83
85
|
CLI `v3.41.0:Documentation/reference/screenplay.md` for renderer disposition.
|
|
@@ -35,8 +35,9 @@ Runs: the worked example (`references/worked-example.md`) was probed with cratis
|
|
|
35
35
|
corpus was rendered and published successfully, and an MCP-edited copy was rendered, run and checked in
|
|
36
36
|
the browser; the CLI 3.40.7 browser acceptance run passed 51 assertions (Cratis/Screenplay#605). The AI
|
|
37
37
|
repository's `screenplay-mcp-transcript.ts` harness proves the real stdio proposal/apply path against the
|
|
38
|
-
packaged CLI. Guarded Close actions (`STAGE-SCENE-ACTION-001
|
|
39
|
-
|
|
38
|
+
packaged CLI. Guarded Close actions and double-click interactions were refused (`STAGE-SCENE-ACTION-001`,
|
|
39
|
+
`STAGE-SCENE-INTERACTION-001`) on that vector; since Stage 4.65 they render and run, and the codes
|
|
40
|
+
remain for a guard Stage cannot evaluate, which is never emulated (Cratis/Screenplay#671). UI-profile
|
|
40
41
|
rejection (`CLI-RENDER-007` to `-010`) and render-then-recover shipped in 3.41.0 (Cratis/cli#301). The
|
|
41
42
|
renderer facts are owned by `cratis-stage-rendering-and-sandbox`; this skill links them and never
|
|
42
43
|
restates the admission table.
|
|
@@ -54,10 +54,11 @@ All three levels hold for the canonical `ScreenComposition` corpus on the histor
|
|
|
54
54
|
comments, deep links, stale-response handling and theme included. On CLI
|
|
55
55
|
3.43.0 an agent-edited column was rendered, run and seen in the browser.
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
Guarded Close and double-click: Stage refused both with `STAGE-SCENE-ACTION-001` and
|
|
58
|
+
`STAGE-SCENE-INTERACTION-001` from Stage 4.51 through 4.57. Since Stage 4.65 they render and
|
|
59
|
+
run (public CLI 3.53.1 with `cratis/stage:4.73.1`, Cratis/Screenplay#671); the codes remain the
|
|
60
|
+
refusal for a guard Stage cannot evaluate, which is never emulated. Studio production Play
|
|
61
|
+
proof is a separate vector (Cratis/Screenplay#173). Refusing an unresolvable UI
|
|
61
62
|
profile and recovering a rendered application shipped in Cratis/cli#301. (History: on Stage 4.24.2 or
|
|
62
63
|
cratis 3.28.x renderers authored screens were reported as omitted/default composition; Stage 4.73.1
|
|
63
64
|
(CLI 3.53.1) renders navigation contributions and toolbars, and `CLI-RENDER-015`, which refused them, is retired.)
|
|
@@ -124,7 +125,8 @@ from parsing; do not publish an empty plain action as equivalent behavior.
|
|
|
124
125
|
The complete checked model and conditions are in
|
|
125
126
|
`cratis-screenplay-read-surface` `references/guarded-actions.md`, grounded in
|
|
126
127
|
Screenplay v4.125.0 `screens.md`. Historical CLI 3.41.0/Stage 4.51.3 checks reported
|
|
127
|
-
unsupported screen actions with STAGE-SCENE-ACTION-001
|
|
128
|
+
unsupported screen actions with STAGE-SCENE-ACTION-001; since Stage 4.65 a guard of the form
|
|
129
|
+
`item.<field>` compared with a literal is evaluated at runtime (Cratis/Screenplay#671).
|
|
128
130
|
|
|
129
131
|
## Exposed template configuration
|
|
130
132
|
|
|
@@ -24,10 +24,10 @@ browser assertions passed, none blocked or pending (Cratis/Screenplay#605).
|
|
|
24
24
|
Native command forms, required fields, scoped comments, deep links,
|
|
25
25
|
stale-response handling and theme all pass there. On public CLI 3.43.0 (default
|
|
26
26
|
`cratis/stage:4.52.0`) an agent-edited table column was rendered, run and seen in
|
|
27
|
-
the browser.
|
|
28
|
-
|
|
29
|
-
(`STAGE-SCENE-
|
|
30
|
-
|
|
27
|
+
the browser. Guarded Close and guarded double-click run since
|
|
28
|
+
Stage 4.65: on CLI 3.53.1 with `cratis/stage:4.73.1` Close shows only while the item is
|
|
29
|
+
open and a double click navigates or notifies by row (Cratis/Screenplay#671). `STAGE-SCENE-ACTION-001` and `STAGE-SCENE-INTERACTION-001` still
|
|
30
|
+
refuse a guard Stage cannot evaluate: no artifacts. Require a generated plan or artifact entry for every
|
|
31
31
|
authored UI construct, and report every unsupported UI directive as a diagnostic.
|
|
32
32
|
Silent fallback to a default screen is a failure, not a partial render.
|
|
33
33
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
## Contents
|
|
4
4
|
|
|
5
5
|
- Current behavior at Stage 4.73.1 (source reading; Docker not run)
|
|
6
|
+
- Specification engine selection at Stage 4.78.0 and CLI 3.53.1
|
|
6
7
|
- The sandbox: a partial, disposable runtime
|
|
7
8
|
- The specification runner
|
|
8
9
|
- Choosing between them
|
|
@@ -25,6 +26,31 @@ it prints a deprecation notice, refuses routes with exit 1 and writes no results
|
|
|
25
26
|
`0`/`1`/`2` are as in `SKILL.md`. The sections below describe the 4.24-era behavior and remain the
|
|
26
27
|
engine comparison.
|
|
27
28
|
|
|
29
|
+
## Specification engine selection at Stage 4.78.0 and CLI 3.53.1
|
|
30
|
+
|
|
31
|
+
Re-verified from Stage `v4.78.0`, `Source/SpecRunner/SpecRunnerArguments.cs:18,76`,
|
|
32
|
+
`Program.cs:46–78,86–113`, and
|
|
33
|
+
`Source/Specifications/SemanticSpecificationExecutor.cs:115–136,183–197`:
|
|
34
|
+
|
|
35
|
+
- The runner still defaults to deprecated `structural`, which executes no slice.
|
|
36
|
+
Pass `--engine semantic` to the **Stage specification runner**, not to `cratis run`.
|
|
37
|
+
- Semantic execution creates a fresh Chronicle in-memory event log per
|
|
38
|
+
specification and dispatches admitted commands through Arc's in-memory
|
|
39
|
+
pipeline. The `stage-spec-run/1` report carries `Passed`, `Failed`, `Unsupported`
|
|
40
|
+
or `Cancelled`; exit `0` only proves the report was written, not that specs passed
|
|
41
|
+
(`Documentation/docker/spec-runner.md:150–174`).
|
|
42
|
+
- CLI `v3.53.1` does **not** expose `--engine` or a Stage specification-runner
|
|
43
|
+
command. Its `run` command starts the **sandbox**, with path, tag, port,
|
|
44
|
+
workbench-port and verbose settings only (`Source/Cli/Commands/Run/RunSettings.cs:9–52`,
|
|
45
|
+
`RunCommand.cs:10–23,100–105`). Changing `--tag` does not select an engine.
|
|
46
|
+
For semantic specifications use the runner directly, for example
|
|
47
|
+
`cratis/stage-specrunner:4.78.0 --engine semantic`; for the sandbox, engine
|
|
48
|
+
selection is the separate `Stage__Runtime__Engine=semantic` environment setting.
|
|
49
|
+
|
|
50
|
+
This is source verification, not an executed Docker run. It updates engine
|
|
51
|
+
selection only; the historical capability lists below are not a complete
|
|
52
|
+
admission inventory for Stage `v4.78.0`.
|
|
53
|
+
|
|
28
54
|
## The sandbox: a partial, disposable runtime
|
|
29
55
|
|
|
30
56
|
`cratis/stage` is the Stage host plus an in-memory Chronicle kernel. Each session starts
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
{ "kind": "skill-contains", "value": "`Customizations/`" },
|
|
24
24
|
{ "kind": "skill-contains", "value": "`Stage__Runtime__Engine=semantic`" },
|
|
25
25
|
{ "kind": "skill-contains", "value": "`--engine semantic`" },
|
|
26
|
+
{ "kind": "skill-contains", "value": "Stage `v4.78.0`", "file": "references/sandbox-and-specrunner.md" },
|
|
27
|
+
{ "kind": "skill-contains", "value": "does **not** expose `--engine`", "file": "references/sandbox-and-specrunner.md" },
|
|
28
|
+
{ "kind": "skill-contains", "value": "fresh Chronicle in-memory event log per", "file": "references/sandbox-and-specrunner.md" },
|
|
26
29
|
{ "kind": "skill-contains", "value": "through v10" },
|
|
27
30
|
{ "kind": "skill-contains", "value": "CLI-RENDER-004" },
|
|
28
31
|
{ "kind": "skill-contains", "value": "STAGE-ESM-024" },
|
|
@@ -631,6 +631,8 @@
|
|
|
631
631
|
"id": "cratis/components",
|
|
632
632
|
"availableTargets": [
|
|
633
633
|
"cratis-components-accessibility",
|
|
634
|
+
"cratis-components-common-controls",
|
|
635
|
+
"cratis-components-data-surfaces",
|
|
634
636
|
"cratis-components-schema-editor",
|
|
635
637
|
"cratis-components-stepper-command-dialog",
|
|
636
638
|
"cratis-components-styling",
|
|
@@ -760,7 +762,8 @@
|
|
|
760
762
|
{
|
|
761
763
|
"id": "cratis/fundamentals",
|
|
762
764
|
"availableTargets": [
|
|
763
|
-
"cratis-fundamentals-concept"
|
|
765
|
+
"cratis-fundamentals-concept",
|
|
766
|
+
"cratis-fundamentals-telemetry"
|
|
764
767
|
],
|
|
765
768
|
"languages": [
|
|
766
769
|
"csharp"
|
|
@@ -993,7 +996,8 @@
|
|
|
993
996
|
"cratis/engineering/core"
|
|
994
997
|
],
|
|
995
998
|
"availableTargets": [
|
|
996
|
-
"cratis-engineering-csharp-conventions"
|
|
999
|
+
"cratis-engineering-csharp-conventions",
|
|
1000
|
+
"cratis-orleans-jobs"
|
|
997
1001
|
],
|
|
998
1002
|
"languages": [
|
|
999
1003
|
"csharp"
|