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: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ jobs:
ConcurrencyDelegated ArchiveVisibilityOperational \
UniqueBySingleAndComposite UniqueByEmptyList \
ValueObjectProtectionScalar ValueObjectProtectionPerMember \
FieldSetsDeclared ValueObjectClassifiedUntrusted \
DeprecatedSchemaKey; do
if grep "specfuse-xentity-shape" "$REPORT" | grep -q "$good"; then
echo "::error::$good uses only documented x-entity keys but was flagged." >&2
Expand All @@ -220,6 +221,7 @@ jobs:
UniqueByNoProperties UniqueByEmptyProperties \
ValueObjectProtectionUnknownSubKey \
ValueObjectProtectionMemberNotObject \
FieldSetsEmpty FieldSetBadName FieldSetNotAList FieldSetNoEntries \
UniqueByUnknownMember UniqueByBadPropertyName; do
if ! grep "specfuse-xentity-shape" "$REPORT" | grep -q "$bad"; then
echo "::error::specfuse-xentity-shape did not flag $bad." >&2
Expand Down
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.31.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.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).

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
33 changes: 32 additions & 1 deletion compatibility.md

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions docs/csharp-component-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ The references are the Clean Architecture layering made compile-time: Domain ref

Two libraries are deliberately **not** on the list: AutoMapper and FluentAssertions. Both became commercially licensed; generated code maps through Mapperly and asserts through AwesomeAssertions (the Apache-2.0 fork with the same `.Should()` API). Referencing FluentAssertions beside AwesomeAssertions makes every assertion ambiguous.

Two more packages depend on what you register, from generator 0.24. Add `Cronos` to the generic-host project when it registers `hostedScheduledJob` for an `inProcess` job (a `cronJob`-only host does not need it). A host registering `mcpHost` references `ModelContextProtocol.AspNetCore` at 2.3.0, because concurrent first tool creation can build a wrong input schema on 2.1.0 and 2.2.0. Keep `mcpHost` off a real manifest at 0.24 ([`Project_File.md` §1.5](../handbooks/Project_File.md)).

### 2.3 Solution, SDK pin and git

- `HelloOrders.slnx` (or `.sln`) lists every project; `global.json` pins the SDK (`"version": "10.0.100", "rollForward": "feature"`).
Expand Down Expand Up @@ -422,6 +424,49 @@ A `FAILED` patch exits non-zero and stops the run, so a regen never finishes wit

**Retiring one.** When the issue is fixed and the generator the spec repo runs (the kit's pin, or the project's `.specfuse/generator.lock`) reaches that release, regenerate and expect `already present`. Confirm the output is right without the patch, delete the patch file, move its row to **Retired**, and remove its expectations entry and any `src/` workaround tied to it. Review the index at every pin bump: a patch whose issue is closed but that still reports `applied` means the fix did not cover your case, so comment on or reopen the issue.

### 9.2 Upgrading to generator 0.24

Every C# consumer of `eventRuntime` authors migrations at this regen, and some must be applied **before** the new code runs. [`generator-releases.md`](generator-releases.md) has the full account; this is the order of work in this repo.

**Migrations.** One regen can carry all of these in order:

| Migration | Who needs it | What to write |
|---|---|---|
| `EventOutbox.Status` holds names | Every consumer of `eventRuntime` | **Write it by hand**, see below. `dotnet ef migrations add` scaffolds only the `AlterColumn`, which leaves existing rows holding `'0'`. |
| `AggregateVersion` and `SessionKey` | Every consumer of `eventRuntime` | `dotnet ef migrations add` produces it: an `int` `AggregateVersion` (default 0) on each table of an entity that is an event's subject, and `EventOutbox.SessionKey` with its filtered index. It goes after the `Status` migration, because the index filter names the new status values. |
| `ScheduledJobRun` | Every C# consumer | `dotnet ef migrations add`. Without it every timer tick fails. |
| Outbox tenancy columns | Tenancy not named `CompanyId` / `RestaurantId` | A `RenameColumn` for a renamed tier, or a `DropColumn` for an undeclared sub-tenant. Check that column is `NULL` everywhere first if a Python producer writes into the table. |
| `dbColumn` renames | A `namingOverrides.dbColumn` whose result differs from the C# name, such as `snake_case` | `CreatedAt`, `UpdatedAt`, `ConcurrencyToken` and optional scalars take the override. **Check the migration says `RenameColumn`**: a drop and add loses the data, so edit it if EF chose that. |

```csharp
// EventOutbox.Status: ordinals to names. Run the UPDATE between the AlterColumn and the CreateIndex.
migrationBuilder.DropIndex(name: "IX_EventOutbox_Status_CreatedAt", table: "EventOutbox");
migrationBuilder.DropIndex(name: "IX_EventOutbox_Status_NextRetryAt", table: "EventOutbox");
migrationBuilder.AlterColumn<string>(name: "Status", table: "EventOutbox", type: "nvarchar(32)", maxLength: 32, nullable: false, oldClrType: typeof(int), oldType: "int");
migrationBuilder.Sql("UPDATE EventOutbox SET Status = CASE Status WHEN '0' THEN 'Pending' WHEN '1' THEN 'Sent' WHEN '2' THEN 'Failed' WHEN '3' THEN 'Poisoned' ELSE Status END");
migrationBuilder.CreateIndex(name: "IX_EventOutbox_Status_CreatedAt", table: "EventOutbox", columns: new[] { "Status", "CreatedAt" });
migrationBuilder.CreateIndex(name: "IX_EventOutbox_Status_NextRetryAt", table: "EventOutbox", columns: new[] { "Status", "NextRetryAt" });
```

Raw SQL or a dashboard reading `Status = 0` must change. Code using `OutboxStatus.Pending` does not.

**Deploy order, in one release:**

1. Apply the migrations. C# and Python from this release both fail against the old `Status` column. The old C# hosts cannot read the new one either, so their drainer fails until step 3; rows wait in the table and none is lost.
2. Add `ScheduleHeartbeat.LogCategory` to the log-floor exemption in `OpenTelemetryBuilderExtensions.cs`, or the heartbeat is dropped.
3. Deploy the C# hosts.
4. Deploy Python workers. A Python worker from before this release keeps working after the migration, for this release only.

**Hand-written code that stops compiling or behaves differently:**

- **CS7036 on a runtime function.** `EventOutboxDrainerFunction`, `EventOutboxRetentionFunction` and `EventInboxRetentionFunction` gain `ILogger<ScheduleHeartbeat> heartbeatLogger` and `IScheduledJobGuard guard` after `logger`. A test that constructs one directly passes both.
- **CS0535 or CS0115 on an application service.** A `filter`, `sort`, `page` or `pageSize` parameter on a non-queryable application GET now reaches `I{Tag}ApplicationService` as a trailing parameter with no default, such as `string? filter`. Add it to the interface method, or to the `…Core` override and any `…Authorize` override, and apply it there. Drop any workaround that read the value off `HttpContext`.
- **`DbUpdateConcurrencyException` where the last writer used to win.** On an entity that is an event's subject and is not `concurrency: optimistic`, `AggregateVersion` is the concurrency token, and a generated service answers `409`. A detached instance passed to `Update` must carry the version it read, and two concurrent child creates on such a parent can answer `409`. A hand-written `ExecuteUpdate` or raw SQL moves nothing, so move the version in that statement.
- **`InvalidOperationException` at `BufferedEventCollector.Add`.** A test or producer adding a tenant-scoped event with no tenant now fails there.
- **A wire reader looking for the tenant in the body.** A tenancy member the payload does not declare is no longer in the outbox body; read the envelope header.
- **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.

## 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
2 changes: 1 addition & 1 deletion docs/flows.html
Original file line number Diff line number Diff line change
Expand Up @@ -1612,7 +1612,7 @@ <h1>Specfuse Authoring Flows</h1>
{"label": "AsyncAPI structure", "script": "validate-async-structure.sh", "what": "The root asyncapi.yaml only $refs channels and operations; async-common and domain async folders are laid out as required."},
{"label": "AsyncAPI Spectral lint", "script": "validate-async-spectral.sh", "what": "Channel, message and operation shape: domains, addresses, names, categories, snapshot guardrails."},
{"label": "Arazzo structure", "script": "validate-arazzo.sh", "what": "14 checks: file placement and naming, operationId and event resolution, recipe chains (depth, cycles), step overlap, unique MCP tool names."},
{"label": "Arazzo Spectral lint", "script": "validate-arazzo-spectral.sh", "what": "Extension shapes: x-version, x-domain, x-actors, x-as, x-async, x-recipe, x-mcp."},
{"label": "Arazzo Spectral lint", "script": "validate-arazzo-spectral.sh", "what": "Extension shapes: x-version, x-domain, x-actors, x-as, x-async, x-recipe."},
{"label": "Generator validation (OpenAPI DDD + async cross-spec + source-tree)", "script": "validate-generator.sh", "what": "Bundles, checks source-tree rules the bundle erases, then validates the project file: the DDD ruleset and the ASYNC_* cross-spec rules. Runs last, as the cross-spec gate."}
],

Expand Down
66 changes: 65 additions & 1 deletion docs/generator-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,73 @@ 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.24.0

**Status: pending.** Pinned by kit `v0.32.0` (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.

### Do not use yet

**Do not register `mcpHost` or `mcpEvalHarness` on a real manifest at this pin.** They are verified on fixtures only. On a real manifest the generated tools fail to compile, some composite tools advertise required ids a caller cannot obtain, and **a preview tool commits instead of rolling back, so a preview performs the real action with no confirmation.** A group that does not register these artifacts is unaffected. `mcpServerResources`, `validate` and the `MCP_*` findings work as documented.

### Before you regenerate

**Spec authors**

- **`aiAccess` is required (ERROR).** An `x-entity` without it fails `validate` and `generate` with `ENTITY_AIACCESS_MISSING`. To grant nothing, add `aiAccess: { operations: [], reason: "Tier 0 — no AI access" }`. `ENTITY_FIELDSETS_AI_ACCESS_MISSING` no longer exists.
- **Event headers must bind the envelope (ERROR).** A message that declares headers and binds no `eventId`, or no tenant or sub-tenant on a scoped event, fails with `ASYNC_ENVELOPE_HEADER_MISSING`. A global event whose headers require the tenant fails with `ASYNC_TENANT_HEADER_ON_GLOBAL_EVENT`. Mark the headers with `x-envelope`, and split a shared trait into a tenant-scoped trait and a tenant-optional one. Neither may layer on the other.
- **Only `application/json` events (ERROR).** Any other resolved content type fails with `ASYNC_CONTENT_TYPE_UNSUPPORTED`. A message declaring none is unaffected.
- **`outboxDialect: postgres` is removed.** It fails the load, and the message names the removal. Remove the key.
- **Arazzo `x-mcp` is retired.** Each document declaring it gets the warning `X_MCP_RETIRED`, and nothing reads the block. Delete it. Tool exposure moves to the MCP manifest, and `safeForAutoInvoke` to `x-effects`.
- **`MCP_OPERATION_CLOSURE_NOT_READABLE` is an ERROR** for a project that sets `mcpManifests`.

**API clients (C#)**

- **`filter` accepts only what the spec lists.** That means `filterableProperties`, plus `Id`, `CreatedAt` and `UpdatedAt`. Searchable properties, foreign keys and properties named `status` are no longer implicitly filterable, and a clause on one answers 400. Add the property to `filterableProperties` to keep it.
- **Enum values match exactly.** A wrong-case value, or `unknown` on an enum that neither declares it nor is `x-extensible`, answers 400.
- **Writes outside the caller's visibility are refused.** In a project declaring `tenancy`, a write outside it answers 403, and a body reference outside it answers 404.

**C# component repos**

Author the EF migrations before you deploy. [`csharp-component-guide.md`](csharp-component-guide.md) has the steps and the order.

- **`AggregateVersion`** is a new column on every entity that is the subject of an event. On an entity without optimistic concurrency it is also the concurrency token, so hand-written writes can now see `DbUpdateConcurrencyException`, and two concurrent child creates can answer 409.
- **`EventOutbox.Status` holds names, not ordinals.** Write this migration by hand, because the scaffolded `AlterColumn` leaves existing rows holding `'0'`. Raw SQL and dashboards reading `Status = 0` must change.
- **`ScheduledJobRun` is a new table.** Without its migration, every timer tick fails.
- **Other migrations:** `EventOutbox` tenancy columns follow `tenancy` when your names are not `CompanyId` / `RestaurantId`. Under a `dbColumn` override, optional scalars, audit columns and the concurrency token are renamed: check that EF renames them rather than dropping and re-adding.
- **Compile breaks:** the runtime functions' constructors gain `heartbeatLogger` and `guard` (CS7036 in a test that builds one). `I{Tag}ApplicationService` gains `filter`, `sort`, `page` or `pageSize` on a non-queryable operation that declares them (CS0535, CS0115).
- **Events:** a tenancy member the payload does not declare leaves the outbox body, so read the envelope header. `BufferedEventCollector.Add` throws on a tenant-scoped body with no tenant. The legacy `companyId` / `restaurantId` / `producedAt` headers are written for this release only.

**Python components**

- A tenant-scoped `emit_*` requires `company_id`; omitting it is a `TypeError` and `None` is a `ValueError`. Grep your call sites.
- A Tier 0 aggregate no longer gets an AI repository. Delete the stale file if a hand-written module used it.
- `EventOutbox.Status` needs its migration first: Python from this release must not run before it is applied. The legacy header names are read for this release only.

### New in the spec

- **`x-entity.fieldSets`:** named subsets of an entity's properties, checked against `aiAccess`.
- **`x-classification: untrusted`:** states provenance, not protection. `x-protection.atRest` is still required beside it.
- **`x-envelope`:** binds an AsyncAPI message header to an envelope role.
- **`x-effects`:** an operation's reversibility, external effects and compensation.
- **`mcpManifests`:** MCP server manifests as a validated input. See the warning above before registering the host artifacts. `server` is a closed list at this pin: `admin`, `compliance`, `inbound`, `ops`, `scheduling`, `workforce`.
- **`workerPlacement.schedules` and `workerPlacement.runtime`:** place scheduled jobs and the event runtime on the generic host, through the new `hostedScheduledJob` and `eventRuntimeHosted` artifacts. A job placed `inProcess` needs the `Cronos` package.

### Tracked issues

FEAT-2026-0243 shipped, so [`Project_File.md`](../handbooks/Project_File.md) §17.2 point 4 (one database per pillar) now applies. MCP repair is open as FEAT-GEN-2026-0259.

### Kit changes

- The OpenAPI ruleset accepts `fieldSets` and `untrusted`. The Arazzo ruleset warns on `x-mcp` instead of checking its shape; `validate-arazzo.sh` drops its `toolName` uniqueness check.
- `hello-orders`, the samples and the scenario agents and skills no longer write `x-mcp`. The agents used to call it required and add it automatically; they now delete it.
- `AsyncAPI_Handbook.md` states where `x-trigger-when` gates emission: not on explicit or hand-written paths.
- `Vendor_Extensions.md` adds author guidance for `x-effects`, the rule for marking a field `untrusted`, and a warning that a client-writable reference to a global entity gets no tenant check.
- `check-extension-vocabulary.py` reads the jar's vocabulary correctly when a surface has no keyed entries.
- Handbooks and the C# component guide document the changes above.

## 0.23.0 (with 0.22.0)

**Status: pending.** Pinned by kit `v0.31.0` (jar sha256 `b906ed3f…5ffacc6`; full record in [`compatibility.md`](https://github.com/specfuse/authoring/blob/main/compatibility.md) §59). Generator release dates: 0.22.0 on 2026-10-28, 0.23.0 on 2026-10-29.
**Status: published.** Pinned by kit `v0.31.0` (jar sha256 `b906ed3f…5ffacc6`; full record in [`compatibility.md`](https://github.com/specfuse/authoring/blob/main/compatibility.md) §59). Generator release dates: 0.22.0 on 2026-10-28, 0.23.0 on 2026-10-29.

The kit goes from **0.21.0 to 0.23.0**; everything below covers both releases.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ x-doc:
Self-service order placement is the core happy path of the hello-orders
example domain.

x-mcp:
exposed: false

x-actors:
customer:
role: Customer
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.31.0",
"kit_version": "0.32.0",
"min_java": 21,
"generator": {
"version": "0.23.0",
"asset": "specfuse-generator-0.23.0.jar",
"sha256": "b906ed3f6bd8e6d657ff5e14d82cacb545ce46fc9674fb8d0d67551535ffacc6",
"version": "0.24.0",
"asset": "specfuse-generator-0.24.0.jar",
"sha256": "85d1496ec1d58353379dd4ed93faec36829497b86e88da5c4b74f49c088902e7",
"release_repo": "Specfuse/generator-dist",
"release_tag": "v0.23.0"
"release_tag": "v0.24.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."
}
Loading
Loading