@cratis/pi 7.2.1 → 7.3.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 (27) hide show
  1. package/README.md +4 -2
  2. package/package/corpus/harnesses/pi/extensions/cratis-path-guidance/SkillTrigger.ts +1 -1
  3. package/package/corpus/harnesses/pi/extensions/cratis-path-guidance/index.ts +1 -1
  4. package/package/corpus/harnesses/pi/extensions/cratis-path-guidance/skills.ts +3 -4
  5. package/package/corpus/harnesses/pi/extensions/shared/frontmatter.ts +100 -4
  6. package/package/corpus/harnesses/pi/extensions/shared/skillFrontmatter.ts +27 -5
  7. package/package/corpus/rules/profiles.md +23 -0
  8. package/package/corpus/skills/cratis-application-react-specifications/SKILL.md +2 -3
  9. package/package/corpus/skills/cratis-application-slice-specifications/SKILL.md +2 -3
  10. package/package/corpus/skills/cratis-arc-command-typescript/LICENSE +2 -0
  11. package/package/corpus/skills/cratis-arc-command-typescript/SKILL.md +495 -0
  12. package/package/corpus/skills/cratis-arc-command-typescript/references/chronicle-events.md +141 -0
  13. package/package/corpus/skills/cratis-arc-command-typescript/references/hosting-and-storage.md +183 -0
  14. package/package/corpus/skills/cratis-arc-command-typescript/verification.json +15 -0
  15. package/package/corpus/skills/cratis-arc-query-typescript/LICENSE +2 -0
  16. package/package/corpus/skills/cratis-arc-query-typescript/SKILL.md +350 -0
  17. package/package/corpus/skills/cratis-arc-query-typescript/verification.json +14 -0
  18. package/package/corpus/skills/cratis-arc-validation-typescript/LICENSE +2 -0
  19. package/package/corpus/skills/cratis-arc-validation-typescript/SKILL.md +320 -0
  20. package/package/corpus/skills/cratis-arc-validation-typescript/verification.json +14 -0
  21. package/package/corpus/skills/cratis-documentation-writing/SKILL.md +2 -2
  22. package/package/corpus/skills/cratis-engineering-docs-authoring/SKILL.md +2 -2
  23. package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +2 -2
  24. package/package/corpus/skills/cratis-specifications-typescript/SKILL.md +2 -3
  25. package/package/corpus/skills/cratis-technical-examples/SKILL.md +2 -2
  26. package/package/profile-catalog.json +24 -0
  27. package/package.json +1 -1
@@ -0,0 +1,495 @@
1
+ ---
2
+ name: cratis-arc-command-typescript
3
+ description: Define a Cratis Arc command in a Node.js TypeScript server with Arc for TypeScript — project setup from the source preview, ArcApplication.createBuilder() and discovery, the @command() class with @field inputs, handle() and provide(), outcome helpers, service injection, authorization, returning Chronicle events through withChronicle, generated artifact metadata and proxies, hosting on the standalone host, Express, Fastify, Hono or the Fetch entry, and CommandScenario specs. Use when adding or changing a command in an Arc for TypeScript server, or bootstrapping such a server. Do not use for the .NET or Kotlin/Java Arc command shapes, for a React frontend that calls generated proxies, for validation-only changes, or for query/read-model definition.
4
+ license: MIT
5
+ ---
6
+
7
+ # Define an Arc command in TypeScript (Node.js server)
8
+
9
+ Arc for TypeScript (`Cratis/Arc.TypeScript`) is the Node.js server implementation
10
+ of Arc. A command is a decorated class that carries the input and handles itself;
11
+ Arc supplies the route, body binding, validation, authorization, and the
12
+ `CommandResult` envelope. There is no controller and no route to write.
13
+
14
+ ## Source preview — read this first
15
+
16
+ - **None of the server packages is published to npm.** `@cratis/arc.core`,
17
+ `@cratis/arc.express`, `@cratis/arc.fastify`, `@cratis/arc.hono`,
18
+ `@cratis/arc.testing`, `@cratis/arc.mongodb`, `@cratis/arc.drizzle`,
19
+ `@cratis/arc.chronicle`, `@cratis/cratis`, `@cratis/arc.proxygenerator` and
20
+ `@cratis/eslint-plugin-arc-core` are an unpublished source preview (0.x
21
+ manifests; versions appear only as source-only GitHub pre-releases). The API may still change. `npm install @cratis/arc.core` fails.
22
+ - **`@cratis/arc` is a different package.** `@cratis/arc`, `@cratis/arc.react` and
23
+ `@cratis/arc.react.mvvm` are the published **client** runtime, built from
24
+ `Cratis/Arc`. Generated proxies import them in the frontend. The server packages
25
+ never depend on them. Never tell a user to install `@cratis/arc` to get a
26
+ server.
27
+ - `@cratis/arc.chronicle` and `@cratis/cratis` are additionally marked
28
+ **experimental**.
29
+
30
+ ## Verified product sources
31
+
32
+ Verified against `Cratis/Arc.TypeScript` `main` at commit `94d398d` (tag
33
+ `v0.33.0` plus three commits), reading `Source/Core/index.ts` and the package
34
+ `index.ts` exports, `Documentation/**`, and the `Samples/Tasks` and
35
+ `Samples/Library` applications. Every API named here exists at that commit.
36
+ Chronicle SDK: `@cratis/chronicle` peer `^6.7.0`; the Library sample pins
37
+ `6.14.0`. Fundamentals: `@cratis/fundamentals` `^7.19.6`.
38
+
39
+ Host setup, the Fetch entry, and the storage integrations are in
40
+ [references/hosting-and-storage.md](references/hosting-and-storage.md). Returned
41
+ Chronicle events and their specs are in
42
+ [references/chronicle-events.md](references/chronicle-events.md).
43
+
44
+ ## Set up a project
45
+
46
+ Consume the preview in one of two ways:
47
+
48
+ | Path | How | Use when |
49
+ | --- | --- | --- |
50
+ | Inside a clone | Put the application under `Samples/<Name>` in a clone of `Cratis/Arc.TypeScript`; reference packages as `workspace:^` | Experimenting, or changing Arc while building on it |
51
+ | Packed tarballs | In the clone: `yarn install`, `yarn build`, then `yarn workspace @cratis/arc.core pack --out <file>` per package; `npm install <file>.tgz` in your project | A separate repository |
52
+
53
+ Use `yarn pack`, not `npm pack` — Yarn rewrites `workspace:^` dependencies to
54
+ version ranges; npm does not, and the tarball then fails to install. A
55
+ `workspace:^` dependency outside the clone fails at install.
56
+
57
+ - Node.js 22.19 or later (the core runs on 22; the workspace build needs 22.19;
58
+ 24 LTS is recommended).
59
+ - ES modules only: `"type": "module"` in `package.json`.
60
+ - Peer dependencies of `@cratis/arc.core`: `@cratis/fundamentals` `^7.19.6`,
61
+ `@opentelemetry/api` `^1.9.0`; `rxjs` `^7.8.2` is an optional peer, needed for
62
+ RxJS observable queries.
63
+ - TypeScript: the repository and proxy generator use TypeScript 6
64
+ (`typescript@npm:@typescript/typescript6@^6.0.2`, command `tsc6`).
65
+
66
+ ```json
67
+ {
68
+ "compilerOptions": {
69
+ "target": "ES2022",
70
+ "module": "NodeNext",
71
+ "moduleResolution": "NodeNext",
72
+ "lib": ["ES2022", "ESNext.Decorators"],
73
+ "types": ["node"],
74
+ "strict": true,
75
+ "verbatimModuleSyntax": true,
76
+ "skipLibCheck": true,
77
+ "rootDir": ".",
78
+ "outDir": "dist"
79
+ },
80
+ "include": ["main.ts", "Features/**/*.ts"]
81
+ }
82
+ ```
83
+
84
+ - **No `experimentalDecorators`.** Arc's decorators compile as standard TC39
85
+ decorators, as the samples do; `ESNext.Decorators` declares `Symbol.metadata`.
86
+ Legacy `experimentalDecorators` also work. The Library sample compiles its
87
+ `@cratis/chronicle` 6.14 event types and projections under the same
88
+ standard-decorator settings. Generated frontend proxies are the exception:
89
+ they compile with `experimentalDecorators: true`.
90
+ - With `NodeNext`, relative imports carry a `.js` extension. `ESNext` +
91
+ `Bundler` (the samples' choice) also works.
92
+ - Node's own type stripping does not transform decorators — `node main.ts`
93
+ fails at the first `@`. Run source with `tsx watch main.ts` during
94
+ development and compile with `tsc6` for production.
95
+ - A Vitest/Vite transform must lower standard decorators with
96
+ `useDefineForClassFields: true` (`target: 'es2022'`,
97
+ `experimentalDecorators: false`), or decorators silently do nothing in specs.
98
+
99
+ ## Bootstrap the application
100
+
101
+ `Samples/Tasks/main.ts`:
102
+
103
+ ```typescript
104
+ import { ArcApplication } from '@cratis/arc.core';
105
+ import { Tasks } from './Features/Tasks/Tasks.js';
106
+ import { metadata } from './Features/generatedMetadata.js';
107
+
108
+ const builder = ArcApplication.createBuilder();
109
+ builder.useGeneratedMetadata(metadata);
110
+ builder.services.addSingleton(Tasks);
111
+ await builder.discover(new URL('./Features/', import.meta.url));
112
+ export const app = await builder.build();
113
+ await app.run({ port: Number(process.env.PORT ?? 3000) });
114
+ ```
115
+
116
+ | Call | Effect |
117
+ | --- | --- |
118
+ | `ArcApplication.createBuilder(options?)` | Starts the application; the Node builder reads `appsettings.json` (`Cratis:Arc`) and `Cratis__...` environment variables |
119
+ | `useGeneratedMetadata(metadata)` | Installs parameter and return-type metadata extracted from source. Call it **before** `discover()`/`add()` |
120
+ | `services.addSingleton(Token)` | Registers a service; `addScoped`/`addTransient` take factories. Classes can also carry `@singleton()`, `@scoped()`, `@transient()` |
121
+ | `discover(folderUrl)` | Imports every module under the folder and registers **exported**, decorated artifacts. Skips `dist`, `node_modules`, `given`, `for_*`, `index.*`, declaration files |
122
+ | `add(...types)` | Registers artifacts explicitly — required on the Fetch entry, which cannot discover |
123
+ | `build()` | Validates the whole graph (missing services, lifetime mismatches, misplaced decorators, stale metadata) and throws before any listener opens |
124
+ | `app.run()` / `app.start()` | Standalone Node host. `app.dispose()` stops it and disposes services |
125
+
126
+ `discover()` refuses a folder that contains the entry point; keep artifacts in a
127
+ dedicated folder such as `Features/`. `Cratis:Arc:Development: true` is for a
128
+ local machine only.
129
+
130
+ ## Lay out the slice
131
+
132
+ House convention (not a runtime requirement): one folder per behavior, one
133
+ TypeScript file named for the behavior holding the command, its validator and —
134
+ for Chronicle — its events. Concepts shared by several slices live one level up.
135
+ Specs sit beside the slice in `for_<Subject>/when_<action>/<case>.ts`, which
136
+ `discover()` skips.
137
+
138
+ ```text
139
+ Features/Tasks/
140
+ ├── TaskId.ts
141
+ ├── TaskTitle.ts
142
+ ├── Tasks.ts
143
+ ├── Registration/
144
+ │ ├── Registration.ts
145
+ │ └── for_RegisterTask/when_registering/with_valid_title.ts
146
+ └── Listing/
147
+ └── Listing.ts
148
+ ```
149
+
150
+ ## Declare the command
151
+
152
+ `Samples/Tasks/Features/Tasks/Registration/Registration.ts`:
153
+
154
+ ```typescript
155
+ import { field } from '@cratis/fundamentals';
156
+ import { command, CommandValidator, validator } from '@cratis/arc.core';
157
+ import { TaskId } from '../TaskId.js';
158
+ import { TaskTitle } from '../TaskTitle.js';
159
+ import { Tasks } from '../Tasks.js';
160
+
161
+ /** Register a task. */
162
+ @command()
163
+ export class RegisterTask {
164
+ @field(TaskId) id!: TaskId;
165
+ @field(TaskTitle) title!: TaskTitle;
166
+
167
+ handle(tasks: Tasks): TaskId {
168
+ tasks.register(this.id, this.title);
169
+ return this.id;
170
+ }
171
+ }
172
+
173
+ @validator(RegisterTask)
174
+ export class RegisterTaskValidator extends CommandValidator<RegisterTask> {
175
+ constructor() {
176
+ super();
177
+ this.ruleFor(command => command.title).notEmpty().withMessage('A title is required');
178
+ }
179
+ }
180
+ ```
181
+
182
+ Rules Arc enforces:
183
+
184
+ - `@command()` from `@cratis/arc.core` marks the class; it needs a public
185
+ instance `handle()` (which may be inherited). Name it as the action —
186
+ `RegisterTask`, not `RegisterTaskCommand`.
187
+ - `@field(Type)` comes from **`@cratis/fundamentals`**, not Arc. Every field is
188
+ required unless marked `@optional()`, `@nullable()`, or `@defaultValue(value)`
189
+ (from `@cratis/arc.core`). `@enumeration(EnumObject)` restricts a scalar field.
190
+ Arrays: `@field(Array, { genericArguments: [Item] })`.
191
+ - Domain values are `ConceptAs<T>` classes with a static `valueType`
192
+ (`static readonly valueType = Guid;`) because generics are erased at runtime.
193
+ A concept travels as its primitive on the wire.
194
+ - The artifact must be **exported** from a module under the discovery root, or
195
+ it is never registered.
196
+ - `handle()` returns a value or a promise; the value becomes `response`.
197
+ Returning nothing is success without a `response`.
198
+
199
+ ### The command key
200
+
201
+ The key identifies what the command is about. It is resolved from, in order: a
202
+ registered `CommandKeyResolver`, a `getKey()` method, then the `@key()` field
203
+ (a concept unwraps to its primitive). **No key is inferred from an unmarked `id`
204
+ field.** With Chronicle, the key is the default event source.
205
+
206
+ ### Route
207
+
208
+ `POST /api/<discovery-namespace>/<command-name>` in kebab case —
209
+ `Tasks.Registration.RegisterTask` serves `POST /api/tasks/registration/register-task`.
210
+ `POST <route>/validate` runs authorization and validation only, never
211
+ `provide()` or `handle()`. Pin a route across folder moves with
212
+ `@command({ namespace: 'Tasks.Registration' })` or `@path('/api/...')`, and keep
213
+ class names in bundled builds (`keepNames` in esbuild).
214
+
215
+ ## Bind parameters
216
+
217
+ TypeScript erases parameter types, so Arc learns them one of two ways:
218
+
219
+ - **Generated artifact metadata** (the samples): `arc-proxygenerator --metadata`
220
+ writes a module from source; `handle(tasks: Tasks)` then needs no decorator.
221
+ - **Explicit tokens**: `@inject(Tasks)` on `handle()` or `provide()`, one token
222
+ or marker per parameter, in order.
223
+
224
+ Built-in markers for `@inject(...)`:
225
+
226
+ | Marker | Binds |
227
+ | --- | --- |
228
+ | `abortSignal()` | The request's `AbortSignal` — pass it to cancelable I/O |
229
+ | `commandContext()` | The `CommandContext`: `key`, `values`, `correlationId`, `principal`, `tenantId`, `signal` |
230
+ | `provided(Type)` | A value from `provide()`, matched by runtime type |
231
+ | `commandReadModel(Type, { optional? })` | The read model for the command key, from a registered resolver (MongoDB, Drizzle, Chronicle) |
232
+ | `mongoCollection(Model)` / `drizzleDatabase()` | Storage handles from the optional integrations |
233
+
234
+ `build()` fails with `Unbound handle parameters on <Type>.handle` when neither
235
+ metadata nor tokens cover a parameter. Default and rest parameters always need
236
+ explicit tokens.
237
+
238
+ ## Prepare data with `provide()`
239
+
240
+ `provide()` is optional. It runs after authorization and validation, before
241
+ `handle()`, on the same instance; its value becomes the **first** `handle()`
242
+ argument, before injected services.
243
+
244
+ ```typescript
245
+ import { field } from '@cratis/fundamentals';
246
+ import { command, inject, rejected, validation } from '@cratis/arc.core';
247
+
248
+ @command()
249
+ export class RenameTask {
250
+ @field(TaskId) id!: TaskId;
251
+ @field(TaskTitle) title!: TaskTitle;
252
+
253
+ @inject(Tasks)
254
+ provide(tasks: Tasks) {
255
+ const task = tasks.byId(this.id);
256
+ return task ?? rejected(validation('The task does not exist', ['id'], 'notFound'));
257
+ }
258
+
259
+ handle(task: TaskItem): void {
260
+ task.title = this.title;
261
+ }
262
+ }
263
+ ```
264
+
265
+ For several prepared values, return `tuple(...)` and mark each `handle()`
266
+ parameter with `@inject(provided(A), commandContext(), provided(B))`.
267
+ `@inject` is accepted only on a command's `handle()` or `provide()`;
268
+ authorization decorators on `provide()` fail at build.
269
+
270
+ ## Return an outcome
271
+
272
+ | Return from `provide()` or `handle()` | Result |
273
+ | --- | --- |
274
+ | A plain value, or `response(value)` | Success; the value is `response` |
275
+ | Nothing | Success without `response` |
276
+ | `rejected(...results)` | 400 with the validation results; later steps do not run |
277
+ | `denied(reason?)` | 403 with `authorizationFailureReason`; later steps do not run |
278
+ | `tuple(a, b, ...)` | At most one client response; every other value must be consumed by a response value handler (Chronicle events, command operations) or the command fails |
279
+
280
+ Outcomes are recognized by origin (a private brand), not shape — an object with
281
+ a `kind` property is ordinary data. `rejected()` with no results throws. A
282
+ thrown error is a 500 whose message is redacted unless `exposeExceptionDetails`
283
+ is enabled; do not throw for a rejection the user can act on. Rules that can
284
+ run before `handle()` belong in validators: see
285
+ `cratis-arc-validation-typescript`.
286
+
287
+ ## Protect the command
288
+
289
+ ```typescript
290
+ import { authorize, command, roles } from '@cratis/arc.core';
291
+
292
+ @command()
293
+ @roles('Librarian')
294
+ export class RegisterAuthor { /* ... */ }
295
+
296
+ @command()
297
+ @authorize({ policy: 'Finance' })
298
+ export class ApproveBudget { /* ... */ }
299
+ ```
300
+
301
+ - No decorator allows everyone, including anonymous callers.
302
+ - `@authorize()` requires an authenticated caller; `@authorize({ policy?, roles?, schemes? })`
303
+ a policy, a role, or a scheme; `@roles('A', 'B')` any listed role;
304
+ `@allowAnonymous()` states anonymous access explicitly.
305
+ - Stacked decorators must **all** pass. `@allowAnonymous()` with an authenticated
306
+ requirement on the same class fails at startup.
307
+ - Authorization belongs on the **class**. Decorators on `handle()`, `provide()`
308
+ or any other command method fail the build (a deliberate difference from Arc
309
+ on .NET, which ignores them).
310
+ - A decision that needs loaded data returns `denied(reason)` from `provide()`.
311
+
312
+ ## Return Chronicle events
313
+
314
+ With `@cratis/arc.chronicle` registered (`withChronicle`), `handle()` returns the
315
+ event and Arc appends it after authorization, validation and `provide()` pass,
316
+ in the namespace of the resolved tenant. Arc does not require event sourcing: a
317
+ command without Chronicle calls a service, as `RegisterTask` does.
318
+
319
+ `Samples/Library/Features/Authors/Registration/Registration.ts`:
320
+
321
+ ```typescript
322
+ import { field } from '@cratis/fundamentals';
323
+ import { eventType } from '@cratis/chronicle/events';
324
+ import { command, key, roles } from '@cratis/arc.core';
325
+ import { AuthorId } from '../AuthorId.js';
326
+ import { AuthorName } from '../AuthorName.js';
327
+
328
+ @eventType()
329
+ export class AuthorRegistered {
330
+ @field(AuthorName) name: AuthorName;
331
+ constructor(name: AuthorName = new AuthorName('')) { this.name = name; }
332
+ }
333
+
334
+ @command()
335
+ @roles('Librarian')
336
+ export class RegisterAuthor {
337
+ @key() @field(AuthorId) id!: AuthorId;
338
+ @field(AuthorName) name!: AuthorName;
339
+
340
+ handle(): AuthorRegistered { return new AuthorRegistered(this.name); }
341
+ }
342
+ ```
343
+
344
+ - The event class lives in the slice file beside the command, carries
345
+ `@eventType()` from `@cratis/chronicle/events` and `@field` declarations, and
346
+ is created through its **constructor** in `handle()` — the house pattern in
347
+ both samples.
348
+ - `@key()` names the event source; without a key or `getEventSourceId()`, each
349
+ execution gets a new UUID.
350
+ - A returned event is consumed on the server, so the result carries no
351
+ `response`.
352
+ - Never append through the SDK inside `handle()` — that append is outside the
353
+ command's batch.
354
+
355
+ Returning several events, events beside a response, cross-stream targets,
356
+ routing decorators, concurrency, and the in-memory `ChronicleCommandScenario`
357
+ are in [references/chronicle-events.md](references/chronicle-events.md).
358
+
359
+ ## Generate metadata and proxies
360
+
361
+ `arc-proxygenerator` (package `@cratis/arc.proxygenerator`, also unpublished)
362
+ reads the TypeScript source with the compiler API — it never queries a running
363
+ server. It writes server metadata and client proxies in one run. The CLI
364
+ rejects relative paths, and the output folder must exist:
365
+
366
+ ```javascript
367
+ import { spawnSync } from 'node:child_process';
368
+ import { mkdirSync } from 'node:fs';
369
+ import process from 'node:process';
370
+ import { fileURLToPath } from 'node:url';
371
+
372
+ const path = relative => fileURLToPath(new URL(relative, import.meta.url));
373
+ const cli = fileURLToPath(new URL('./cli.js', import.meta.resolve('@cratis/arc.proxygenerator')));
374
+ mkdirSync(path('./generated'), { recursive: true });
375
+ const result = spawnSync(process.execPath, [cli,
376
+ '--project', path('./tsconfig.json'),
377
+ '--artifacts', path('./Features'),
378
+ '--output', path('./generated'),
379
+ '--metadata', path('./Features/generatedMetadata.ts'),
380
+ ...process.argv.slice(2)], { stdio: 'inherit' });
381
+ process.exitCode = result.status ?? 1;
382
+ ```
383
+
384
+ - Commit the metadata module; never edit it. Regenerate after **every** change
385
+ to a command, read model or validator — `useGeneratedMetadata` throws
386
+ `Stale generated artifact metadata for <Type>` otherwise. Reordering
387
+ parameters without changing their count goes undetected, so gate CI with
388
+ `--check-metadata` and keep `--watch` running beside `tsx` while developing.
389
+ - `--artifacts` must be the folder passed to `discover()`. Match route options
390
+ (`--api-prefix`, `--segments-to-skip`, `--root-namespace`) to the server's
391
+ `generatedApis` options, or proxies call URLs the server does not serve.
392
+ - Generated proxies import the **published** `@cratis/arc` and
393
+ `@cratis/arc.react` (the docs pin `22.19.1`) plus `@cratis/fundamentals`.
394
+ Compile them in `Bundler` resolution with `experimentalDecorators: true`, and
395
+ import `reflect-metadata` once in the frontend entry. Consuming the proxies in
396
+ React is `cratis-arc-react-page`.
397
+
398
+ ## Host the application
399
+
400
+ `app.run()` is the standalone Node host. To mount the same built application in
401
+ Express 5, Fastify 5, Hono 4, or a Fetch API runtime, keep the builder and swap
402
+ the host — see
403
+ [references/hosting-and-storage.md](references/hosting-and-storage.md). Mount
404
+ Express's `cratisArc(arc)` **before** any body parser, or every command answers
405
+ 400 `malformedRequest`.
406
+
407
+ ## Specify the command
408
+
409
+ `@cratis/arc.testing` runs a command through the real pipeline in-process —
410
+ binding, authorization, validators, services, `provide()`, `handle()` — without
411
+ a listener. From `Samples/Tasks`:
412
+
413
+ ```typescript
414
+ import { CommandScenario } from '@cratis/arc.testing';
415
+ import { Tasks } from '../../../Tasks.js';
416
+ import { RegisterTask, RegisterTaskValidator } from '../../Registration.js';
417
+ import { metadata } from '../../../../generatedMetadata.js';
418
+
419
+ export class a_task_registration {
420
+ tasks = new Tasks();
421
+ scenario = CommandScenario.for(RegisterTask, RegisterTaskValidator);
422
+
423
+ constructor() {
424
+ this.scenario.extend(builder => builder.useGeneratedMetadata(metadata));
425
+ this.scenario.services.addSingleton(Tasks, this.tasks);
426
+ }
427
+ }
428
+ ```
429
+
430
+ ```typescript
431
+ import { given, type ScenarioCommandResult } from '@cratis/arc.testing';
432
+ import { TaskId } from '../../../TaskId.js';
433
+ import { TaskTitle } from '../../../TaskTitle.js';
434
+ import { a_task_registration } from '../given/a_task_registration.js';
435
+
436
+ describe('when registering a task with a valid title', given(a_task_registration, context => {
437
+ const id = TaskId.create();
438
+ let result: ScenarioCommandResult;
439
+
440
+ beforeAll(async () => {
441
+ result = await context.scenario.execute({ id, title: new TaskTitle('Plan release') });
442
+ });
443
+ afterAll(async () => { await context.scenario.dispose(); });
444
+
445
+ it('should succeed through the command pipeline', () => { result.shouldBeSuccessful(); });
446
+ }));
447
+ ```
448
+
449
+ - `CommandScenario.for(Command, ...artifacts)` — pass the validators and other
450
+ artifacts the command needs; it cannot discover classes that were never
451
+ imported.
452
+ - Register fakes on `scenario.services` **before** the first call.
453
+ `extend(builder => ...)` installs builder setup such as generated metadata.
454
+ - `execute(values)` runs everything; `validate(values)` stops before
455
+ `provide()`/`handle()`. `withContext({ principal, tenantId, correlationId })`
456
+ sets a trusted caller.
457
+ - `given(Context, ...)` creates **one** context per `describe`: act in
458
+ `beforeAll`, dispose in `afterAll`. A disposed scenario cannot run again.
459
+ - Assertions on the result: `shouldBeSuccessful()`, `shouldNotBeSuccessful()`,
460
+ `shouldBeValid()`, `shouldHaveValidationErrors()`,
461
+ `shouldHaveValidationErrorForMember(member)`, `shouldHaveValidationErrorFor(text)`,
462
+ `shouldHaveValidationErrorBecauseOf(reason)`, `shouldBeAuthorized()`,
463
+ `shouldNotBeAuthorized()`, `shouldHaveExceptions()`, `shouldNotHaveExceptions()`.
464
+ - Specify authorization for three callers — anonymous, without the role, with
465
+ it — and on `validate()` too.
466
+
467
+ Folder and naming conventions for specs are `cratis-specifications-typescript`.
468
+
469
+ ## Route near misses
470
+
471
+ - A rejection rule on an existing command: `cratis-arc-validation-typescript`.
472
+ - A `@readModel()` query or observable query: `cratis-arc-query-typescript`.
473
+ - A React page that calls the generated proxies: `cratis-arc-react-page`.
474
+ - Talking to Chronicle outside Arc (a worker, direct append, reactors,
475
+ projections): `cratis-chronicle-client-typescript`.
476
+ - The .NET command shape: `cratis-arc-command`; Kotlin/Java:
477
+ `cratis-arc-command-kotlin`.
478
+
479
+ ## Verify
480
+
481
+ - The command class is exported under the discovery root, carries `@command()`,
482
+ has a public instance `handle()`, and carries authorization only on the class
483
+ — never on `handle()`, `provide()` or another method.
484
+ - Every input field has `@field(Type)` from `@cratis/fundamentals`; domain values
485
+ are concepts with a static `valueType`.
486
+ - The key is explicit (`@key()`, `getKey()`, or a resolver) wherever something
487
+ depends on it — never an assumed `id`.
488
+ - Rejections the user can act on are `rejected(...)`/`denied(...)` or validator
489
+ results, never thrown errors.
490
+ - Generated metadata is regenerated and `--check-metadata` passes; `build()`
491
+ succeeds at startup.
492
+ - The command's `CommandScenario` specs pass, including the unauthorized and
493
+ `/validate` cases.
494
+ - Guidance never claims the server packages are on npm, and never substitutes
495
+ `@cratis/arc` (the client) for `@cratis/arc.core` (the server).
@@ -0,0 +1,141 @@
1
+ # Returning Chronicle events from an Arc for TypeScript command
2
+
3
+ Verified against `Cratis/Arc.TypeScript` `main` at commit `94d398d` (tag
4
+ `v0.33.0` plus three commits): `Documentation/chronicle/**`,
5
+ `Documentation/testing/chronicle.md`, `Source/Chronicle/index.ts`, and
6
+ `Samples/Library`. `@cratis/arc.chronicle` is **experimental** and, like every
7
+ package in the repository, not published to npm. `@cratis/chronicle` (the SDK)
8
+ is published; the integration peers `^6.7.0`.
9
+
10
+ ## Register the integration
11
+
12
+ ```typescript
13
+ import { ArcApplication } from '@cratis/arc.core';
14
+ import '@cratis/arc.chronicle';
15
+ import { metadata } from './Features/generatedMetadata.js';
16
+
17
+ const builder = ArcApplication.createBuilder();
18
+ builder.useGeneratedMetadata(metadata);
19
+ builder.withChronicle({ connectionString: 'chronicle://localhost:35000', eventStore: 'MyArcApp' });
20
+ await builder.discover(new URL('./Features/', import.meta.url));
21
+ const app = await builder.build();
22
+ await app.run({ port: 3000 });
23
+ ```
24
+
25
+ The Library sample uses the equivalent function form,
26
+ `withChronicle(builder, options)`. Discovery hands every discovered event type,
27
+ projection, reducer, reactor and constraint to the Arc-owned client; they must be
28
+ **exported** beneath the discovery root. You do not import `reflect-metadata` for
29
+ the SDK — it imports it itself.
30
+
31
+ ## What `handle()` may return
32
+
33
+ | `handle()` returns | Result |
34
+ | --- | --- |
35
+ | A registered event instance | Appended to the command's event source |
36
+ | An array of registered events | Appended in one batch |
37
+ | An array of ordinary objects | An ordinary response, not events |
38
+ | `tuple(event, 'message')` | The event is appended to the command key; `'message'` is the response |
39
+ | `tuple(eventSourceIdResponse(id), event)` | The event is appended to `id`, and `id` is the response |
40
+ | `eventForEventSourceId({ eventSourceId, event, eventSourceType?, subject?, tags? })` | Appended to that event source with explicit routing |
41
+ | `eventsWithConcurrencyScopes(events, scopes)` | Appended with exact per-source concurrency scopes |
42
+ | `tuple(event, operation)` | The command operation runs first; the event batch then decides compensation (compensated on a known append rejection, not on an unknown outcome) |
43
+
44
+ `tuple` comes from `@cratis/arc.core`; `eventSourceIdResponse`,
45
+ `eventForEventSourceId`, and `eventsWithConcurrencyScopes` from
46
+ `@cratis/arc.chronicle`. The wrappers are branded, so a DTO that happens to have
47
+ `event` and `eventSourceId` fields is never mistaken for one.
48
+
49
+ ## The event source
50
+
51
+ | The command declares | Event source ID |
52
+ | --- | --- |
53
+ | `getEventSourceId()` | Its result (string, `Guid`, or a concept over either); takes precedence |
54
+ | A `@key()` field | That field's value |
55
+ | Neither | A new UUID per execution |
56
+
57
+ ## Routing defaults and concurrency
58
+
59
+ Class decorators from `@cratis/arc.chronicle` set defaults for every returned
60
+ event:
61
+
62
+ | Decorator | Sets |
63
+ | --- | --- |
64
+ | `@eventSourceType('Book', { concurrency? })` | Event source type |
65
+ | `@eventStreamType('Lending', { concurrency? })` | Event stream type |
66
+ | `@eventStreamId('main', { concurrency? })` | Event stream ID |
67
+ | `@eventSubject('subject')` | Compliance subject |
68
+ | `@notAudited()` (on a command **field**) | Keeps the value out of the permanent causation chain |
69
+
70
+ `{ concurrency: true }` reads that dimension's tail **after** `handle()` and
71
+ before the append. It does not protect a decision made from state read earlier.
72
+ For "create once", return `eventsWithConcurrencyScopes([event], { [this.id]:
73
+ { eventSourceId: true, sequenceNumber: EventSequenceNumber.beforeFirst.value } })`
74
+ (`EventSequenceNumber` from `@cratis/chronicle/eventSequences`). For a decision
75
+ from the source's full history, use a command aggregate (`commandAggregate`).
76
+ For a rule across sources, such as a unique name, use a Chronicle constraint in
77
+ the slice file (`@constraint()` from `@cratis/chronicle/events`, as the Library
78
+ sample's `UniqueAuthorName` does).
79
+
80
+ ## Rejections and the batch
81
+
82
+ - A constraint or concurrency rejection answers 400 with a validation result
83
+ (`reason` `constraintViolation` or `concurrencyViolation`) and appends nothing.
84
+ - Events returned by nested Arc commands that share tenant, correlation ID and
85
+ event store join the outer command's batch, sent once after the outer command
86
+ succeeds. Any failure discards every staged event.
87
+ - An unknown, partial or incomplete acknowledgment fails the command with an
88
+ exception, because Arc cannot tell what was stored.
89
+ - An SDK `eventLog.append(...)` inside `handle()` is outside the batch. Return
90
+ events instead.
91
+ - Appending and projecting are separate: a query sent right after the command
92
+ may not see the event yet. Prefer an observable query on the client.
93
+ `completionTimeoutMs` makes the command wait for observers, but Chronicle waits
94
+ for **every** observer on the log (Cratis/Chronicle#4132), so a projection or
95
+ reactor that does not handle the event makes every command time out with 500.
96
+
97
+ ## Read current state in the command
98
+
99
+ `@inject(commandReadModel(Book))` hands `handle()` the Chronicle read model
100
+ projected for the command key; a missing model rejects the command unless
101
+ `{ optional: true }`, which passes `null`. `ChronicleReadModels` (via
102
+ `service(ChronicleReadModels)` or `@inject`) reads other instances. The
103
+ validation-side equivalent is `readModelForValidation(Type)`.
104
+
105
+ ## Reactors that return commands
106
+
107
+ A Chronicle reactor (from `@cratis/chronicle/reactors`) may return an Arc
108
+ `@command()` instance, or a non-empty array of only commands; Arc runs each
109
+ through the full command pipeline. Handlers are matched by camelCase event-class
110
+ method name, receive parsed JSON content (not a class instance), and run on
111
+ replay by default since SDK 6.9.0 — mark effects `@onceOnly()`, and keep returned
112
+ commands safe to repeat after a failed-partition re-delivery. Reactor authoring
113
+ itself is `cratis-chronicle-client-typescript`.
114
+
115
+ ## Specify it without a kernel
116
+
117
+ `ChronicleCommandScenario` from `@cratis/arc.chronicle/testing` runs the real
118
+ pipeline with an in-memory event log:
119
+
120
+ ```typescript
121
+ import { ChronicleCommandScenario } from '@cratis/arc.chronicle/testing';
122
+
123
+ const scenario = ChronicleCommandScenario.for(LendBook, BookLent, ReturnDateSet, LendBookValidator, Book);
124
+ scenario.givenReadModel(Book, bookId.toString(), book);
125
+ const result = await scenario.execute({ bookId, member: 'member-1', days: 14 });
126
+ result.shouldBeSuccessful();
127
+ result.appendedEvents.should.have.lengthOf(2);
128
+ result.shouldHaveAppendedEvent(BookLent, bookId.toString(), event => event.member === 'member-1');
129
+ await scenario.dispose();
130
+ ```
131
+
132
+ - Pass the command, its event types, validators and read-model types to
133
+ `for(...)`; the in-memory log refuses an unregistered event type.
134
+ - `givenReadModel(Type, id, instance, tenant?)` pins state;
135
+ `scenario.given.forEventSource(id).events(...)` seeds reducer history (SDK
136
+ 6.14.0 or later).
137
+ - `shouldHaveAppendedEvent` does not check count or order — assert
138
+ `appendedEvents` when either matters.
139
+ - The in-memory scenario does **not** run projections, aggregates, constraints
140
+ or concurrency checks. `ChronicleKernelScenario` runs against a live kernel at
141
+ `ARC_CHRONICLE_TEST_URL` for those.