@cratis/pi 7.20.0 → 7.21.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.
@@ -64,15 +64,23 @@ public record AuthorName(string Value) : ConceptAs<string>(Value)
64
64
  ## Chronicle stream identity rules
65
65
 
66
66
  ```csharp
67
- public record AuthorId(Guid Value) : EventSourceId<Guid>(Value)
67
+ public record AuthorId(Guid Value) : EventSourceId<Guid>(Value), IGeneratable<AuthorId>
68
68
  {
69
- public static AuthorId New() => new(Guid.NewGuid());
69
+ public static AuthorId New() => new(GenerateValue.Uuid());
70
70
  public static implicit operator AuthorId(Guid value) => new(value);
71
71
  }
72
72
  ```
73
73
 
74
74
  - `New()`, primitive → derived ID conversion, and sentinels are optional domain
75
75
  conveniences; `EventSourceId<T>` does not create an arbitrary derived type.
76
+ - When the system creates the value, implement `IGeneratable<TSelf>` and use
77
+ `GenerateValue.Uuid()` (or `GenerateValue.UuidV7()` when index locality
78
+ matters), both from `Cratis.Concepts` in Cratis.Fundamentals 7.23.0 or later.
79
+ Never `System.Random`. A client-supplied identifier is an input, not something
80
+ to generate; see the **cratis-fundamentals-concept** skill.
81
+ - Arc and Chronicle can resolve an older Cratis.Fundamentals. Check the resolved
82
+ version first; if it is below 7.23.0, add a direct `Cratis.Fundamentals`
83
+ 7.23.0 or later reference, or keep the existing `static New()` (without the interface) until you can.
76
84
  - The exact `EventSourceId<T>` base supports conversions among its underlying
77
85
  `T`, string, typed ID, and untyped `EventSourceId`, but those operators do not
78
86
  construct your derived domain record from every source form.
@@ -54,10 +54,10 @@ Every domain identifier or named value is a typed record — raw `string`/`Guid`
54
54
  - **[contract] Chronicle stream identities** derive from `EventSourceId<T>` with an `IComparable` underlying primitive. Add `NotSet`, `New()`, or a `T` → derived-id operator only when the domain API justifies it; typed empty/zero values are real specified stream IDs. The base conversion surface does not construct every derived domain record automatically.
55
55
 
56
56
  ```csharp
57
- public record AuthorId(Guid Value) : EventSourceId<Guid>(Value)
57
+ public record AuthorId(Guid Value) : EventSourceId<Guid>(Value), IGeneratable<AuthorId>
58
58
  {
59
59
  public static readonly AuthorId NotSet = new(Guid.Empty);
60
- public static AuthorId New() => new(Guid.NewGuid());
60
+ public static AuthorId New() => new(GenerateValue.Uuid());
61
61
  public static implicit operator AuthorId(Guid value) => new(value);
62
62
  }
63
63
 
@@ -67,6 +67,8 @@ public record AuthorName(string Value) : ConceptAs<string>(Value)
67
67
  }
68
68
  ```
69
69
 
70
+ `IGeneratable<T>` and `GenerateValue` need Cratis.Fundamentals 7.23.0 or later. Arc and Chronicle can resolve an older version; add a direct reference or keep a plain `static New()` until then (see [concepts.md](./concepts.md)).
71
+
70
72
  **[convention] Placement:** slice-specific concept → in the slice file; feature-shared → feature folder; module-shared → module folder; app-wide → `Common/`. Scan `Common/` and the feature folder before creating a new concept. See [concepts.md](./concepts.md) for full patterns, or invoke the **cratis-fundamentals-concept** skill.
71
73
 
72
74
  ## Commands
@@ -15,7 +15,7 @@ This skill is verified against these exact public releases:
15
15
 
16
16
  | Package | Version | Purpose |
17
17
  | --- | --- | --- |
18
- | `Cratis.Fundamentals` | `7.19.2` | `Cratis.Concepts.ConceptAs<T>` |
18
+ | `Cratis.Fundamentals` | `7.23.0` | `Cratis.Concepts.ConceptAs<T>`, `IGeneratable<TSelf>` and `GenerateValue` |
19
19
  | `Cratis.Chronicle` | `18.3.0` | `Cratis.Chronicle.Events.EventSourceId` and `EventSourceId<T>` |
20
20
 
21
21
  Reverify product sources before claiming support for another version.
@@ -76,6 +76,55 @@ A `NotSet` or `Empty` value is optional domain policy. Add one only when the
76
76
  chosen primitive value is impossible or explicitly reserved in that domain.
77
77
  Do not assume `string.Empty`, `0`, or `Guid.Empty` is universally invalid.
78
78
 
79
+ ## Let a concept generate its own value
80
+
81
+ Check the Cratis.Fundamentals version the project resolves first. Arc and
82
+ Chronicle can bring an older one; when it is below 7.23.0, add a direct
83
+ `Cratis.Fundamentals` 7.23.0 or later package reference, or keep the existing
84
+ `static New()` (without the interface) until you can.
85
+
86
+ When the system, not the caller, creates a value, the concept declares how by
87
+ implementing `IGeneratable<TSelf>` (`Cratis.Concepts`, Cratis.Fundamentals
88
+ 7.23.0 or later). The interface requires `static abstract TSelf New()`, so
89
+ generic code constrained on `where T : IGeneratable<T>` calls `T.New()` without
90
+ reflection. Generate a UUID with `GenerateValue`, never with `Guid.NewGuid()` in
91
+ scattered call sites and never with `System.Random`:
92
+
93
+ ```csharp
94
+ using Cratis.Concepts;
95
+
96
+ public record <ConceptName>(Guid Value) : ConceptAs<Guid>(Value), IGeneratable<<ConceptName>>
97
+ {
98
+ public static <ConceptName> New() => new(GenerateValue.Uuid());
99
+ }
100
+ ```
101
+
102
+ Both helpers return a `Guid` with the RFC 9562 variant and fill their random
103
+ bits from `RandomNumberGenerator`:
104
+
105
+ - `GenerateValue.Uuid()` — version 4, 122 random bits. The default.
106
+ - `GenerateValue.UuidV7()` — version 7: Unix-epoch milliseconds first, then 74
107
+ random bits. Choose it only when database index locality or rough creation
108
+ order matters, after checking how the database stores and compares UUIDs. It reveals the creation time, it is **not** monotonic within a
109
+ millisecond, and clock adjustments can reorder values.
110
+
111
+ Neither helper guarantees uniqueness; a collision is extremely unlikely, not
112
+ impossible. `IGeneratable<TSelf>` is not limited to Guid-backed concepts, but
113
+ `GenerateValue` only produces UUIDs.
114
+
115
+ Do not generate when:
116
+
117
+ - The client supplies the identifier. It is an input; keep it.
118
+ - The value must be secret or authenticate someone. A UUID is an identifier,
119
+ not a token; use a dedicated secure-token API.
120
+ - The same input must yield the same value. Random generation is not
121
+ idempotency; derive the value deterministically, or persist and reuse the
122
+ first one.
123
+
124
+ In specifications, construct the concept from a known value
125
+ (`new <ConceptName>(Guid.Parse("…"))`) and assert against it. Do not seed or
126
+ replace the generator. Guide: `/fundamentals/csharp/generating-values/`.
127
+
79
128
  ## Create a Guid-backed Chronicle stream identity
80
129
 
81
130
  Use this shape only for an identity actually supplied to Chronicle append/read
@@ -86,6 +135,7 @@ operations as the event-source ID.
86
135
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
87
136
 
88
137
  using Cratis.Chronicle.Events;
138
+ using Cratis.Concepts;
89
139
 
90
140
  namespace <NamespaceRoot>.<Feature>;
91
141
 
@@ -93,13 +143,13 @@ namespace <NamespaceRoot>.<Feature>;
93
143
  /// Represents the event-source identity of a <description>.
94
144
  /// </summary>
95
145
  /// <param name="Value">The underlying Guid value.</param>
96
- public record <ConceptName>(Guid Value) : EventSourceId<Guid>(Value)
146
+ public record <ConceptName>(Guid Value) : EventSourceId<Guid>(Value), IGeneratable<<ConceptName>>
97
147
  {
98
148
  /// <summary>
99
149
  /// Creates a new <ConceptName>.
100
150
  /// </summary>
101
151
  /// <returns>A new <ConceptName>.</returns>
102
- public static <ConceptName> New() => new(Guid.NewGuid());
152
+ public static <ConceptName> New() => new(GenerateValue.Uuid());
103
153
 
104
154
  /// <summary>
105
155
  /// Converts a Guid to a <ConceptName>.
@@ -108,8 +158,8 @@ public record <ConceptName>(Guid Value) : EventSourceId<Guid>(Value)
108
158
  }
109
159
  ```
110
160
 
111
- `New()` and the primitive-to-derived conversion are conveniences on this domain
112
- type. `EventSourceId<T>` does not construct an arbitrary derived identity for
161
+ `New()` (through `IGeneratable<TSelf>`) and the primitive-to-derived conversion
162
+ are conveniences on this domain type. `EventSourceId<T>` does not construct an arbitrary derived identity for
113
163
  you.
114
164
 
115
165
  ## Create a non-Guid Chronicle stream identity
@@ -185,6 +235,9 @@ repository structure.
185
235
  - `ConceptAs<T>` and `EventSourceId<T>` use an `IComparable` underlying type.
186
236
  - A concept contains exactly one wrapped value and no extra properties.
187
237
  - Enums remain enums.
238
+ - A concept that creates its own value implements `IGeneratable<TSelf>`; a
239
+ Guid-backed one uses `GenerateValue`, not `Guid.NewGuid()` or `Random`.
240
+ Client-supplied, secret or idempotent values are not generated.
188
241
  - Null absence uses a nullable concept reference rather than a null wrapped
189
242
  value.
190
243
  - Primitive-to-derived conversions and sentinels exist only when justified by
@@ -3,6 +3,9 @@
3
3
  "input": "Create a strongly typed value for a domain concept.",
4
4
  "fixtures": [],
5
5
  "assertions": [
6
- { "kind": "skill-contains", "value": "ConceptAs<T>" }
6
+ { "kind": "skill-contains", "value": "ConceptAs<T>" },
7
+ { "kind": "skill-contains", "value": "IGeneratable<TSelf>" },
8
+ { "kind": "skill-contains", "value": "GenerateValue.Uuid()" },
9
+ { "kind": "skill-contains", "value": "not** monotonic within a" }
7
10
  ]
8
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/pi",
3
- "version": "7.20.0",
3
+ "version": "7.21.0",
4
4
  "description": "Configuration-aware Cratis AI integration for Pi",
5
5
  "type": "module",
6
6
  "files": [