@cratis/chronicle 6.35.0 → 6.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.
Files changed (135) hide show
  1. package/EventStore.ts +3 -0
  2. package/IEventStore.ts +4 -0
  3. package/connection/ChronicleConnection.ts +6 -0
  4. package/connection/ChronicleServices.ts +2 -0
  5. package/dist/EventStore.d.ts +2 -0
  6. package/dist/EventStore.d.ts.map +1 -1
  7. package/dist/EventStore.js +3 -0
  8. package/dist/EventStore.js.map +1 -1
  9. package/dist/IEventStore.d.ts +3 -0
  10. package/dist/IEventStore.d.ts.map +1 -1
  11. package/dist/connection/ChronicleConnection.d.ts +1 -0
  12. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  13. package/dist/connection/ChronicleConnection.js +5 -1
  14. package/dist/connection/ChronicleConnection.js.map +1 -1
  15. package/dist/connection/ChronicleServices.d.ts +2 -1
  16. package/dist/connection/ChronicleServices.d.ts.map +1 -1
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/patterns/BehaviorPattern.d.ts +25 -0
  22. package/dist/patterns/BehaviorPattern.d.ts.map +1 -0
  23. package/dist/patterns/BehaviorPattern.js +4 -0
  24. package/dist/patterns/BehaviorPattern.js.map +1 -0
  25. package/dist/patterns/DayOfWeek.d.ts +11 -0
  26. package/dist/patterns/DayOfWeek.d.ts.map +1 -0
  27. package/dist/patterns/DayOfWeek.js +14 -0
  28. package/dist/patterns/DayOfWeek.js.map +1 -0
  29. package/dist/patterns/FacetName.d.ts +15 -0
  30. package/dist/patterns/FacetName.d.ts.map +1 -0
  31. package/dist/patterns/FacetName.js +18 -0
  32. package/dist/patterns/FacetName.js.map +1 -0
  33. package/dist/patterns/FacetSet.d.ts +7 -0
  34. package/dist/patterns/FacetSet.d.ts.map +1 -0
  35. package/dist/patterns/FacetSet.js +4 -0
  36. package/dist/patterns/FacetSet.js.map +1 -0
  37. package/dist/patterns/IPatterns.d.ts +45 -0
  38. package/dist/patterns/IPatterns.d.ts.map +1 -0
  39. package/dist/patterns/IPatterns.js +4 -0
  40. package/dist/patterns/IPatterns.js.map +1 -0
  41. package/dist/patterns/PatternMoment.d.ts +11 -0
  42. package/dist/patterns/PatternMoment.d.ts.map +1 -0
  43. package/dist/patterns/PatternMoment.js +4 -0
  44. package/dist/patterns/PatternMoment.js.map +1 -0
  45. package/dist/patterns/PatternQueryOptions.d.ts +8 -0
  46. package/dist/patterns/PatternQueryOptions.d.ts.map +1 -0
  47. package/dist/patterns/PatternQueryOptions.js +4 -0
  48. package/dist/patterns/PatternQueryOptions.js.map +1 -0
  49. package/dist/patterns/Patterns.d.ts +32 -0
  50. package/dist/patterns/Patterns.d.ts.map +1 -0
  51. package/dist/patterns/Patterns.js +76 -0
  52. package/dist/patterns/Patterns.js.map +1 -0
  53. package/dist/patterns/PatternsAtOptions.d.ts +8 -0
  54. package/dist/patterns/PatternsAtOptions.d.ts.map +1 -0
  55. package/dist/patterns/PatternsAtOptions.js +4 -0
  56. package/dist/patterns/PatternsAtOptions.js.map +1 -0
  57. package/dist/patterns/TimeBucket.d.ts +16 -0
  58. package/dist/patterns/TimeBucket.d.ts.map +1 -0
  59. package/dist/patterns/TimeBucket.js +19 -0
  60. package/dist/patterns/TimeBucket.js.map +1 -0
  61. package/dist/patterns/index.d.ts +13 -0
  62. package/dist/patterns/index.d.ts.map +1 -0
  63. package/dist/patterns/index.js +9 -0
  64. package/dist/patterns/index.js.map +1 -0
  65. package/dist/patterns/toBehaviorPattern.d.ts +9 -0
  66. package/dist/patterns/toBehaviorPattern.d.ts.map +1 -0
  67. package/dist/patterns/toBehaviorPattern.js +22 -0
  68. package/dist/patterns/toBehaviorPattern.js.map +1 -0
  69. package/dist/patterns/toDayOfWeek.d.ts +10 -0
  70. package/dist/patterns/toDayOfWeek.d.ts.map +1 -0
  71. package/dist/patterns/toDayOfWeek.js +16 -0
  72. package/dist/patterns/toDayOfWeek.js.map +1 -0
  73. package/dist/patterns/toOffsetDate.d.ts +9 -0
  74. package/dist/patterns/toOffsetDate.d.ts.map +1 -0
  75. package/dist/patterns/toOffsetDate.js +20 -0
  76. package/dist/patterns/toOffsetDate.js.map +1 -0
  77. package/dist/patterns/toTimeBucket.d.ts +10 -0
  78. package/dist/patterns/toTimeBucket.d.ts.map +1 -0
  79. package/dist/patterns/toTimeBucket.js +25 -0
  80. package/dist/patterns/toTimeBucket.js.map +1 -0
  81. package/dist/testing/ReactorScenario.js +1 -1
  82. package/dist/testing/ReactorScenario.js.map +1 -1
  83. package/dist/testing/projections/ProjectionArithmetic.d.ts +11 -0
  84. package/dist/testing/projections/ProjectionArithmetic.d.ts.map +1 -0
  85. package/dist/testing/projections/ProjectionArithmetic.js +70 -0
  86. package/dist/testing/projections/ProjectionArithmetic.js.map +1 -0
  87. package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -1
  88. package/dist/testing/projections/ProjectionCapabilities.js +26 -10
  89. package/dist/testing/projections/ProjectionCapabilities.js.map +1 -1
  90. package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
  91. package/dist/testing/projections/ProjectionReadModelProcessor.js +10 -3
  92. package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
  93. package/dist/tsconfig.tsbuildinfo +1 -1
  94. package/index.ts +3 -0
  95. package/package.json +7 -2
  96. package/patterns/BehaviorPattern.ts +28 -0
  97. package/patterns/DayOfWeek.ts +13 -0
  98. package/patterns/FacetName.ts +17 -0
  99. package/patterns/FacetSet.ts +9 -0
  100. package/patterns/IPatterns.ts +52 -0
  101. package/patterns/PatternMoment.ts +13 -0
  102. package/patterns/PatternQueryOptions.ts +10 -0
  103. package/patterns/Patterns.ts +88 -0
  104. package/patterns/PatternsAtOptions.ts +11 -0
  105. package/patterns/TimeBucket.ts +18 -0
  106. package/patterns/for_PatternMoment/when_deriving_facets/with_invalid_moments.spec.ts +31 -0
  107. package/patterns/for_Patterns/given/a_patterns_service.fixture.ts +21 -0
  108. package/patterns/for_Patterns/when_browsing/with_scopes.spec.ts +21 -0
  109. package/patterns/for_Patterns/when_browsing/within_a_scope.spec.ts +25 -0
  110. package/patterns/for_Patterns/when_querying/with_query_outcomes.spec.ts +47 -0
  111. package/patterns/for_Patterns/when_querying_a_context/with_established_patterns.spec.ts +34 -0
  112. package/patterns/for_Patterns/when_querying_a_context/with_facets.spec.ts +32 -0
  113. package/patterns/for_Patterns/when_querying_at_a_moment/with_additional_facets.spec.ts +34 -0
  114. package/patterns/for_Patterns/when_querying_at_a_moment/with_an_invalid_moment.spec.ts +20 -0
  115. package/patterns/for_Patterns/when_querying_at_a_moment/without_a_moment.spec.ts +37 -0
  116. package/patterns/for_Patterns/when_querying_through_the_connection/after_resetting_the_channel.spec.ts +58 -0
  117. package/patterns/for_toDayOfWeek/when_reading_the_day/at_an_offset.spec.ts +31 -0
  118. package/patterns/for_toTimeBucket/when_bucketing/at_boundaries.spec.ts +39 -0
  119. package/patterns/index.ts +15 -0
  120. package/patterns/toBehaviorPattern.ts +25 -0
  121. package/patterns/toDayOfWeek.ts +19 -0
  122. package/patterns/toOffsetDate.ts +22 -0
  123. package/patterns/toTimeBucket.ts +22 -0
  124. package/testing/ReactorScenario.spec.ts +13 -0
  125. package/testing/ReactorScenario.ts +1 -1
  126. package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_a_fluent_projection.spec.ts +85 -0
  127. package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_legacy_decorators.legacy.spec.ts +91 -0
  128. package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts +92 -0
  129. package/testing/projections/ProjectionArithmetic.ts +71 -0
  130. package/testing/projections/ProjectionCapabilities.ts +27 -9
  131. package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +9 -6
  132. package/testing/projections/ProjectionReadModelProcessor.ts +9 -3
  133. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +3 -3
  134. package/testing/projections/for_ProjectionCapabilities/when_validating_arithmetic.spec.ts +127 -0
  135. package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +4 -5
package/index.ts CHANGED
@@ -9,6 +9,7 @@ import * as seeding from './seeding/index.js';
9
9
  import * as readModels from './readModels/index.js';
10
10
  import * as projections from './projections/index.js';
11
11
  import * as jobs from './jobs/index.js';
12
+ import * as patterns from './patterns/index.js';
12
13
  import * as webhooks from './webhooks/index.js';
13
14
  import * as externalServices from './externalServices/index.js';
14
15
  import * as identities from './identities/index.js';
@@ -53,6 +54,7 @@ export * from './seeding/index.js';
53
54
  export * from './readModels/index.js';
54
55
  export * from './projections/index.js';
55
56
  export * from './jobs/index.js';
57
+ export * from './patterns/index.js';
56
58
  export * from './webhooks/index.js';
57
59
  export * from './externalServices/index.js';
58
60
  export * from './identities/index.js';
@@ -78,6 +80,7 @@ export {
78
80
  readModels,
79
81
  projections,
80
82
  jobs,
83
+ patterns,
81
84
  webhooks,
82
85
  externalServices,
83
86
  identities,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.35.0",
3
+ "version": "6.37.0",
4
4
  "description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -96,6 +96,11 @@
96
96
  "import": "./dist/projections/index.js",
97
97
  "default": "./dist/projections/index.js"
98
98
  },
99
+ "./patterns": {
100
+ "types": "./dist/patterns/index.d.ts",
101
+ "import": "./dist/patterns/index.js",
102
+ "default": "./dist/patterns/index.js"
103
+ },
99
104
  "./jobs": {
100
105
  "types": "./dist/jobs/index.d.ts",
101
106
  "import": "./dist/jobs/index.js",
@@ -168,7 +173,7 @@
168
173
  "clean": "rm -rf dist",
169
174
  "test": "vitest run",
170
175
  "test:integration": "vitest run --config vitest.integration.config.ts",
171
- "test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts",
176
+ "test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts",
172
177
  "test:native": "node ./scripts/check-native-esm.mjs"
173
178
  },
174
179
  "dependencies": {
@@ -0,0 +1,28 @@
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
+ import type { FacetSet } from './FacetSet.js';
5
+
6
+ /** Recurring behavior established by Chronicle, not a prediction invented by the client. */
7
+ export interface BehaviorPattern {
8
+ /** Identity of the persisted pattern. */
9
+ readonly id: string;
10
+ /** Scope the behavior belongs to, typically a user. */
11
+ readonly groupingKey: string;
12
+ /** The pattern's context and, when present, its CommandType action. */
13
+ readonly facets: FacetSet;
14
+ /** How often the pattern holds when its context is present, from 0 to 1. */
15
+ readonly confidence: number;
16
+ /** Share of all observed events containing the pattern, from 0 to 1. */
17
+ readonly support: number;
18
+ /** Approximate occurrence count, preserving the wire's 64-bit precision. */
19
+ readonly occurrences: bigint;
20
+ /** Recency-weighted strength. */
21
+ readonly weight: number;
22
+ /** Number of facets the pattern constrains. */
23
+ readonly specificity: number;
24
+ /** First observed instant, absent if the server omitted it. */
25
+ readonly firstSeen: Date | undefined;
26
+ /** Last observed instant, absent if the server omitted it. */
27
+ readonly lastSeen: Date | undefined;
28
+ }
@@ -0,0 +1,13 @@
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
+ /** Day names used by Chronicle's Day facet, independent of the caller's locale. */
5
+ export enum DayOfWeek {
6
+ Sunday = 'Sunday',
7
+ Monday = 'Monday',
8
+ Tuesday = 'Tuesday',
9
+ Wednesday = 'Wednesday',
10
+ Thursday = 'Thursday',
11
+ Friday = 'Friday',
12
+ Saturday = 'Saturday'
13
+ }
@@ -0,0 +1,17 @@
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
+ /** Well-known contextual dimensions mined from an event's context. */
5
+ export enum FacetName {
6
+ CommandType = 'CommandType',
7
+ InitiatorType = 'InitiatorType',
8
+ InitiatorId = 'InitiatorId',
9
+ OnBehalfOf = 'OnBehalfOf',
10
+ CausedByCommand = 'CausedByCommand',
11
+ CorrelationRootId = 'CorrelationRootId',
12
+ AggregateType = 'AggregateType',
13
+ Year = 'Year',
14
+ Month = 'Month',
15
+ Day = 'Day',
16
+ TimeBucket = 'TimeBucket'
17
+ }
@@ -0,0 +1,9 @@
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
+ /**
5
+ * A partial context of facet names and values. Use {@link FacetName} for well-known names;
6
+ * other names are accepted, but the kernel discards facets outside its configured mining vocabulary rather than
7
+ * narrowing to nothing. An empty object constrains nothing. Empty values are omitted from queries.
8
+ */
9
+ export type FacetSet = Readonly<Record<string, string>>;
@@ -0,0 +1,52 @@
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
+ import type { BehaviorPattern } from './BehaviorPattern.js';
5
+ import type { FacetSet } from './FacetSet.js';
6
+ import type { PatternMoment } from './PatternMoment.js';
7
+ import type { PatternQueryOptions } from './PatternQueryOptions.js';
8
+ import type { PatternsAtOptions } from './PatternsAtOptions.js';
9
+
10
+ /** Queries behavior established in this event store and namespace. Failed queries reject, not return an empty answer. */
11
+ export interface IPatterns {
12
+ /**
13
+ * Finds patterns describing a context, ranked by specificity then confidence.
14
+ * @param scope - Required grouping key, typically a user identifier.
15
+ * @param context - Facets to match, including the action if it is already known.
16
+ * @param options - Optional query limits; defaults come from the server.
17
+ * @returns Matching patterns, or an empty array when nothing is established for this context.
18
+ */
19
+ getPatterns(scope: string, context: FacetSet, options?: PatternQueryOptions): Promise<BehaviorPattern[]>;
20
+
21
+ /**
22
+ * Finds what is usually done in a context, ranked by confidence, at most one answer per action.
23
+ * @param scope - Required grouping key, typically a user identifier.
24
+ * @param context - The known situation; answers name the action through their CommandType facet.
25
+ * @param options - Optional query limits; defaults come from the server.
26
+ * @returns Usual actions, or an empty array when nothing is established for this context.
27
+ */
28
+ getUsualActions(scope: string, context: FacetSet, options?: PatternQueryOptions): Promise<BehaviorPattern[]>;
29
+
30
+ /**
31
+ * Finds what a scope usually does at a moment through getUsualActions.
32
+ * @param scope - Required grouping key, typically a user identifier.
33
+ * @param moment - An instant and its own offset; defaults to now at the system's local offset.
34
+ * @param options - Additional context and query limits. Derived Day and TimeBucket always take precedence.
35
+ * @returns Usual actions, or an empty array when nothing is established for this moment.
36
+ * @throws {RangeError} If the moment is invalid.
37
+ */
38
+ getPatternsAt(scope: string, moment?: PatternMoment, options?: PatternsAtOptions): Promise<BehaviorPattern[]>;
39
+
40
+ /**
41
+ * Gets every pattern established for a scope, without query limits.
42
+ * @param scope - Required grouping key.
43
+ * @returns All patterns held for this scope.
44
+ */
45
+ getPatternsForScope(scope: string): Promise<BehaviorPattern[]>;
46
+
47
+ /**
48
+ * Lists scopes with established patterns in this event store and namespace.
49
+ * @returns The grouping keys of scopes holding patterns.
50
+ */
51
+ getScopes(): Promise<string[]>;
52
+ }
@@ -0,0 +1,13 @@
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
+ /**
5
+ * An instant with its own UTC offset, which a JavaScript Date alone cannot retain.
6
+ * The offset is fixed for this instant, not an IANA time zone or a daylight-saving rule.
7
+ */
8
+ export interface PatternMoment {
9
+ /** The instant to ask about. Must be a valid Date. */
10
+ readonly instant: Date;
11
+ /** Minutes east of UTC, from -840 to 840 inclusive, in whole minutes. For example, +02:00 is 120. */
12
+ readonly offsetMinutes: number;
13
+ }
@@ -0,0 +1,10 @@
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
+ /** Optional limits on a pattern query; omitted values defer to the server's configuration. */
5
+ export interface PatternQueryOptions {
6
+ /** Lowest confidence to return, from 0 to 1. Zero selects the server's configured threshold. */
7
+ readonly minimumConfidence?: number;
8
+ /** Maximum number of answers. Zero selects the server's configured default. */
9
+ readonly maximumResults?: number;
10
+ }
@@ -0,0 +1,88 @@
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
+ import type { MatchingPatternsRequest } from '@cratis/chronicle.contracts';
5
+ import type { ChronicleConnection } from '../connection/index.js';
6
+ import { ensureQuerySuccess } from '../connection/callResults.js';
7
+ import type { BehaviorPattern } from './BehaviorPattern.js';
8
+ import { FacetName } from './FacetName.js';
9
+ import type { FacetSet } from './FacetSet.js';
10
+ import type { IPatterns } from './IPatterns.js';
11
+ import type { PatternMoment } from './PatternMoment.js';
12
+ import type { PatternQueryOptions } from './PatternQueryOptions.js';
13
+ import type { PatternsAtOptions } from './PatternsAtOptions.js';
14
+ import { toBehaviorPattern } from './toBehaviorPattern.js';
15
+ import { toDayOfWeek } from './toDayOfWeek.js';
16
+ import { toTimeBucket } from './toTimeBucket.js';
17
+
18
+ /** Implements {@link IPatterns} using the connection's pattern service. */
19
+ export class Patterns implements IPatterns {
20
+ /**
21
+ * Creates the pattern query surface for an event store namespace.
22
+ * @param _eventStore - Event store name.
23
+ * @param _namespace - Event store namespace.
24
+ * @param _connection - Chronicle connection.
25
+ */
26
+ constructor(
27
+ private readonly _eventStore: string,
28
+ private readonly _namespace: string,
29
+ private readonly _connection: ChronicleConnection
30
+ ) {}
31
+
32
+ /** @inheritdoc */
33
+ async getPatterns(scope: string, context: FacetSet, options: PatternQueryOptions = {}): Promise<BehaviorPattern[]> {
34
+ const response = await this._connection.patterns.matchingPatterns(this.createRequest(scope, context, options));
35
+ return ensureQuerySuccess('get patterns', response).map(toBehaviorPattern);
36
+ }
37
+
38
+ /** @inheritdoc */
39
+ async getUsualActions(scope: string, context: FacetSet, options: PatternQueryOptions = {}): Promise<BehaviorPattern[]> {
40
+ const response = await this._connection.patterns.usualActions(this.createRequest(scope, context, options));
41
+ return ensureQuerySuccess('get usual actions', response).map(toBehaviorPattern);
42
+ }
43
+
44
+ /** @inheritdoc */
45
+ async getPatternsAt(scope: string, moment?: PatternMoment, options: PatternsAtOptions = {}): Promise<BehaviorPattern[]> {
46
+ if (!moment) {
47
+ const instant = new Date();
48
+ moment = { instant, offsetMinutes: -instant.getTimezoneOffset() };
49
+ }
50
+ const context: FacetSet = {
51
+ ...options.alsoConstraining,
52
+ [FacetName.Day]: toDayOfWeek(moment),
53
+ [FacetName.TimeBucket]: toTimeBucket(moment)
54
+ };
55
+ return this.getUsualActions(scope, context, options);
56
+ }
57
+
58
+ /** @inheritdoc */
59
+ async getPatternsForScope(scope: string): Promise<BehaviorPattern[]> {
60
+ const response = await this._connection.patterns.patternsForScope({
61
+ EventStore: this._eventStore,
62
+ Namespace: this._namespace,
63
+ GroupingKey: scope
64
+ });
65
+ return ensureQuerySuccess('get patterns for scope', response).map(toBehaviorPattern);
66
+ }
67
+
68
+ /** @inheritdoc */
69
+ async getScopes(): Promise<string[]> {
70
+ const response = await this._connection.patterns.allPatternScopes({
71
+ EventStore: this._eventStore,
72
+ Namespace: this._namespace
73
+ });
74
+ return ensureQuerySuccess('get pattern scopes', response).map(scope => scope.Id);
75
+ }
76
+
77
+ private createRequest(scope: string, context: FacetSet, options: PatternQueryOptions): MatchingPatternsRequest {
78
+ return {
79
+ EventStore: this._eventStore,
80
+ Namespace: this._namespace,
81
+ GroupingKey: scope,
82
+ Context: Object.fromEntries(Object.entries(context).filter(([, value]) => value.length > 0)),
83
+ // Zero asks the server to use its configuration, rather than duplicating thresholds in the client.
84
+ MinimumConfidence: options.minimumConfidence ?? 0,
85
+ MaximumResults: options.maximumResults ?? 0
86
+ };
87
+ }
88
+ }
@@ -0,0 +1,11 @@
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
+ import type { FacetSet } from './FacetSet.js';
5
+ import type { PatternQueryOptions } from './PatternQueryOptions.js';
6
+
7
+ /** Additional context and limits when asking about a moment. */
8
+ export interface PatternsAtOptions extends PatternQueryOptions {
9
+ /** Additional facets. The moment's Day and TimeBucket replace any supplied values for those names. */
10
+ readonly alsoConstraining?: FacetSet;
11
+ }
@@ -0,0 +1,18 @@
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
+ /** Parts of a day, using the same names and boundaries as Chronicle's pattern miner. */
5
+ export enum TimeBucket {
6
+ /** 05:00 inclusive to 08:00 exclusive. */
7
+ EarlyMorning = 'EarlyMorning',
8
+ /** 08:00 inclusive to 11:00 exclusive. */
9
+ Morning = 'Morning',
10
+ /** 11:00 inclusive to 14:00 exclusive. */
11
+ Midday = 'Midday',
12
+ /** 14:00 inclusive to 17:00 exclusive. */
13
+ Afternoon = 'Afternoon',
14
+ /** 17:00 inclusive to 22:00 exclusive. */
15
+ Evening = 'Evening',
16
+ /** 22:00 inclusive to 05:00 exclusive, crossing midnight. */
17
+ Night = 'Night'
18
+ }
@@ -0,0 +1,31 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { type PatternMoment, toDayOfWeek, toTimeBucket } from '../../index.js';
6
+
7
+ chai.should();
8
+
9
+ const invalidMoments: PatternMoment[] = [
10
+ { instant: new Date(NaN), offsetMinutes: 0 },
11
+ { instant: new Date(0), offsetMinutes: NaN },
12
+ { instant: new Date(0), offsetMinutes: Infinity },
13
+ { instant: new Date(0), offsetMinutes: 1.5 },
14
+ { instant: new Date(0), offsetMinutes: 841 },
15
+ { instant: new Date(0), offsetMinutes: -841 },
16
+ { instant: new Date(8_640_000_000_000_000), offsetMinutes: 60 }
17
+ ];
18
+
19
+ for (const helper of [toDayOfWeek, toTimeBucket]) {
20
+ for (const moment of invalidMoments) {
21
+ describe(`when ${helper.name} receives ${moment.instant} at offset ${moment.offsetMinutes}`, () => {
22
+ let failure: unknown;
23
+ beforeEach(() => {
24
+ try { helper(moment); } catch (error) { failure = error; }
25
+ });
26
+ it('should reject rather than silently ask about a different context', () => {
27
+ (failure as Error).should.be.instanceOf(RangeError);
28
+ });
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,21 @@
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
+ import { vi } from 'vitest';
5
+ import { QueryResultIEnumerableBehaviorPatternDetailsResponse, QueryResultIEnumerablePatternScopeResponse } from '@cratis/chronicle.contracts';
6
+ import type { PatternsClient } from '@cratis/chronicle.contracts';
7
+ import type { ChronicleConnection } from '../../../connection/index.js';
8
+ import { Patterns } from '../../Patterns.js';
9
+
10
+ export class a_patterns_service {
11
+ response = QueryResultIEnumerableBehaviorPatternDetailsResponse.create({ IsAuthorized: true });
12
+ scopesResponse = QueryResultIEnumerablePatternScopeResponse.create({ IsAuthorized: true });
13
+ client = {
14
+ matchingPatterns: vi.fn<PatternsClient['matchingPatterns']>().mockImplementation(async () => this.response),
15
+ usualActions: vi.fn<PatternsClient['usualActions']>().mockImplementation(async () => this.response),
16
+ patternsForScope: vi.fn<PatternsClient['patternsForScope']>().mockImplementation(async () => this.response),
17
+ allPatternScopes: vi.fn<PatternsClient['allPatternScopes']>().mockImplementation(async () => this.scopesResponse),
18
+ allPatterns: vi.fn<PatternsClient['allPatterns']>()
19
+ } satisfies PatternsClient;
20
+ patterns = new Patterns('accounts', 'tenant-a', { patterns: this.client } as unknown as ChronicleConnection);
21
+ }
@@ -0,0 +1,21 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { a_patterns_service } from '../given/a_patterns_service.fixture.js';
6
+
7
+ chai.should();
8
+
9
+ describe('when browsing established scopes', () => {
10
+ let context: a_patterns_service;
11
+ let result: string[];
12
+ beforeEach(async () => {
13
+ context = new a_patterns_service();
14
+ context.scopesResponse.Data = [{ Id: 'user-42', Name: 'A name', UserName: 'a-user' }];
15
+ result = await context.patterns.getScopes();
16
+ });
17
+ it('should return grouping keys rather than display names', () => result.should.deep.equal(['user-42']));
18
+ it('should stay within the event store namespace', () => {
19
+ context.client.allPatternScopes.mock.calls[0][0].should.deep.equal({ EventStore: 'accounts', Namespace: 'tenant-a' });
20
+ });
21
+ });
@@ -0,0 +1,25 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { BehaviorPatternDetailsResponse } from '@cratis/chronicle.contracts';
6
+ import type { BehaviorPattern } from '../../index.js';
7
+ import { a_patterns_service } from '../given/a_patterns_service.fixture.js';
8
+
9
+ chai.should();
10
+
11
+ describe('when browsing every pattern for a scope', () => {
12
+ let context: a_patterns_service;
13
+ let result: BehaviorPattern[];
14
+ beforeEach(async () => {
15
+ context = new a_patterns_service();
16
+ context.response.Data = [BehaviorPatternDetailsResponse.create({ Id: 'pattern-1', GroupingKey: 'user-42' })];
17
+ result = await context.patterns.getPatternsForScope('user-42');
18
+ });
19
+ it('should address the scope within the event store namespace without limits', () => {
20
+ context.client.patternsForScope.mock.calls[0][0].should.deep.equal({
21
+ EventStore: 'accounts', Namespace: 'tenant-a', GroupingKey: 'user-42'
22
+ });
23
+ });
24
+ it('should convert the established patterns', () => result[0].id.should.equal('pattern-1'));
25
+ });
@@ -0,0 +1,47 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { ChronicleCallFailed } from '../../../connection/callResults.js';
6
+ import type { IPatterns } from '../../index.js';
7
+ import { a_patterns_service } from '../given/a_patterns_service.fixture.js';
8
+
9
+ const should = chai.should();
10
+ const queries: Record<string, (patterns: IPatterns) => Promise<unknown[]>> = {
11
+ matching: patterns => patterns.getPatterns('user-42', {}),
12
+ actions: patterns => patterns.getUsualActions('user-42', {}),
13
+ moment: patterns => patterns.getPatternsAt('user-42'),
14
+ scope: patterns => patterns.getPatternsForScope('user-42'),
15
+ scopes: patterns => patterns.getScopes()
16
+ };
17
+
18
+ for (const [name, query] of Object.entries(queries)) {
19
+ describe(`when querying ${name} with no established behavior`, () => {
20
+ let result: unknown[];
21
+ beforeEach(async () => result = await query(new a_patterns_service().patterns));
22
+ it('should return an empty answer without inventing behavior', () => result.should.deep.equal([]));
23
+ });
24
+
25
+ for (const failureKind of ['authorization', 'validation', 'exception', 'transport']) {
26
+ describe(`when querying ${name} with a ${failureKind} failure`, () => {
27
+ let failure: unknown;
28
+ const transportFailure = new Error('transport unavailable');
29
+ beforeEach(async () => {
30
+ const context = new a_patterns_service();
31
+ for (const response of [context.response, context.scopesResponse]) {
32
+ if (failureKind === 'authorization') response.IsAuthorized = false;
33
+ if (failureKind === 'validation') response.ValidationResults = [{ Severity: 3, Message: 'invalid context', Members: [] }];
34
+ if (failureKind === 'exception') response.ExceptionMessages = ['query failed'];
35
+ }
36
+ if (failureKind === 'transport') {
37
+ for (const client of Object.values(context.client)) client.mockRejectedValue(transportFailure);
38
+ }
39
+ try { await query(context.patterns); } catch (error) { failure = error; }
40
+ });
41
+ it('should reject instead of disguising failure as no behavior', () => {
42
+ if (failureKind === 'transport') should.equal(failure, transportFailure);
43
+ else (failure as Error).should.be.instanceOf(ChronicleCallFailed);
44
+ });
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,34 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { BehaviorPatternDetailsResponse } from '@cratis/chronicle.contracts';
6
+ import type { BehaviorPattern } from '../../index.js';
7
+ import { a_patterns_service } from '../given/a_patterns_service.fixture.js';
8
+
9
+ const should = chai.should();
10
+
11
+ describe('when querying a context with established patterns', () => {
12
+ let result: BehaviorPattern[];
13
+ beforeEach(async () => {
14
+ const context = new a_patterns_service();
15
+ context.response.Data = [
16
+ BehaviorPatternDetailsResponse.create({
17
+ Id: 'pattern-1', GroupingKey: 'user-42', Facets: { Day: 'Monday', CommandType: 'RegisterInvoice' },
18
+ Confidence: 0.75, Support: 0.2, Occurrences: 9_007_199_254_740_993n, Weight: 3.5, Specificity: 2,
19
+ FirstSeen: { Value: '2026-01-05T09:00:00+02:00' }, LastSeen: { Value: '2026-01-12T09:00:00+02:00' }
20
+ }),
21
+ BehaviorPatternDetailsResponse.create({ Id: 'pattern-2', Confidence: 0.95 })
22
+ ];
23
+ result = await context.patterns.getPatterns('user-42', {});
24
+ });
25
+ it('should convert every response field without losing occurrence precision', () => {
26
+ result[0].should.deep.equal({
27
+ id: 'pattern-1', groupingKey: 'user-42', facets: { Day: 'Monday', CommandType: 'RegisterInvoice' },
28
+ confidence: 0.75, support: 0.2, occurrences: 9_007_199_254_740_993n, weight: 3.5, specificity: 2,
29
+ firstSeen: new Date('2026-01-05T07:00:00Z'), lastSeen: new Date('2026-01-12T07:00:00Z')
30
+ });
31
+ });
32
+ it('should preserve the server ranking', () => result.map(pattern => pattern.id).should.deep.equal(['pattern-1', 'pattern-2']));
33
+ it('should not invent a missing observation timestamp', () => should.equal(result[1].firstSeen, undefined));
34
+ });
@@ -0,0 +1,32 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { FacetName } from '../../index.js';
6
+ import { a_patterns_service } from '../given/a_patterns_service.fixture.js';
7
+
8
+ chai.should();
9
+
10
+ for (const method of ['getPatterns', 'getUsualActions'] as const) {
11
+ for (const limited of [false, true]) {
12
+ describe(`when ${method} queries facets ${limited ? 'with explicit limits' : 'with server defaults'}`, () => {
13
+ let context: a_patterns_service;
14
+ beforeEach(async () => {
15
+ context = new a_patterns_service();
16
+ await context.patterns[method]('user-42', {
17
+ [FacetName.CommandType]: 'RegisterInvoice',
18
+ [FacetName.AggregateType]: '',
19
+ CustomFacet: 'custom-value'
20
+ }, limited ? { minimumConfidence: 0.75, maximumResults: 3 } : undefined);
21
+ });
22
+ it('should scope the query and omit unspecified facet values', () => {
23
+ const client = method === 'getPatterns' ? context.client.matchingPatterns : context.client.usualActions;
24
+ client.mock.calls[0][0].should.deep.equal({
25
+ EventStore: 'accounts', Namespace: 'tenant-a', GroupingKey: 'user-42',
26
+ Context: { CommandType: 'RegisterInvoice', CustomFacet: 'custom-value' },
27
+ MinimumConfidence: limited ? 0.75 : 0, MaximumResults: limited ? 3 : 0
28
+ });
29
+ });
30
+ });
31
+ }
32
+ }
@@ -0,0 +1,34 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { DayOfWeek, FacetName, type FacetSet, TimeBucket } from '../../index.js';
6
+ import { a_patterns_service } from '../given/a_patterns_service.fixture.js';
7
+
8
+ chai.should();
9
+
10
+ describe('when querying at a moment with additional facets', () => {
11
+ let context: a_patterns_service;
12
+ const facets: FacetSet = Object.freeze({
13
+ [FacetName.AggregateType]: 'Invoice',
14
+ [FacetName.CommandType]: 'RegisterInvoice',
15
+ [FacetName.Day]: DayOfWeek.Friday,
16
+ [FacetName.TimeBucket]: TimeBucket.Evening,
17
+ CustomFacet: 'custom-value'
18
+ });
19
+ beforeEach(async () => {
20
+ context = new a_patterns_service();
21
+ await context.patterns.getPatternsAt('user-42', {
22
+ instant: new Date('2026-01-04T23:30:00Z'), offsetMinutes: 345
23
+ }, { alsoConstraining: facets, minimumConfidence: 0.8, maximumResults: 5 });
24
+ });
25
+ it('should ask about usual actions in the explicit offset day and bucket', () => {
26
+ context.client.usualActions.mock.calls[0][0].should.deep.equal({
27
+ EventStore: 'accounts', Namespace: 'tenant-a', GroupingKey: 'user-42',
28
+ Context: { ...facets, Day: DayOfWeek.Monday, TimeBucket: TimeBucket.EarlyMorning },
29
+ MinimumConfidence: 0.8, MaximumResults: 5
30
+ });
31
+ });
32
+ it('should not use the descriptive matching endpoint', () => context.client.matchingPatterns.mock.calls.should.have.lengthOf(0));
33
+ it('should leave the caller context unchanged', () => facets[FacetName.Day].should.equal(DayOfWeek.Friday));
34
+ });
@@ -0,0 +1,20 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { a_patterns_service } from '../given/a_patterns_service.fixture.js';
6
+
7
+ chai.should();
8
+
9
+ describe('when querying an invalid moment', () => {
10
+ let context: a_patterns_service;
11
+ let failure: unknown;
12
+ beforeEach(async () => {
13
+ context = new a_patterns_service();
14
+ try {
15
+ await context.patterns.getPatternsAt('user-42', { instant: new Date(NaN), offsetMinutes: 0 });
16
+ } catch (error) { failure = error; }
17
+ });
18
+ it('should reject the query', () => (failure as Error).should.be.instanceOf(RangeError));
19
+ it('should not send a misleading context to the kernel', () => context.client.usualActions.mock.calls.should.have.lengthOf(0));
20
+ });
@@ -0,0 +1,37 @@
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
+ import { afterEach, beforeEach, chai, describe, it, vi } from 'vitest';
5
+ import { DayOfWeek, FacetName, TimeBucket } from '../../index.js';
6
+ import { a_patterns_service } from '../given/a_patterns_service.fixture.js';
7
+
8
+ chai.should();
9
+
10
+ for (const withOptions of [false, true]) {
11
+ describe(`when querying now ${withOptions ? 'with additional facets' : 'with only a scope'}`, () => {
12
+ let context: a_patterns_service;
13
+ beforeEach(async () => {
14
+ vi.useFakeTimers({ toFake: ['Date'] });
15
+ vi.setSystemTime(new Date('2026-01-05T01:00:00Z'));
16
+ // Simulate a machine west of UTC regardless of the machine running the specs.
17
+ vi.spyOn(Date.prototype, 'getTimezoneOffset').mockReturnValue(240);
18
+ context = new a_patterns_service();
19
+ if (withOptions) {
20
+ await context.patterns.getPatternsAt('user-42', undefined, { alsoConstraining: { [FacetName.AggregateType]: 'Invoice' } });
21
+ } else {
22
+ await context.patterns.getPatternsAt('user-42');
23
+ }
24
+ });
25
+ afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); });
26
+ it('should capture now at the local system offset with server default limits', () => {
27
+ context.client.usualActions.mock.calls[0][0].should.deep.equal({
28
+ EventStore: 'accounts', Namespace: 'tenant-a', GroupingKey: 'user-42',
29
+ Context: {
30
+ ...(withOptions ? { AggregateType: 'Invoice' } : {}),
31
+ Day: DayOfWeek.Sunday, TimeBucket: TimeBucket.Evening
32
+ },
33
+ MinimumConfidence: 0, MaximumResults: 0
34
+ });
35
+ });
36
+ });
37
+ }