Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ The kit is upstream of both: it defines *what* a Specfuse spec must look like. T

## Status

**Incubating** (`v0.32.0`), Apache-2.0. Handbooks, samples, schemas, the `project-init` template, the bundled `hello-orders` example, the `specfuse-authoring` plugin (in the `specfuse/specfuse` marketplace), and the `specfuse authoring` CLI are all in place. Generator-side alignment items are tracked in [`compatibility.md`](compatibility.md#outstanding-generator-side-follow-ups).
**Incubating** (`v0.33.0`), Apache-2.0. Handbooks, samples, schemas, the `project-init` template, the bundled `hello-orders` example, the `specfuse-authoring` plugin (in the `specfuse/specfuse` marketplace), and the `specfuse authoring` CLI are all in place. Generator-side alignment items are tracked in [`compatibility.md`](compatibility.md#outstanding-generator-side-follow-ups).

The kit is distributed on PyPI as `specfuse-authoring` and hosted under [`Specfuse/authoring`](https://github.com/Specfuse/authoring). The code generator it drives is distributed separately as a pinned, checksum-verified release asset (see [`generator.lock`](generator.lock)); `specfuse authoring generate` resolves, verifies, and runs it on demand.

Expand Down
29 changes: 28 additions & 1 deletion compatibility.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions docs/csharp-component-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ The generated `TestSeed` (stable GUIDs and a pre-wired entity graph) and the `A.

From generator 0.22.0, also call `TestSeed.SeedSpareParents(context)` from `EnsureFakeData()`, beside the rows you already add. It seeds a spare parent row for each `uniqueBy` tuple made only of references (one gadget per owner), so the generated create test does not collide with the seeded child and answer 409. It is emitted on every `TestSeed`, empty when no tuple needs it, so the call can stay put. Without it, the create test answers 404 on the parent lookup.

**Known failures through generator 0.25.0.** On a relational provider the call can fail the whole seed, because a spare repeats its builder's values and hits a unique index on the parent itself (`clabonte/generator#3366`). Until that is fixed, add those parents yourself first with values of your own; the method skips a row already present. The in-memory provider enforces no index. Separately, a create test that names `TestSeed.Spare<Parent>Id` can answer 403 for a tenant-scoped role, because that role's token does not reach the spare (`clabonte/generator#3365`). The API is right to refuse, so treat those rows as known failures in a functional baseline.

Also from 0.22.0, the generated service unit tests assert what each emitted event carries and the exact set of events an update emits, including `x-trigger-when` transitions. A red service test after a regen means the service emits something other than what the spec implies, not a flaky test: fix the service, there is no opt-out. Events now carry the acting user: generated controllers and worker handler bases enter `EventActorScope`, so `EventEnvelope.UserId` is filled. Hand-written code that emits on behalf of a user outside those entry points (a hand-written endpoint, a hub, a background service) wraps the work in `using var _ = EventActorScope.Enter(userId);` with the domain user id.

## 5. Extending generated code
Expand Down Expand Up @@ -467,6 +469,18 @@ Raw SQL or a dashboard reading `Status = 0` must change. Code using `OutboxStatu
- **A `400` where the client used to succeed.** `filter` accepts only `filterableProperties` plus `Id`, `CreatedAt` and `UpdatedAt`, and an enum value in a request must match the spec exactly, case included, with `"unknown"` refused unless the enum declares it or is `x-extensible`. Stored enum data is unaffected.
- **A `403` or `404` on a write.** A project declaring `tenancy` gets a write guard: a write outside the caller's visibility answers `403`, and a body reference outside it `404`, where a missing id used to be a `500` and another tenant's id was accepted.

### 9.3 Upgrading to generator 0.25

No migration is required by this release. It changes what hand-written code and tests can see:

- **A test expecting a wrapped exception.** A domain exception now arrives as itself, not inside `InternalException`, so a 401, 403, 409, 412 or 422 raised below a service no longer answers 500. A test asserting the wrapper must assert the exception, and a host filter that unwraps `InternalException` can go. A group that writes its own exception classes must declare all ten that `domainExceptions` emits.
- **A `404` from a system path.** The write guard now checks references for an unrestricted caller (`RecordVisibility.All`) too: a missing target answers `404` rather than a foreign-key `500`, and a reference into another tenant is refused for every caller. A system path that wrote a cross-tenant reference on purpose must have its column declare `x-references: none`. A pillar-bound group's guard also compares a reference to a held `Read{Entity}` replica. A guarded save of a changed reference costs one more query per target type.
- **A `400` for a bad `filter` value.** A value that cannot convert to its property's type answers `400` for every type, where several answered `500`.
- **Partitioned Service Bus topics.** The dispatcher now sends one batch per session. Rows already `Poisoned` by the old refusal stay `Poisoned`.
- **`BlastRadius` values move.** It counts the route again, and a top-level list only superuser roles may call reads `platform`. `RequiresConfirmation` does not change. Recheck anything gating on the radius.
- **Hand-written workarounds you can delete:** a filter adding `Location`, `API-Version` or `ETag`, a `<Compile Remove>` of the authorization conformance test in a spec with no roles, and a placeholder type written to get past CS0246 in a `pillar` or `domains` group.
- **`Location` on a create can be absent on purpose.** When several reads spell the created entity's key, none is set. A client must then take the id from the response body.

## 10. The audit checklist

Run from the component repo root. Every check names the section with the explanation and the fix. The IDs are stable; the interactive page uses the same ones.
Expand Down
48 changes: 48 additions & 0 deletions docs/generator-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,54 @@ Newest first. An entry is **pending** until the kit release that pins it is publ
| **Tracked issues** | you were waiting on a fix |
| **Kit changes** | you want to know what moved in the handbooks, rules and tooling |

## 0.25.0

**Status: pending.** To be pinned by kit `v0.33.0` (jar sha256 `a8649a70…5df1cc9`; full record in [`compatibility.md`](https://github.com/specfuse/authoring/blob/main/compatibility.md) §61). Generator release date: 2026-11-04.

### Do not use yet

**Still do not register `mcpHost` or `mcpEvalHarness` on a real manifest.** This is unchanged from `0.24.0`: **a preview tool commits instead of rolling back.** FEAT-GEN-2026-0259 tracks the repair.

### Before you regenerate

**Spec authors**

- **A tenant-scoped event must require the tenant header (ERROR).** An event under the tenant, on a trait that leaves the header optional, fails with `ASYNC_TENANT_HEADER_NOT_REQUIRED`. Move it to the tenant-required trait. A nullable tenant foreign key and the sub-tenant are not reported. An event below the tenant through a `belongsTo` chain still has to stay on the global trait: see the known issue below.

**C# component repos.** No migration is required. [`csharp-component-guide.md`](csharp-component-guide.md) §9.3 has the steps.

- **Domain exceptions are no longer wrapped.** A 401, 403, 409, 412 or 422 raised below a service stops answering 500. A test asserting `InternalException` must assert the exception itself. A group with its own exception classes must declare all ten that `domainExceptions` emits.
- **The write guard covers unrestricted callers and tenant crossings.** Under `RecordVisibility.All`, a missing target answers 404, and a reference into another tenant is refused for every caller, including a held `Read{Entity}` replica. A column meant to cross declares `x-references: none`.
- **A bad `filter` value answers 400** for every type, not 500.
- **`BlastRadius` values move.** `RequiresConfirmation` does not.

**TypeScript test support**

- **A generated fake's child create refuses a missing or archived parent.** Seed a live parent into `RepositoryFakeStores` first. Your next regen reports every guarded create as breaking once, because the stored baseline never recorded the guard.

### Known issues

- **An event on an entity below the tenant through a `belongsTo` chain carries no tenant header (`clabonte/generator#3295`).** `validate` treats the entity as global, so requiring the header fails. Validation does not report the missing header, and a subscriber filtering on it misses these events. When this is fixed, those events must move to the tenant-required trait in that upgrade.
- **C#: `TestSeed.SeedSpareParents` can fail the whole seed on a relational provider (`#3366`),** and **some generated create tests answer 403 for a tenant-scoped role (`#3365`).** Both are unchanged from `0.24.0`. See `csharp-component-guide.md` §4.4.

### New in the spec

- **`x-sample: { provider: fixed, format: "null" }`** seeds null on a `nullable: true` scalar (C#, Dart, TypeScript).
- **C# list responses fill the `links` their envelope declares.** Additive on the wire.
- **C# controllers set `Location`, `API-Version` and `ETag`** where the spec declares them. A create with several reads that spell its key sets no `Location`.
- **`deletionState` works on reads below an aggregate root (C#).** It used to be ignored there.

The vendor-extension vocabulary is unchanged.

### Tracked issues

`clabonte/generator#3296` is fixed. `#3295` is open (FEAT-GEN-2026-0269), and `#3289` (the closed MCP `server` list) is planned for `0.26.0`.

### Kit changes

- `AsyncAPI_Handbook.md` adds `ASYNC_TENANT_HEADER_NOT_REQUIRED`, rewrites the tenant-gap warning, and corrects the legacy header window: the `0.24.0` notes said those names were written for that release only, and `0.25.0` still writes them.
- Handbooks and the C# component guide document the changes above.

## 0.24.0

**Status: published.** Pinned by kit `v0.32.0` (released 2026-11-02) (jar sha256 `85d1496e…c088902e7`; full record in [`compatibility.md`](https://github.com/specfuse/authoring/blob/main/compatibility.md) §60). Generator release date: 2026-11-02.
Expand Down
10 changes: 5 additions & 5 deletions generator.lock
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"kit_version": "0.32.0",
"kit_version": "0.33.0",
"min_java": 21,
"generator": {
"version": "0.24.0",
"asset": "specfuse-generator-0.24.0.jar",
"sha256": "85d1496ec1d58353379dd4ed93faec36829497b86e88da5c4b74f49c088902e7",
"version": "0.25.0",
"asset": "specfuse-generator-0.25.0.jar",
"sha256": "a8649a7062835134826aa49a460edfc8a3890a278e2b2923ccd0f78745df1cc9",
"release_repo": "Specfuse/generator-dist",
"release_tag": "v0.24.0"
"release_tag": "v0.25.0"
},
"_note": "Pin the kit<->generator contract here. On each generator release: set version/asset/sha256/release_tag, then publish a matching kit patch. sha256=PENDING means no generator is bundled yet; `specfuse authoring generate` will refuse with a clear message until a real jar is pinned."
}
7 changes: 5 additions & 2 deletions handbooks/API_Handbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ CustomerSearchRequest:
### 1.7 Paginated list wrapper
- **Schema name:** `{Resource}List` (e.g., `CustomerList`).
- **Data:** array of `Basic{Resource}` within the standard pagination envelope.
- **Required:** every envelope field except `links`. `hasPrev` and `hasNext` are required because clients page on them: the generated C# `PaginatedList` always serializes both, computed from `page` and `pageCount`, and the generated Dart client reads `hasNext` as the server's answer rather than guessing from the row count. A schema that leaves them optional lets a conformant server omit the one field a client needs to decide whether to render a "next" control. `links` stays optional.
- **Required:** every envelope field except `links`. `hasPrev` and `hasNext` are required because clients page on them: the generated C# `PaginatedList` always serializes both, computed from `page` and `pageCount`, and the generated Dart client reads `hasNext` as the server's answer rather than guessing from the row count. A schema that leaves them optional lets a conformant server omit the one field a client needs to decide whether to render a "next" control. `links` stays optional. From generator `0.25.0` a generated C# list response fills the `links` its envelope declares: relative URLs that keep the request's query and change only `page`, with `prev` and `next` null where no such page exists, and `last` null when the total is unknown.

```yaml
CustomerList:
Expand Down Expand Up @@ -1094,6 +1094,7 @@ GET /orders/123?deletionState=archived # 404 if 123 is live, so a caller can c
- `deletionState` is legal only on a read of an entity declaring `archiveVisibility: operational`. On a `restricted` entity `validate` fails with `ARCHIVE_DELETION_STATE_TARGET_NOT_OPERATIONAL`, and on a hard-delete entity with `ARCHIVE_DELETION_STATE_HARD_TARGET` (both ERROR). The operation must name its entity in `x-operation.target`.
- **`filter=deletedAt ne null` does not reach archived rows.** The `filter` runs inside the default query filter, which has already removed them, so it returns an empty page. `deletionState` is the only generated route to an archived row.
- `deletionState=all` or `archived` still does not return a row whose owning **ancestor** is archived. The generated read bypasses the filter for this entity's own archive state only.
- **Below an aggregate root, `deletionState` needs generator `0.25.0` (C#).** Before it, a generated service accepted the parameter on a child or related-aggregate read and ignored it, so `archived` returned live rows.
- Standard GET by ID (`GET /orders/123`) returns `404` for an archived row.

#### AI Agent Considerations
Expand Down Expand Up @@ -2187,7 +2188,7 @@ Sentinel name list to scan against (case-insensitive): `unknown`, `unspecified`,
| Provider | When to use | Required fields | Example |
|----------|-------------|-----------------|---------|
| `faker` | Realistic data — names, emails, addresses, dates, numbers | `path` (required), `locale` (optional) | `{ provider: faker, path: person.firstName, locale: en-US }` |
| `fixed` | Enum values, known constants, boolean defaults | `format` (required — the literal value) | `{ provider: fixed, format: "premium" }` |
| `fixed` | Enum values, known constants, boolean defaults; from generator `0.25.0`, `format: "null"` seeds null on a `nullable: true` scalar | `format` (required — the literal value) | `{ provider: fixed, format: "premium" }` |
| `template` | Compound/derived values — formatted codes, composed strings | `format` (required — template with `{faker.path}` interpolation) | `{ provider: template, format: "ORD-{{string.numeric(6)}}" }` |

**JSON Schema**: `schemas/arazzo-extensions/x-sample.schema.json`
Expand Down Expand Up @@ -2773,6 +2774,8 @@ The `type` each one sets is fixed: `validationError`, `insufficientPrivileges`,

If `Error` also declares an `automatedDecision` property naming a schema, the constructor takes an optional `automatedDecision` of that type as a third argument. `InternalException` and `UnauthorizedException` get no reason constructor. Through `0.17.0` the exceptions took only an `int errorCode` or a message, so a project built the `Error` itself and passed it to the `(Error error)` constructor. That still works.

**A domain exception is never wrapped, from `0.25.0` (C#).** Every generated service method and application-service gate lets all ten classes `domainExceptions` emits through unchanged, and wraps only anything else in `InternalException`. A group that writes its own exception classes instead of registering `domainExceptions` must declare all ten.

**`id` follows the schema from `0.18.0`.** The exceptions set `Error.Id` to `Guid.NewGuid().ToString()` for the kit's plain-string `id`, and to `Guid.NewGuid()` for a `format: uuid` one. Through `0.17.0` it was `<entry assembly>-<UTC ticks>`, which two errors raised in the same tick shared. Treat `id` as opaque: it identifies one error occurrence in logs and support requests, and its format is not a contract.

### Error Types and Common Reasons
Expand Down
2 changes: 1 addition & 1 deletion handbooks/Arazzo_Handbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,7 @@ tools:
source: { type: workflow, workflowId: get-order-status }
```

`Project_File.md` §1.5 covers the `mcpManifests` setting, what `validate` checks, and the 0.24.0 warning against registering `mcpHost` on a real manifest.
`Project_File.md` §1.5 covers the `mcpManifests` setting, what `validate` checks, and the warning against registering `mcpHost` on a real manifest.

---

Expand Down
Loading
Loading