diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b769b07..1bca83f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/README.md b/README.md index 3287dec..d5803a6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/compatibility.md b/compatibility.md index a28bd7f..fd127a6 100644 --- a/compatibility.md +++ b/compatibility.md @@ -58,7 +58,8 @@ This file tracks which Specfuse generator commits are known compatible with each | `v0.25.0` (incubating) | generator **`0.16.0`** — `Specfuse/generator-dist` release `v0.16.0`, asset `specfuse-generator-0.16.0.jar`, sha256 `dcd77128…7e8151` | **⚠ BREAKING for C# consumers with stored enums or encrypted enums, and for any C# project whose `service` emits events without `eventRuntime`.** *Minor: a generator pin bump that adds an authorable key (`x-ai.dataHandling`), four Spectral rules, and ERRORs that start failing specs which passed `0.15.0`.* Measured on the released jar against `0.15.0` (§52): `hello-orders` (fresh bundle) and a fresh `init` scaffold validate with finding lists identical to `0.15.0`'s; `hello-orders` generates 14 files whose only change is the namespace fix; `templates` is unchanged for all four languages; every new diagnostic code is in the CHANGELOG. **(1) `x-ai.dataHandling`** (#153): `personalData` + `providerRegion`, and the generated Python worker refuses to start when the configured provider region differs (`AsyncAPI_Handbook.md` §4.3). New kit rules `specfuse-async-ai-shape` (warn, closes `x-ai` over its ten documented keys), `specfuse-async-ai-data-handling-shape` (error) and `specfuse-async-ai-personal-data-region` (warn); fixture `ai-data-handling.yaml`. **(2) Breaking C#:** `PROJECT_EVENT_RUNTIME_REQUIRED` (`Project_File.md` §10); enum columns store the wire value and an encrypted enum is really encrypted, both needing data migrations (`Vendor_Extensions.md` §1.4.1, §1.5). **(3) New findings:** `ENTITY_MISSING_ID` (ERROR) / `ENTITY_MISSING_AUDIT_FIELD`; `ARAZZO_STEP_PAYLOAD_MISSING_REQUIRED` (ERROR) / `_UNDECLARED_PROPERTY`; `ASYNC_EMITS_NO_EMIT_OPERATION`, `ASYNC_WORKER_SELF_TRIGGER`, `FUNCTIONAL_TEST_TAG_UNGROUPED`; `TEST_GRANTS_GRANT_INVALID`, `TENANT_REMOTE_REFERENCE_FOREIGN_KEY_MISMATCH`, `INLINE_ENUM_SCHEMA` on an `allOf`-wrapped enum. **(4) Closed:** every §49 issue (`#2564`–`#2571`), §19, §44 item 8. **(5) Kit rules widened to match the jar:** `specfuse-xprotection-unavailable-properties-required` now covers an encrypted value object declared `storage: flatten` (the jar has required it since `0.14.0`); `specfuse-no-inline-enums` catches an `allOf`-wrapped inline enum. **(6) Ships** the business-retention docs and the `Error.code` note (§51, corrected in §52). **Spec-author action:** register `eventRuntime` or drop `x-emits` where `service` is generated; migrate enum columns and encrypted enums; give every entity an id; fix Arazzo step payloads; declare `dataHandling` on AI workers that touch personal data. | | `v0.26.0` (incubating) | generator **`0.17.0`** — `Specfuse/generator-dist` release `v0.17.0`, asset `specfuse-generator-0.17.0.jar`, sha256 `2f2026fb…9ad3` | **⚠ BREAKING for C# consumers that reference a double-pluralized worker name or a `Var`-prefixed navigation, and for clients that relied on an individual list parameter being ignored.** *Minor: a generator pin bump with consumer-breaking C# renames, a list-query behaviour change and one new ERROR path (a nested list across pillars). No kit Spectral rule changes.* Measured on the released jar against `0.16.0` (§53): `hello-orders` (fresh bundle) validates with a JSON report byte-identical to `0.16.0`'s and generates 14 byte-identical files; a fresh `init` scaffold validates with the same five finding codes; `templates` is unchanged and `extensions` differs only in the order of one value set; `check-extension-vocabulary.py --require-jar` passes. **(1) Breaking C#:** worker registration files and classes are pluralized once (`Peoples` → `People`, `CompanyConsumerRegistration.g.cs` → `CompaniesConsumerRegistration.g.cs`, `#2646`); a navigation named after its target type keeps that name (`VarClient` → `Client`, `#2642`). Hand-written code referencing the old names must be renamed; no schema change. **(2) Individual list parameters bind** in the generated C# list (`#2648`): key, filterable, enum, searchable, range and repeated values each narrow the list (`API_Handbook.md` §10.3). An enum or value-object array parameter is not bound and is reported `OPERATION_LIST_PARAMETER_NOT_BOUND` (WARNING). **(3) Pillars** (`Vendor_Extensions.md` §14.6, §14.10): a filtered C# group with `event` + `service` now builds (`#2639`, closes §44 item 9); a nested list under another pillar's parent fails `validate` with `OPERATION_NESTED_LIST_NO_RELATIONSHIP` (`#2652`); `PROJECT_EVENT_RUNTIME_REQUIRED` counts only the operations a group's scope keeps (`#2651`). **(4) New finding:** `ENTITY_TAG_UNDECLARED_PUBLISHED` (WARNING), a safe read whose C# controller sends an `ETag` its response does not declare (`#2641`, `Vendor_Extensions.md` ETag table). **(5) Fixed without a kit change:** `OPERATION_NON_SEARCHABLE_QUERY_PARAM` checks a `Basic{Entity}` list item against its own entity (`#2665`); Dart and TypeScript test builders seed a declared date (`#2594`, `#2638`); C# timer triggers record a `ScheduleHeartbeat` event (`#2666`). **(6) Ships** the corrections of authoring#157. **Spec-author action:** rename consumer references to the C# names in (1); review list operations that declare individual parameters, since they now filter; declare `ETag` on reads `ENTITY_TAG_UNDECLARED_PUBLISHED` names; flatten any list nested under another pillar's parent. | | `v0.27.0` (incubating) | generator **`0.18.0`** — `Specfuse/generator-dist` release `v0.18.0`, asset `specfuse-generator-0.18.0.jar`, sha256 `b8ef150e…5e3b1ae` | **⚠ BREAKING for any project scaffolded by kit `≤ 0.26.0` that adopts the new `Error` shape, for clients that branch on `Error.code` as a rule name, and for a spec that nests a write route under another pillar's parent.** *Minor: a generator pin bump that adds C# exception constructors, turns one generate-time failure into a `validate` ERROR and adds three findings, shipped with the kit's `Error` shape change.* Measured on the released jar against `0.17.0` (§55): `hello-orders` (fresh bundle) validates with a JSON report byte-identical to `0.17.0`'s and generates 15 files, identical but for the timestamp in `Domain.generated-paths.json`; a fresh `init` scaffold validates with the same finding codes; `templates` is unchanged and `extensions` differs only in the order of one value set; `check-extension-vocabulary.py --require-jar` passes; the §14.10 pillar matrix gives the same verdict on every row. **(1) The kit's `Error`** carries the HTTP status in an integer `code` and the rule name in `reason`, drops `status`, makes `id` a plain string and adds `type` with an `ErrorType` enum (§54, authoring#159). Breaking on the wire for a client deployed against the old shape; §54 has the migration. A new prose rule, `specfuse-error-rule-name-in-code` (warn), lists the prose still writing a rule name into `code` (authoring#163). **(2) Reason constructors** (`clabonte/generator#2671`): the six exceptions a rule refuses with take `(string reason, string message)` and set `Code`, `Type` and `Reason`; `Error.Id` follows the schema (`API_Handbook.md` §11). Closes §54's generator follow-up. CI's error-shape probe now calls all six. **(3) Pillars** (`Vendor_Extensions.md` §14.3, §14.10, `Project_File.md` §8.13.2): a write route nested under another pillar's parent fails `validate` with `OPERATION_NESTED_CROSS_PILLAR` (ERROR, `#2680`), where it used to pass and fail `generate`; `PROJECT_HELD_ENTITY_MISSING` (warn, `#2682`) and `PROJECT_CONFIGURATION_CONVERTERS_MISSING` (warn, `#2673`) report a group that would not build; a Domain group with nothing to register builds (`#2678`); a value object or enum no entity reaches follows its own `x-domain` (`#2679`). **(4) Tenancy:** a marker named `ITenantScoped` or `ISubTenantScoped` is refused at load, `PROJECT_TENANCY_MARKER_RESERVED` (`#2672`, `Project_File.md` §4.1); closes §53 item 1. **(5) Fixed without a kit change:** a schema named after an event-runtime or implicitly imported .NET type compiles (FEAT-2026-0226); snapshot doc comments cite `Change{T}` (`#2701`); the Markdown channel topology and event catalog place publishers on the producing side and list every message a multi-message worker consumes (`#2677`, `#2704`). **(6) Ideation:** the dossier records `started:` with `graduated_to` (authoring#161). **Spec-author action:** adopt §54's `Error` shape and throw rule refusals with the reason constructors; move any write operation nested under another pillar's parent to the entity's own path; register `heldEntity` in each pillar that holds, and `heldEntityConfiguration` beside `infrastructureProject`; rename a tenancy marker called `ITenantScoped`. | -| `v0.31.0` (incubating, current) | generator **`0.23.0`** — `Specfuse/generator-dist` release `v0.23.0`, asset `specfuse-generator-0.23.0.jar`, sha256 `b906ed3f…5ffacc6` | **⚠ BREAKING for C# projects whose `name` differs from their groups' root, for two C# groups emitting the same layer without `pillar`, for kiosk apps that relied on path-level admission, and for hand-written `[ScopesRequired]` without `OperationId`.** *Minor: a two-release pin bump (`0.22.0` and `0.23.0`, both published 2026-10-28/29), plus the project generator pin (#169), Markdown groups read from the project file (#191) and several-pillar worker guidance (#192).* Measured on the released jar against `0.21.0` (§59): `hello-orders` (fresh bundle) validates with identical finding codes (19 warnings, 0 errors); `extensions --format json` is unchanged; `templates` adds `EventActorScope` and four authorization templates (`IOperationAuthorizer`, `OperationAuthorizer`, `OperationInvocation`, `OperationAuthorizationCatalog`); `check-extension-vocabulary.py --require-jar` passes with output identical to `0.21.0`. **(1) Breaking:** per-group `basePackage` namespaces and `AMBIGUOUS_LAYER_OWNER` (`Project_File.md` §8.5, §9); kiosk-session admission per operation (`Vendor_Extensions.md` §3.3, security fix); `[ScopesRequired]` must carry a catalogued `OperationId` (`Project_File.md` §16); generated service tests assert event contents and sets; `TestSeed.SeedSpareParents` consumer step. **(2) New:** named `genericHost` pools (`Project_File.md` §17.1, already documented in #193); `IOperationAuthorizer`; `ARAZZO_ACTOR_LACKS_SCOPE` exempts 403 denial steps; nested `emit.expect` maps and `ARAZZO_MATCH_EXPRESSION_UNBOUND` (`Arazzo_Handbook.md`); `EventEnvelope.UserId` filled via `EventActorScope`. **(3) Kit:** `specfuse authoring generate` honours a project `.specfuse/generator.lock` and `SPECFUSE_GENERATOR_JAR` (#175); `generate-scenario-docs.sh` runs the project file's Markdown groups (#194); C# guide §4.4 documents the new consumer steps. Release notes: `docs/generator-releases.md`. **Spec-author action:** flag every operation a kiosk app calls; give same-layer C# groups distinct `pillar`s; C# components call `TestSeed.SeedSpareParents(context)` in `EnsureFakeData()`, set `OperationId` on hand-written `[ScopesRequired]`, and fix `using .` lines if `name` differs from the groups' root. | +| `v0.32.0` (incubating, current) | generator **`0.24.0`** — `Specfuse/generator-dist` release `v0.24.0`, asset `specfuse-generator-0.24.0.jar`, sha256 `85d1496e…c088902e7` | **⚠ BREAKING for every C# consumer of `eventRuntime` (EF migrations), for a spec with an `x-entity` lacking `aiAccess`, for AsyncAPI messages whose headers bind no `eventId` or tenant, for clients filtering on a property not in `filterableProperties` or sending an enum in another case, and for `outboxDialect: postgres`.** *Minor.* **⚠ Do not register `mcpHost` or `mcpEvalHarness` on a real manifest at this pin: a preview tool commits instead of rolling back.** Measured on the released jar against `0.23.0` (§60): `hello-orders` and a fresh scaffold validate with 0 errors; the `add-component csharp` seed builds on regenerated output. **Kit:** `fieldSets` and `untrusted` admitted by the rulesets; Arazzo `x-mcp` shape rules replaced by one warning; `check-extension-vocabulary.py` no longer falls back to its heuristic on the new `mcp-manifest` surface. Release notes: `docs/generator-releases.md`. | +| `v0.31.0` (incubating) | generator **`0.23.0`** — `Specfuse/generator-dist` release `v0.23.0`, asset `specfuse-generator-0.23.0.jar`, sha256 `b906ed3f…5ffacc6` | **⚠ BREAKING for C# projects whose `name` differs from their groups' root, for two C# groups emitting the same layer without `pillar`, for kiosk apps that relied on path-level admission, and for hand-written `[ScopesRequired]` without `OperationId`.** *Minor: a two-release pin bump (`0.22.0` and `0.23.0`, both published 2026-10-28/29), plus the project generator pin (#169), Markdown groups read from the project file (#191) and several-pillar worker guidance (#192).* Measured on the released jar against `0.21.0` (§59): `hello-orders` (fresh bundle) validates with identical finding codes (19 warnings, 0 errors); `extensions --format json` is unchanged; `templates` adds `EventActorScope` and four authorization templates (`IOperationAuthorizer`, `OperationAuthorizer`, `OperationInvocation`, `OperationAuthorizationCatalog`); `check-extension-vocabulary.py --require-jar` passes with output identical to `0.21.0`. **(1) Breaking:** per-group `basePackage` namespaces and `AMBIGUOUS_LAYER_OWNER` (`Project_File.md` §8.5, §9); kiosk-session admission per operation (`Vendor_Extensions.md` §3.3, security fix); `[ScopesRequired]` must carry a catalogued `OperationId` (`Project_File.md` §16); generated service tests assert event contents and sets; `TestSeed.SeedSpareParents` consumer step. **(2) New:** named `genericHost` pools (`Project_File.md` §17.1, already documented in #193); `IOperationAuthorizer`; `ARAZZO_ACTOR_LACKS_SCOPE` exempts 403 denial steps; nested `emit.expect` maps and `ARAZZO_MATCH_EXPRESSION_UNBOUND` (`Arazzo_Handbook.md`); `EventEnvelope.UserId` filled via `EventActorScope`. **(3) Kit:** `specfuse authoring generate` honours a project `.specfuse/generator.lock` and `SPECFUSE_GENERATOR_JAR` (#175); `generate-scenario-docs.sh` runs the project file's Markdown groups (#194); C# guide §4.4 documents the new consumer steps. Release notes: `docs/generator-releases.md`. **Spec-author action:** flag every operation a kiosk app calls; give same-layer C# groups distinct `pillar`s; C# components call `TestSeed.SeedSpareParents(context)` in `EnsureFakeData()`, set `OperationId` on hand-written `[ScopesRequired]`, and fix `using .` lines if `name` differs from the groups' root. | | `v0.30.0` (incubating) | generator **`0.21.0`** — unchanged; `Specfuse/generator-dist` release `v0.21.0`, asset `specfuse-generator-0.21.0.jar`, sha256 `a78b5ea7…27f08a94` | **Adopter docs ship with the kit.** *Minor, not patch: the overlay delivers new files into every project.* **No pin change.** `init` and `upgrade` now copy into `.specfuse/authoring/docs/`, beside `flows.html`: `component-repos.md`, `add-component.md`, `csharp-component-guide.md` with its interactive map `csharp-component.html`, and `generator-releases.md`; and the starter generated-code contract to `.specfuse/authoring/templates/component-contract/`. A component repo checked out beside its spec repo reads the guide and seeds the contract from there, and the seeded `CLAUDE.md` of a new component points its agent at the guide. Links from those docs to files the overlay does not carry (`compatibility.md`, `getting-started.md`, `generator-access.md`, `examples/`) are now GitHub URLs, held by the new CI check `scripts/check-shipped-docs.py`. The `0.21.0` entry in `generator-releases.md` is marked published. **Also fixed:** `upgrade` counted every changed kit file as "locally-modified or unowned" in its summary, so an ordinary upgrade reported overwrites with no warning to explain them; only files it actually warns about are counted now. **Spec-author action:** none; run `specfuse authoring upgrade` to receive the docs. | | `v0.29.0` (incubating) | generator **`0.21.0`** — `Specfuse/generator-dist` release `v0.21.0`, asset `specfuse-generator-0.21.0.jar`, sha256 `a78b5ea7…27f08a94` | **⚠ BREAKING for a spec whose operation emits another entity's event, for C# components of a project that declares `tenancy`, and for event handlers still overriding `HandleCoreAsync`.** *Minor: a two-release pin bump (`0.20.0` was never published to `generator-dist`, so it ships here), with breaking validation and runtime changes, four new spec surfaces and the end of the release hold on `clabonte/generator#2882`–`#2884`.* Measured on the released jar against `0.19.0` (§58): `hello-orders` (fresh bundle) validates with identical finding codes and generates the same files; a fresh `init` scaffold validates with the same 19 codes; `extensions` adds only `x-entity.visibility`; `templates --language csharp` adds `hostedTopicConsumer`, `tenantIsolationTest`, `bareIgnoreSourceScanTest` and 32 templates; `check-extension-vocabulary.py --require-jar` passes once `specfuse-xprotection-shape` accepts `unmask`. **(1) Breaking:** `OPERATION_EMITS_FOREIGN_ENTITY` (ERROR, `API_Handbook.md`); `TRIGGER_REQUIRES_EVENT_RUNTIME_IMPL` (load failure, `Project_File.md`); `ASYNC_SUBSCRIPTION_FILTER_TOO_LONG` (ERROR on a `filterOverride`, `AsyncAPI_Handbook.md`); `HandleAsync` abstract and `HandleCoreAsync` removed; C# projects with `tenancy` get a generated `Tenant` query filter and must register an `IRecordVisibilityResolver`; hidden rows answer 404, not 403; Python AI workers need `primary_model`. **(2) New spec surfaces** (`Vendor_Extensions.md`): `uniqueBy` `where` / `nullsDistinct`; `x-protection.unmask` and `x-unmask` on PUT/PATCH (#182); `x-entity.visibility`. `specfuse-xentity-shape` and `specfuse-xprotection-shape` widened to match, fixture `uniqueby-visibility-unmask.yaml`. **(3) Project file** (`Project_File.md` §4.1, §17, §18): `workerPlacement`, `tenancy.enforcement: route`, `functionalTests.emitAssertions`, `generateUserHelper`; `tenancy` no longer required for services or the event runtime. **(4) Kit:** `add-component` drops both refusals and derives Python roots as the generator now does, and its C# seed now configures JWT bearer authentication: since `0.20.0` registers the role filters itself, a seed with no scheme answered every role-gated request with a 500 (caught by `example-regen` on this bump). The C# guide documents both and the `Tenant` filter. Release notes: `docs/generator-releases.md`. **Spec-author action:** move a foreign `x-emits` entry to the owning entity's operation; register `eventRuntimeImpl` beside any topic trigger; C# components with `tenancy` register an `IRecordVisibilityResolver` (or set `tenancy.enforcement: route` while migrating); replace `HandleCoreAsync` overrides; set `_PRIMARY_MODEL` for Python AI workers. | | `v0.28.0` (incubating) | generator **`0.19.0`** — `Specfuse/generator-dist` release `v0.19.0`, asset `specfuse-generator-0.19.0.jar`, sha256 `110f49f3…fe024f83` | **⚠ BREAKING for a project file still declaring a legacy artifact id, for a spec with a required property no create fills, and for clients that display an encrypted date.** *Minor: a generator pin bump with three breaking validation changes, three new spec surfaces and two new kit Spectral rules.* Measured on the released jar against `0.18.0` (§57): `hello-orders` (fresh bundle) validates with the same finding codes and generates 17 files, two more than before (`DateMask.g.cs` and a regen `CHANGELOG.md`); a fresh `init` scaffold validates with the same codes; `extensions` adds only the top-level `media-type` key; `check-extension-vocabulary.py --require-jar` passes. **(1) Breaking:** the fifteen artifact ids deprecated in `0.11.0` now fail the load (`Project_File.md` §11.4.1). `CREATE_REQUIRED_PROPERTY_UNPOPULATED` is an ERROR (`API_Handbook.md` §1.9). `CROSS_SPEC_VALIDATION_SKIPPED` is an ERROR when nothing else failed. An encrypted `format: date` with no `masking` now reads back as `0004-01-01`. On a C# mutable value object, `==` now compares by value. **(2) Date masking** (`Vendor_Extensions.md` §1.5, `API_Handbook.md` §1.4.1): `masking.reveal`, `maskedProperties`, and keeping a masked date that a PUT or PATCH sends back. New kit rule `specfuse-xprotection-masking-target` (error), and `specfuse-xprotection-shape` accepts `reveal`. **(3) `x-populate-from`** (`Vendor_Extensions.md` §1.11): a required field filled from the caller or the clock at create time. **(4) `x-stream`** (`API_Handbook.md` §1.10, `Vendor_Extensions.md` §5.2): streamed responses are declared and validated, not yet generated. New kit rule `specfuse-xstream-declaration` (error). **(5) Also:** typed `HandleAsync` worker methods (`HandleCoreAsync` deprecated); `ARAZZO_ACTOR_ROLE_NOT_PERMITTED` (WARNING); `FLATTEN_NESTING_TOO_DEEP` (ERROR); `ENTITY_MISSING_AUDIT_FIELD` no longer fires on a non-mutable entity; an unknown `x-protection` sub-key no longer aborts the other validation passes. **(6) Ships** PR #166 (§56). **Spec-author action:** rename legacy artifact ids; give every required create-time field a route (`Vendor_Extensions.md` §1.11); add `maskedProperties` where `MASKED_PROPERTIES_NOT_DECLARED` warns; declare `reveal` (and `nullable: true`) on encrypted dates that clients display; move `HandleCoreAsync` overrides into the typed methods. | @@ -1639,6 +1640,36 @@ This bump processes the consumer's `uniqueby-nulls-distinct-where.md` and `unmas **Not measured:** a C# build of a project whose `name` differs from its groups' root, a two-pillar project, and a kiosk-session runtime; these rest on the generator CHANGELOG. Several-pillar point 4 (one database per pillar) and `workerPlacement.schedules` / `runtime` wait on FEAT-2026-0243, which is not in `0.23.0`. +### 60. Generator `0.24.0` pin bump (kit `0.32.0`) + +**How it was measured.** On the released jar (`v0.24.0`, sha256 `85d1496ec1d58353379dd4ed93faec36829497b86e88da5c4b74f49c088902e7`, downloaded from `Specfuse/generator-dist` and checked against its `.sha256` asset), with `0.23.0` (`b906ed3f…5ffacc6`) as the baseline. + +- `hello-orders`, freshly bundled, before its `x-mcp` block was deleted: `validate` passes on both jars, 0 errors; 19 warnings on `0.23.0`, 20 on `0.24.0`. The one new finding is `X_MCP_RETIRED`. After deleting the block: 19 warnings, and `generate` exits 0 with 18 files. +- Fresh scaffold: `validate` passes on both jars, 0 errors, 2 warnings, identical findings. +- `add-component csharp` seed on `hello-orders`, regenerated on `0.24.0`: `dotnet build` succeeds, 0 errors, 229 generated files. The seed's package list needed no change. `hello-orders` declares no `tenancy`, so a tenancy-scoped build is not covered by this. +- `extensions --format json` adds `x-entity.fieldSets`, `valueObjects.classification = untrusted`, published members of `uniqueBy`, and the new `operation` (`x-effects`), `property` (`x-classification`, `x-envelope`) and `mcp-manifest` surfaces. Nothing removed. +- `templates` adds the artifacts `mcpServerResources`, `mcpHostConformanceTest`, `hostedScheduledJob`, `eventRuntimeHosted` and `mcpHost`. +- `templates --format json` is not valid JSON on `0.23.0`, so that diff used the text form. + +**A bug this bump found in the kit's gate.** `check-extension-vocabulary.py` returned no published vocabulary when any surface had no keyed entries, and `mcp-manifest` has none. Every surface then dropped to the constant-pool scan and the report said the jar "predates `extensions`". The scan read the retired `encryptedProperties` as a blocked key. A surface with no keyed entries is now skipped on its own. + +**Spectral.** +- `specfuse-xentity-shape` admits `fieldSets`, with the shape the jar also rejects. Fixture cases in both directions. +- `untrusted` joins `specfuse-classification-values` and `valueObjects.classification`. +- `specfuse-arazzo-mcp-exposed-required`, `-fields-when-exposed` and `-toolname-format` are removed, replaced by the warning `specfuse-arazzo-x-mcp-retired`. They could fail lint on a block the jar no longer reads. Their `rm-*` ids move to `retired` in `rule-renames.yaml`. +- `arazzoRecipeScenarioExclusion` and the combined Arazzo schema no longer forbid `x-mcp` on a recipe, since the jar only warns. `validate-arazzo.sh` drops check F14. + +**Still reported by the gate, not new:** `never_persist` accepted at `valueObjects.protection.atRest`; `cascadeDelete`, `children` and `requiresPagination` accepted by the ruleset and unread by the generator. + +**Not measured:** any runtime behaviour, including EF migrations, `AggregateVersion` ordering, the job lock, filter and enum refusals; MCP artifacts on a manifest; a project declaring `tenancy` or a `dbColumn` override; Python output. These rest on the generator CHANGELOG. + +**Measured with probe specs on the `0.24.0` jar.** `ENTITY_AIACCESS_MISSING` is an ERROR. `outboxDialect: postgres` fails the load, with a message naming the removal. `fieldSets` shape and readability codes fire, and a `pii` entry passes only once listed in an explicit `readableProperties`. `untrusted` needs `atRest`, and an `email` classified only `[untrusted]` is still reported. `ENTITY_AGGREGATE_VERSION_RESERVED`, six `X_EFFECTS_*` codes, `ASYNC_ENVELOPE_ROLE_UNKNOWN` / `_DUPLICATE`, `ASYNC_ENVELOPE_HEADER_MISSING`, `ASYNC_TENANT_HEADER_ON_GLOBAL_EVENT` and `ASYNC_CONTENT_TYPE_UNSUPPORTED` all fire. A malformed `x-mcp` passes `validate`. Filter narrowing shows in the generated `[Filterable]` set (`Customer` 5 to 0, `Order` 3 to 2). In the jar's templates, `x-trigger-when` compiles into the C# update, replace and related-entity update paths and the Python emitter, and not into the explicit context-bearing method. + +**Where the jar and the notes differ.** The printed postgres failure carries no `PROJECT_BROKER_DIALECT_UNKNOWN` code, and a misspelled `runtime.host` prints no `UNKNOWN_WORKER_HOST`. The MCP `server` value is a closed list of six names, which looks like one consumer's vocabulary in the jar; it should be raised upstream. + +**Handoffs adopted in this release:** the MCP vocabulary and manifest, `x-effects`, `x-mcp` retirement and `x-trigger-when` enforcement. A client-writable reference to a global entity is documented as a known gap; the generator-side diagnostic and opt-in check stay open. A Spectral rule with a file `$ref` inside `functionOptions.schema` validates nothing, as reported from Spectral 6.16.2; no kit rule uses one. + + ## Outstanding kit-side work These are gaps in the kit itself, separate from the generator-side follow-ups above. Each represents an enforcement promise the handbooks make that has no implementation today. diff --git a/docs/csharp-component-guide.md b/docs/csharp-component-guide.md index 0b2f9da..23c59fd 100644 --- a/docs/csharp-component-guide.md +++ b/docs/csharp-component-guide.md @@ -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"`). @@ -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(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 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. diff --git a/docs/flows.html b/docs/flows.html index aac8d27..cc0402e 100644 --- a/docs/flows.html +++ b/docs/flows.html @@ -1612,7 +1612,7 @@

Specfuse Authoring Flows

{"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."} ], diff --git a/docs/generator-releases.md b/docs/generator-releases.md index c77d4f6..c2ed412 100644 --- a/docs/generator-releases.md +++ b/docs/generator-releases.md @@ -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. diff --git a/examples/hello-orders/api/specs/v1/scenarios/cross-domain/place-order.arazzo.yaml b/examples/hello-orders/api/specs/v1/scenarios/cross-domain/place-order.arazzo.yaml index dc6bc57..783229d 100644 --- a/examples/hello-orders/api/specs/v1/scenarios/cross-domain/place-order.arazzo.yaml +++ b/examples/hello-orders/api/specs/v1/scenarios/cross-domain/place-order.arazzo.yaml @@ -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 diff --git a/generator.lock b/generator.lock index 26815bb..4b7ca66 100644 --- a/generator.lock +++ b/generator.lock @@ -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." } diff --git a/handbooks/AI_Access_Policy_Framework.md b/handbooks/AI_Access_Policy_Framework.md index d9278f0..b53b1ba 100644 --- a/handbooks/AI_Access_Policy_Framework.md +++ b/handbooks/AI_Access_Policy_Framework.md @@ -60,7 +60,7 @@ aiAccess: ### 2.3 Tier 2 — Collaborative (`ownedBy: shared`) -Both AI and humans may write. Conflicts are resolved last-writer-wins with audit trail. +Both AI and humans may write. Conflicts are resolved last-writer-wins with audit trail, except on an entity that is the subject of an event: from generator `0.24.0` (C#) a generated write on such an entity without `concurrency: optimistic` answers a lost race with `409` (`Vendor_Extensions.md` §1.1.1). ```yaml aiAccess: @@ -332,7 +332,7 @@ The flowchart is biased toward restriction by design. | `x-entity.encryptedProperties` | **Retired, generator 0.7.0** — fails with `ENTITY_INVALID_CONFIG`. Held the exclusion role through 0.6.0. | | `x-classification: [encrypted]` | **Retired, generator 0.7.0** — rejected with `INVALID_EXTENSION_VALUE`, and by kit `specfuse-classification-encrypted-superseded` (error). | | `x-classification: [pii \| sensitive]` | PII/sensitive fields are not auto-excluded from AI read, but they trigger snapshot acknowledgement rules (`x-snapshot-pii-acknowledged`) when included in event snapshots. See `AsyncAPI_Handbook.md §2.3`. | -| `x-ai-safe` (operations) | **Read by nothing — do not gate on it.** It was intended to operate at the HTTP operation level (may an AI agent invoke this endpoint without approval) where `aiAccess` operates at the entity/repository level, but no generated code and no lint rule enforces it. `aiAccess` is enforced; `x-ai-safe` is documentation. The live per-tool control is `x-mcp.safeForAutoInvoke` on an Arazzo scenario (`Arazzo_Handbook.md` §4.8). See `Vendor_Extensions.md` §4.1 and `compatibility.md` §25. | +| `x-ai-safe` (operations) | **Read by nothing — do not gate on it.** It was intended to operate at the HTTP operation level (may an AI agent invoke this endpoint without approval) where `aiAccess` operates at the entity/repository level, but no generated code and no lint rule enforces it. `aiAccess` is enforced; `x-ai-safe` is documentation. The per-operation control is `x-effects`, from which the generator derives whether a call needs confirmation (`Vendor_Extensions.md` §4.7); `x-mcp.safeForAutoInvoke` is retired from generator `0.24.0`. See `Vendor_Extensions.md` §4.1 and `compatibility.md` §25. | | `x-ai.entities` (AsyncAPI workers) | When an AsyncAPI worker declares `x-ai.entities.{reads,creates,updates,deletes}`, every listed entity MUST have a matching `aiAccess` block granting the corresponding operation. The cross-spec validator enforces this. See `AsyncAPI_Handbook.md §4.3`. | | `x-ai.dataHandling` (AsyncAPI workers) | Declares whether the worker sends personal data to its LLM provider (`personalData`) and the country that provider must run in (`providerRegion`). Generator `0.16.0`: the generated Python worker refuses to start when the configured provider region differs. It checks configuration, not where the endpoint actually runs. Complementary to `aiAccess`: `aiAccess` decides which data an agent may touch, `dataHandling` where that data may go. See `AsyncAPI_Handbook.md §4.3`. | | `filterableProperties` / `searchableProperties` | Govern HTTP query surfaces, not AI repository methods. AI filtering/search allow-lists are derived separately from `readableProperties`. | diff --git a/handbooks/API_Handbook.md b/handbooks/API_Handbook.md index fb1dc64..8a99b93 100644 --- a/handbooks/API_Handbook.md +++ b/handbooks/API_Handbook.md @@ -1511,11 +1511,11 @@ All write operations (POST/PUT/PATCH/DELETE) must support the `validateOnly` que - Assistant may infer cardinality from `hasOne` / `hasMany` / `belongsTo`, but asks if in doubt. - One-sided declarations are allowed; assistants warn on likely mismatches. - `filterableProperties` / `searchableProperties` must reference existing **top-level** fields (no nested paths, except for BilingualText subfields like `title.en`). -- **CRITICAL (filterableProperties)**: All fields usable in OData `filter` expressions MUST be declared in `filterableProperties` (except generic parameters: page, pageSize, sort, search, filter, expand, validateOnly). +- **CRITICAL (filterableProperties)**: All fields usable in OData `filter` expressions MUST be declared in `filterableProperties` (except generic parameters: page, pageSize, sort, search, filter, expand, validateOnly). From generator `0.24.0` the generated C# enforces this: `filter` accepts only those properties plus `Id`, `CreatedAt` and `UpdatedAt`, and a clause on anything else answers `400`. - **CRITICAL (searchableProperties)**: All fields included in free-text `search` MUST be declared in `searchableProperties`. For BilingualText fields, reference subfields (e.g., `title.en`, `title.fr`). - Encryption is declared **per property**, not in `x-entity`: `x-protection: {atRest: encrypted}` on the property schema, with `x-protection.masking: {first, last}` for how many characters survive masking on reads. `x-entity.encryptedProperties` was retired in generator 0.7.0 and fails with `ENTITY_INVALID_CONFIG`. See `Vendor_Extensions.md` §1.5. - Writes accept plaintext. From generator `0.14.0` a C# API group registering `apiMapper` returns every masked field masked, on every response; an `atRest: encrypted` string with no declared `masking` reads back as `****` plus its last four characters, and, from `0.19.0`, an encrypted `format: date` with no `masking` reads back as `0004-01-01`. A read returns a field in the clear only through a declared `x-unmask` query parameter on the single-resource GET, behind a deny-by-default policy the host implements. See `Vendor_Extensions.md` §1.5, "Masking on the wire, and `x-unmask`", and §1.4.1 above for what a PUT does with a field it read masked. -- `aiAccess` (required on every `x-entity`): declares the AI agent access policy enforced by generated repositories. Absence triggers a validator WARN (`ENTITY_AIACCESS_MISSING`); for entities the AI must not touch, use the canonical Tier 0 form `operations: []` with `reason`. `operations` lists allowed verbs from `[read, create, update, delete]`; an empty array is the Tier 0 declaration. Write verbs require `writableProperties` and a `reason`. Fields declaring `x-protection: {atRest: encrypted}` are excluded from implicit read access and must be listed explicitly in `readableProperties` to be AI-readable. Full schema and examples: see `Vendor_Extensions.md` §1.1.1. +- `aiAccess` (required on every `x-entity`): declares the AI agent access policy enforced by generated repositories. Absence is a validator ERROR from generator `0.24.0` (`ENTITY_AIACCESS_MISSING`), so `validate` and `generate` fail; for entities the AI must not touch, use the canonical Tier 0 form `operations: []` with `reason`. `operations` lists allowed verbs from `[read, create, update, delete]`; an empty array is the Tier 0 declaration. Write verbs require `writableProperties` and a `reason`. Fields declaring `x-protection: {atRest: encrypted}` are excluded from implicit read access and must be listed explicitly in `readableProperties` to be AI-readable. Full schema and examples: see `Vendor_Extensions.md` §1.1.1. --- @@ -2014,6 +2014,8 @@ genericParameters: #### **filterableProperties** Fields that support **structured OData filtering** via the `filter` query parameter. +> **The list is exact from generator `0.24.0` (C#).** `filter` accepts the properties listed here, plus `Id`, `CreatedAt` and `UpdatedAt`. Any other clause answers `400` ("Property '…' is not filterable"). Before `0.24.0` three implicit sources also made a property filterable: every `searchableProperties` entry, every property named `status`, and every foreign key. If a client filters on one of those, add it here. Sorting, lookup indexes, the named list parameters below and the Python finders did not change. + **Purpose**: Enable precise, field-specific queries with operators (`eq`, `ne`, `gt`, `lt`, `and`, `or`) **Example**: @@ -2088,6 +2090,8 @@ GET /tasks?search=inventory&filter=status eq 'pending' and priority eq 'high' | `{field}After` / `{field}Before` | `?expiresAfter=` | `>` / `<` (boundary excluded) | | `{field}From` / `{field}To`, `{field}Min` / `{field}Max` | `?capacityMin=` | `>=` / `<=` (boundary included) | | repeated value on a scalar property | `?status=a&status=b` | any of the values | + +This table is about **named** query parameters. The `filter` parameter is narrower and accepts only what §10.2 lists, so a foreign key can bind as `?customerId=` and still answer `400` in `filter=customerId eq '…'` unless `filterableProperties` lists it. | repeated value on a plain array property (`array`) | `?tags=a&tags=b` | the row's array holds any of them | - An omitted parameter adds nothing. Parameters combine with `filter` and `search` (all apply). diff --git a/handbooks/Arazzo_Handbook.md b/handbooks/Arazzo_Handbook.md index b5a8096..b488d36 100644 --- a/handbooks/Arazzo_Handbook.md +++ b/handbooks/Arazzo_Handbook.md @@ -201,9 +201,6 @@ x-doc: personas: [customer, support-agent] businessOutcome: "Reduces friction in refund handling and provides a documented audit trail." -x-mcp: - exposed: false - x-actors: customer: role: Customer @@ -383,30 +380,17 @@ x-setup: The recipe's `outputs` become available in the scenario via the `$setup.outputs.X` expression root. -### 4.8 `x-mcp` (optional on scenario workflows) - -Declares whether this scenario is exposed as an MCP tool for AI orchestrators. - -```yaml -x-mcp: - exposed: true - toolName: request-refund - description: "Request a refund for a completed order" - requiresActorAuth: true - safeForAutoInvoke: false -``` +### 4.8 `x-mcp` (retired, generator `0.24.0`) -| Field | Required | Type | Description | -|-------|----------|------|-------------| -| `exposed` | Yes | boolean | Explicit opt-in. Default: `false` | -| `toolName` | When exposed | string | kebab-case tool name, **globally unique** across all scenarios in the repo | -| `description` | When exposed | string | MCP-facing description of what the tool does | -| `requiresActorAuth` | No | boolean | Whether the tool requires actor authentication context. Default: `true` | -| `safeForAutoInvoke` | No | boolean | Whether an AI agent can invoke this without human confirmation. Default: `false` | +**Delete the block.** Nothing reads `x-mcp` from generator `0.24.0`: it exposes no tool and gates nothing. A document that still declares it gets one `X_MCP_RETIRED` warning (scenario or recipe), even when the block is malformed, and no other change. The kit's Spectral has one matching warning, `specfuse-arazzo-x-mcp-retired`, and no longer checks the block's shape. -**Inputs and outputs are derived** from the workflow's `inputs` and `outputs` -- they are not redeclared in `x-mcp`. +| Old field | Where it went | +|-----------|---------------| +| `exposed`, `toolName`, `description` | The MCP manifest (`Project_File.md` §1.5). A tool with `source: {type: workflow, workflowId}` makes a workflow a tool. | +| `safeForAutoInvoke` | `x-effects` on the operations and the confirmation predicate derived from it (`Vendor_Extensions.md` §4.7). | +| `requiresActorAuth` | Derived from `x-actors`. | -**Global uniqueness of `toolName`** is enforced by a Spectral rule from Phase 0 onward. This prevents conflict accumulation while the MCP runtime consumer (Phase 7) is still deferred. +The generated scenario document no longer has a `## MCP Tool Integration` section, and the codes `ARAZZO_MCP_MISSING_TOOL_FIELDS`, `ARAZZO_MCP_TOOLNAME_FORMAT`, `ARAZZO_MCP_TOOLNAME_DUPLICATE` and `ARAZZO_RECIPE_HAS_MCP` are never raised. --- @@ -976,7 +960,6 @@ This means: | Step graph acyclic; all inputs satisfied | Workflow steps | Internal | Error | | Recipe `extends` chain valid, depth <= 6, no cycles | Recipe | Other recipes | Error | | No output namespace collisions in recipe chains | Recipe outputs | Parent recipe outputs | Error | -| `x-mcp.toolName` globally unique | All scenarios | All scenarios | Error | A step that deliberately sends an invalid body, to prove the server rejects it, must assert the 4xx in `successCriteria`. That assertion is what exempts it from the payload checks. A step that asserts a 2xx, or no status, is checked. A missing required property there fails `validate`, and `generate` then refuses the whole project. @@ -1037,29 +1020,18 @@ The `info.version` field uses semantic versioning for the Arazzo document itself ## 11) MCP Exposure -### 11.1 Contract - -The `x-mcp` extension on scenario workflows declares opt-in exposure as MCP tools. See Section 4.8 for the full schema. - -### 11.2 Design Principles - -- **Explicit opt-in:** `exposed: false` is the default. Scenarios are not MCP tools unless explicitly marked. -- **Derived I/O:** MCP tool inputs and outputs are derived from the workflow's `inputs` and `outputs` -- never redeclared in `x-mcp`. This avoids drift. -- **Global uniqueness:** `toolName` must be unique across all scenarios in the repo. Enforced by Spectral from Phase 0. -- **Safety default:** `safeForAutoInvoke: false` means an AI agent must confirm with a human before running the tool. Only mark `true` for read-only, low-risk scenarios. - -### 11.3 Example +Arazzo no longer declares MCP exposure. `x-mcp` is retired (§4.8), so a workflow becomes a tool by being named in an MCP manifest: ```yaml -x-mcp: - exposed: true - toolName: get-order-status - description: "Check the current status of a customer's order" - requiresActorAuth: true - safeForAutoInvoke: true # Read-only, safe for auto-invocation +tools: + - toolName: get-order-status + title: Get order status + description: "Check the current status of a customer's order" + kind: tool + source: { type: workflow, workflowId: get-order-status } ``` -The MCP runtime (Phase 7) consumes this metadata. Phase 0 establishes the contract and uniqueness enforcement. +`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. --- @@ -1124,9 +1096,7 @@ These files are the authoritative templates for new scenarios and recipes. When 16. **Do NOT create scenarios with >= 80% step-sequence overlap.** The Spectral rule `arazzo-scenario-step-overlap` flags this as an error. Consolidate into one scenario with input parameters or workflows for variants. -17. **Do NOT make `x-mcp.toolName` collide with another scenario's `toolName`.** Global uniqueness is enforced by Spectral. Choose descriptive, domain-prefixed names (e.g., `order-submit`, not `submit`). - -18. **Do NOT use `x-mcp` on recipe workflows.** Recipes are infrastructure, not user-facing tools. +17. **Do NOT keep an `x-mcp` block.** Nothing reads it from generator `0.24.0`, and it reads like a safety control that is not one. Delete it (§4.8). --- diff --git a/handbooks/AsyncAPI_Handbook.md b/handbooks/AsyncAPI_Handbook.md index e610211..f0b19a7 100644 --- a/handbooks/AsyncAPI_Handbook.md +++ b/handbooks/AsyncAPI_Handbook.md @@ -373,6 +373,49 @@ payload: after: { $ref: '../events/OrderSnapshot.yaml' } ``` +#### Binding the envelope to headers (generator `0.24.0`) + +The generator reads each message's merged `traits[]` headers and works out which header carries which envelope role. A header binds a role by its canonical name, or explicitly with `x-envelope: ` (`Vendor_Extensions.md` §12.2). What the C# runtime writes and the C# and Python readers look up follows from that binding. + +| Code | Severity | Fires when | +|---|---|---| +| `ASYNC_ENVELOPE_HEADER_MISSING` | ERROR | A message declares headers but binds no header to `eventId`, or, on a scoped event, to the `tenant` / `subTenant` the project declares | +| `ASYNC_TENANT_HEADER_ON_GLOBAL_EVENT` | ERROR | An event not under the tenant has headers that require the tenant header | +| `ASYNC_ENVELOPE_ROLE_UNKNOWN` | ERROR | An `x-envelope` value outside the role vocabulary | +| `ASYNC_ENVELOPE_ROLE_DUPLICATE` | ERROR | Two headers of one message mark the same role | +| `ASYNC_ENVELOPE_HEADER_RECOMMENDED` | WARNING | `eventVersion`, `aggregateId`, `aggregateType`, `producedAt` or `correlationId` is unbound | +| `ASYNC_ENVELOPE_HEADERS_UNDECLARED` | SUGGESTION | Once per spec whose messages declare no headers | + +**Migrating a trait that requires the tenant header: split it.** A single trait that lists the tenant in `required` cannot serve both a tenant-scoped event and a global one, and making the tenant optional would drop the missing-tenant guarantee. Write two self-contained traits, and point each tenant-less message at the second: + +```yaml +auditableEvent: # tenant-scoped: the tenant is required + headers: + type: object + required: [eventId, correlationId, producedAt, accountId] + properties: &envelopeHeaders + eventId: { type: string, format: uuid } + correlationId: { type: string, format: uuid } + producedAt: { type: string, format: date-time, x-envelope: producedAt } + accountId: { type: string, format: uuid, x-envelope: tenant } + +globalAuditableEvent: # no tenant: declared and marked, not required + headers: + type: object + required: [eventId, correlationId, producedAt] + properties: *envelopeHeaders +``` + +Neither trait layers on the other: AsyncAPI applies traits as JSON Merge Patch and replaces `required` arrays, while the generator unions them, so a layered pair means different things to the two readers. Measured on `0.24.0`, adding a `tenancy` block to a project whose one trait requires the tenant reports `ASYNC_TENANT_HEADER_ON_GLOBAL_EVENT` on the events of an entity not under the tenant. Dropping the tenant header instead reports `ASYNC_ENVELOPE_HEADER_MISSING` on the scoped ones. + +**The legacy header names go away in the next minor release.** A bound name now replaces `companyId`, `restaurantId` and `producedAt` on the wire. For `0.24.0` only, the C# producer also writes those literals when the bound name differs, and the C# and Python readers try the bound name and then the literal. A subscriber outside the generated code that reads `companyId` off the application properties must read the bound name before the next minor release. A message that declares no headers is unchanged. + +**`aggregateVersion` travels on every event of a C# entity.** It is written as an `aggregateVersion` application property even when a message's headers do not bind it, and it moves once per save of the record. Handler bases do not drop stale events. To compare, treat a version equal to or lower than the last one you applied for that record as stale, and treat `null` as no ordering information. Two events raised in one save share a version, so compare per label if a handler treats them as distinct work. A property that maps to the C# member `AggregateVersion` is reported as `ENTITY_AGGREGATE_VERSION_RESERVED`: an error on an event's subject, a warning otherwise. + +**The body no longer carries an undeclared tenant (C#).** A tenancy member the payload schema does not declare is left out of the outbox body, so a reader of the wire body that looked for that key must read the envelope header. A tenant-scoped event with no tenant now fails: `BufferedEventCollector.Add` throws, and a generated handler dead-letters it. On Python, `emit_*` requires `company_id` for a tenant-scoped event, raising `TypeError` when omitted and `ValueError` on `None`. + +**`contentType` must be JSON.** The generated runtime only serializes JSON, so a resolved content type (message, else last trait, else `defaultContentType`) other than `application/json` is `ASYNC_CONTENT_TYPE_UNSUPPORTED` (ERROR). The comparison ignores case and allows `charset=utf-8`; a message that declares none is unaffected. Measured on `0.24.0`: `application/avro` fails, and `application/json; charset=utf-8` and `APPLICATION/JSON` pass. + ### 2.2 Event Action Classes (Suffix-Driven Inference) The action class of an event is **inferred from its message-name suffix** — there is no `x-action-class` extension, and authors do not declare it. The Spectral rule `specfuse-async-event-name-action-class` validates that the suffix and payload shape align. @@ -823,6 +866,8 @@ x-scheduled-job: | `overlap` | No | string | `skip`, `queue`, `cancelPrevious` | Default: `skip` | | `scope` | No | string | `global`, `perTenant`, project-defined narrower scopes | Default: `perTenant` | +`host`, `placement` and `pool` are not keys of this block: from generator `0.24.0` they fail the parse with `ASYNC_FORBIDDEN_SCHEDULED_JOB_PLACEMENT`. Where a job runs is `workerPlacement.schedules` in the project file (`Project_File.md` §17.3). + The `scope` enum is project-tunable. Generator accepts arbitrary kebab/camelCase values and emits one job instance per scope row. #### `x-partition-key` (optional, on events requiring ordered delivery) @@ -853,7 +898,18 @@ x-partition-key: #### `x-trigger-when` (required on state-transition messages, forbidden on `*Created`/`*Updated`/`*Deleted`) -Pure boolean predicate over `Before`/`After` snapshot fields. The generator-emitted service code evaluates the predicate against `EntityEntry.OriginalValues` and `EntityEntry.CurrentValues` before `SaveChangesAsync`; on a match, the transition event fires AND the corresponding `*Updated` is suppressed (mutual exclusivity). +Pure boolean predicate over `Before`/`After` snapshot fields. On the generated C# update and replace paths, the service evaluates it against `EntityEntry.OriginalValues` and `EntityEntry.CurrentValues` before `SaveChangesAsync`; on a match, the transition event fires AND the corresponding `*Updated` is suppressed (mutual exclusivity). + +**The predicate gates emission only where generated code evaluates it:** + +| How the event is triggered | Is the predicate enforced? | +|---|---| +| Auto mode, generated C# update or replace | Yes | +| `x-trigger-mode: explicit` (C#) | No. The generated method persists and emits; satisfying the predicate is the caller's precondition | +| Hand-written lifecycle action | No. The generator emits nothing, so your code decides | +| Generated Python `emit_` | Yes. It returns `None` when the predicate is false | + +So do not read a predicate as a guarantee on a hand-written or explicit path. **Do not write a predicate over `updatedAt` or `createdAt`:** an audit stamp moves on nearly every write, so it says nothing about the transition. `validate` warns with `ASYNC_TRIGGER_WHEN_AUDIT_FIELD`. ```yaml x-trigger-when: "After.status == 'archived' && Before.status != 'archived'" diff --git a/handbooks/Project_File.md b/handbooks/Project_File.md index 368d14a..21cf043 100644 --- a/handbooks/Project_File.md +++ b/handbooks/Project_File.md @@ -47,7 +47,7 @@ Language-coupling column: ✱ means the field is honoured only for a specific la | `broker` | object | optional | ✱ C# event-runtime only | Messaging-broker dialect / topology settings. | | `broker.type` | enum | optional | ✱ | `azureServiceBus` (only working value). | | `broker.topology` | enum | optional | ✱ | `runtime` (default) \| `bicep` (experimental — emits stub). | -| `broker.outboxDialect` | enum | optional | ✱ | `sqlServer` (default) \| `postgres` (experimental — emits stub). | +| `broker.outboxDialect` | enum | optional | ✱ | `sqlServer` (default). `postgres` was **removed in generator `0.24.0`** and fails the load. | | `persistence` | object | optional | **⚠ not implemented** | Multi-backend persistence configuration. **Read by nothing today — declaring it changes nothing and raises no diagnostic.** Design of record; see the banner on §6 and `compatibility.md` §27. Every `persistence.*` row below carries the same caveat. | | `persistence.connections` | object | yes (when `persistence` set) | — | Named connection descriptors. Every referenced connection must be declared here. | | `persistence.connections..provider` | enum | yes | — | `sqlServer` \| `postgres` \| `mySql` \| `cosmos` \| `azureBlob`. Must match the `kind` of any descriptor referencing this connection. | @@ -70,9 +70,12 @@ Language-coupling column: ✱ means the field is honoured only for a specific la | `functionalTests.emitAssertions` | enum | optional | ✱ C# scenario tests | **From generator `0.21.0`.** `off` (default) \| `strict`. `strict` makes a scenario step's `x-async.emit` an awaited assertion the test fails on. See §18. | | `generateUserHelper` | boolean | optional | ✱ C# only | **From generator `0.20.0`.** Default `false`. `true` makes `authorizationRuntime` emit `Utils/UserHelper.g.cs`. See §16. | | `workerPlacement` | object | optional | ✱ C# workers | **From generator `0.20.0`.** Which host runs each topic worker: `azureFunctions` (default) or `genericHost`. See §17. | +| `mcpManifests` | string (path) | optional | validation; ✱ C# MCP artifacts | **From generator `0.24.0`.** A file, or a directory of `*.mcp.yaml` files. See §1.5, including its warning. | | `workerPlacement.messages.default` | enum | optional | ✱ | `azureFunctions` (default) \| `genericHost`. | | `workerPlacement.messages.azureFunctions[]` / `.genericHost[]` | array | optional | ✱ | AsyncAPI operation names placed on that host, whatever the default. | | `workerPlacement.messages.genericHost` (pools) | object> | optional | ✱ | **From generator `0.22.0`.** Pool name → operation names; each pool deploys as its own unit. See §17.1. | +| `workerPlacement.schedules` | object | optional | ✱ C# workers | **From generator `0.24.0`.** `default`, `inProcess` (array or pools), `cronJob`. Places scheduled jobs. See §17.3. | +| `workerPlacement.runtime` | object | optional | ✱ C# workers | **From generator `0.24.0`.** `host`, `pool`. Places the outbox drainer and both retention jobs together. See §17.3. | | `groups[]` | array | yes for generation | — | One artifact group per output tree. | | `groups[].name` | string | recommended | — | Human-readable group label; used by `--group` and diagnostics. | | `groups[].description` | string | optional | — | Free-form description. | @@ -188,6 +191,47 @@ The three spec-path fields tell the generator where to find the bundled inputs. { "asyncSourceRoot": "./api/specs/v3" } ``` +### 1.5 `mcpManifests` (generator `0.24.0`) + +> **⚠ Do not register `mcpHost` or `mcpEvalHarness` on a real manifest at `0.24.0`.** They are verified on fixtures only. On a real manifest the generated tools fail to compile, five composite tools advertise required ids a caller cannot obtain, and **a preview tool commits instead of rolling back, so a `preview-*` tool performs the real write with no confirmation**. A group that does not register these two is unaffected. `mcpServerResources`, `validate` and the `MCP_*` findings work as documented. + +**Purpose**: A file, or a directory of `*.mcp.yaml` files, resolved against the project file's directory like `arazzoSpecifications`. A manifest declares one MCP server's tools, prompts and resources. + +**Required**: Optional. A project that does not set it is unchanged. + +```yaml +server: ops +budget: 20 +tools: + - toolName: get-order-status + title: Get order status + description: Check the current status of a customer's order + kind: tool + source: { type: operation, operationId: getOrder } +``` + +| Level | Keys | +|---|---| +| Manifest | `server`, `tools`, `budget`, `tokenBudget`, `evalThreshold`, `resources` | +| Tool | `toolName`, `kind` (`tool`, `prompt`), `source`, `title`, `description`, `entities`, `fieldSet`, `responseFields`, `previewTool`, `requiresConfirmation`, `maxPageSize`, `examples`, `x-version` | +| `source.type` | `operation`, `workflow`, `composite` | +| Resource | `name`, `enum`, `schema` | + +**`server` is a closed list on `0.24.0`**: `admin`, `compliance`, `inbound`, `ops`, `scheduling`, `workforce`. Anything else is `MCP_MANIFEST_SHAPE_INVALID`. `budget` takes 1 to 40, `maxPageSize` 1 to 100. + +**What `validate` checks**, all ERROR unless marked (`java -jar extensions` prints the current vocabulary): + +| Finding | Meaning | +|---|---| +| `MCP_MANIFEST_PARSE_ERROR`, `MCP_MANIFEST_SHAPE_INVALID` | The file cannot be read, or breaks the shape rules above | +| `MCP_RESOURCES_WITHOUT_MANIFESTS` | A group registers `mcpServerResources` and the project sets no `mcpManifests`. Each group that lists the artifact gets the files; a group that does not gets nothing | +| `MCP_SOURCE_UNRESOLVED` and the other reference codes | A manifest reference names nothing | +| `MCP_VERB_NOT_PERMITTED` and the other permission codes | A tool does more than the touched entities' `aiAccess` allows | +| `MCP_OPERATION_CLOSURE_NOT_READABLE` | A tool's response reaches an entity whose `aiAccess` does not grant `read`. Grant `read` on that entity on its own merits, or narrow `fieldSet` / `responseFields` | +| `MCP_INPUT_NOT_PRODUCIBLE` | A required input that no tool on the server produces and the caller cannot supply | +| `MCP_ADVERTISED_SIZE` (SUGGESTION) | The server's size in tokens, one finding per server. **`generate --fail-on-findings=suggestion` therefore fails every project with a manifest**: use `warning` | +| `MCP_ADVERTISED_SIZE_EXCEEDED` (WARNING) | The measured size is over `tokenBudget` | + --- ## 2. Project Identity @@ -437,7 +481,7 @@ The broker block is meaningful only for projects that emit the event-runtime art |-------|------|---------|----------------| | `type` | enum | `azureServiceBus` | `azureServiceBus` | | `topology` | enum | `runtime` | `runtime` \| `bicep` (experimental — emits stub) | -| `outboxDialect` | enum | `sqlServer` | `sqlServer` \| `postgres` (experimental — emits a `NotImplementedException` stub in the drainer) | +| `outboxDialect` | enum | `sqlServer` | `sqlServer`. `postgres` was removed in generator `0.24.0`: it only ever generated a drainer and a job lock that threw at run time, and now fails the load with a message naming the removal (exit 2). Remove the key. | **Validation**: Unknown values fail the load with a Levenshtein-based "did you mean" suggestion. @@ -976,10 +1020,10 @@ The project file fails to load when any of the following conditions hold. All er | `PROJECT_TENANCY_FK_NOT_ID` | Tier `foreignKey` does not end in `Id`. | | `PROJECT_TENANCY_MARKER_NOT_INTERFACE` | Tier `marker` does not match `^I[A-Z][A-Za-z0-9]*$`. | | `PROJECT_TENANCY_ENFORCEMENT_UNKNOWN` | **From generator `0.21.0`.** `tenancy.enforcement` is set to anything but `route` (§4.1). | -| `UNKNOWN_WORKER_HOST` | **From generator `0.20.0`.** `workerPlacement.messages.default`, or a key under `messages`, names neither `azureFunctions` nor `genericHost` (§17). | +| `UNKNOWN_WORKER_HOST` | **From generator `0.20.0`.** `workerPlacement.messages.default`, or a key under `messages`, names neither `azureFunctions` nor `genericHost` (§17). From `0.24.0` this also covers a misspelled `schedules` or `runtime` host or key. | | `UNKNOWN_EMIT_ASSERTIONS` | **From generator `0.21.0`.** `functionalTests.emitAssertions` is neither `off` nor `strict` (§18). | | `TRIGGER_REQUIRES_EVENT_RUNTIME_IMPL` | **From generator `0.20.0`, breaking.** A group registers `azureFunctionTopicTrigger`, `hostedTopicConsumer` or `heldEntityHydrationTrigger`, and no group of the same language registers `eventRuntimeImpl` (§10). | -| `PROJECT_BROKER_TYPE_UNKNOWN` / `PROJECT_BROKER_TOPOLOGY_UNKNOWN` / `PROJECT_BROKER_DIALECT_UNKNOWN` | `broker.type` / `broker.topology` / `broker.outboxDialect` set to an unrecognised value. | +| `PROJECT_BROKER_TYPE_UNKNOWN` / `PROJECT_BROKER_TOPOLOGY_UNKNOWN` / `PROJECT_BROKER_DIALECT_UNKNOWN` | `broker.type` / `broker.topology` / `broker.outboxDialect` set to an unrecognised value, which from generator `0.24.0` includes `postgres`. | | `INVALID_DART_GROUP_FIELD` | `unknownEnumPolicy` or `formatPolicy` outside its accepted set (Dart/Flutter groups only). | | `INVALID_FILTER` | `groups[].filter` malformed — multiple top-level operators, unknown operator, wrong value shape. | | `INVALID_DOMAIN_FILTER` | `groups[].domains` declares both `include` and `exclude` non-empty. | @@ -1005,6 +1049,12 @@ The OpenAPI / AsyncAPI / Arazzo spec contents are validated separately by the pe | `WORKER_PLACEMENT_UNKNOWN_WORKER` | ERROR | `0.20.0` | A name under `workerPlacement.messages` matches no AsyncAPI operation. | Spell it as the operation key under `operations`, or remove the line. | | `WORKER_PLACEMENT_NOT_A_TOPIC_WORKER` | ERROR | `0.20.0` | The name is a scheduled job, a publisher or an `x-ai` worker. | Remove it. Only a topic worker can be placed. | | `WORKER_PLACEMENT_NO_HOST_GROUP` | ERROR | `0.20.0` | A topic worker is placed on a host whose artifact (`azureFunctionTopicTrigger` or `hostedTopicConsumer`) no C# group registers, so nothing emits it. | Register the host artifact, or place the worker elsewhere. | +| `WORKER_PLACEMENT_SCHEDULE_UNKNOWN_JOB` / `WORKER_PLACEMENT_SCHEDULE_NOT_A_JOB` | ERROR | `0.24.0` | A name under `workerPlacement.schedules` matches no worker, or is not a scheduled job. | Spell it as the job's operation key, or remove the line. | +| `WORKER_PLACEMENT_SCHEDULE_CONFLICT` | ERROR | `0.24.0` | A job is placed twice. | Place it once. | +| `WORKER_PLACEMENT_CRONJOB_SUB_MINUTE` / `WORKER_PLACEMENT_CRONJOB_NOT_FIVE_FIELD` | ERROR | `0.24.0` | A job under `cronJob` has a schedule a Kubernetes CronJob cannot run. | Use a five-field cron, or `inProcess`. | +| `WORKER_PLACEMENT_SCHEDULE_POOL_NAME_INVALID` / `WORKER_PLACEMENT_SCHEDULE_POOL_EMPTY` / `WORKER_PLACEMENT_RUNTIME_POOL_INVALID` | ERROR | `0.24.0` | A bad or empty schedule pool, or a `runtime.pool` that is not valid. | Fix the pool. | +| `WORKER_PLACEMENT_SCHEDULE_NO_HOST_GROUP` / `WORKER_PLACEMENT_RUNTIME_NO_HOST_GROUP` | ERROR | `0.24.0` | No C# group registers `hostedScheduledJob` / `eventRuntimeHosted`, so nothing emits what you placed. | Register the artifact in the generic-host group. | +| `WORKER_PLACEMENT_HOST_ARTIFACT_WITHOUT_CONSUMER` | ERROR | `0.24.0` | A host artifact is registered with nothing placed on it. | Place something, or remove the artifact. | | `WORKER_PLACEMENT_CONFLICT` | ERROR | `0.20.0` | An operation is listed under both hosts, or (from `0.22.0`) in two pools of `genericHost`, or one C# group registers both `azureFunctionTopicTrigger` and `hostedTopicConsumer`. | List it once; give each host artifact its own group. | | `WORKER_PLACEMENT_POOL_NAME_INVALID` | ERROR | `0.22.0` | A pool name under `genericHost` is not a DNS-1123 label of at most 20 characters, so it cannot name a Deployment. | Use lowercase letters, digits and `-`, starting and ending with a letter or digit (§17.1). | | `WORKER_PLACEMENT_POOL_EMPTY` | ERROR | `0.22.0` | A pool under `genericHost` lists no worker, so it would deploy nothing. | Add its workers, or remove the pool. | @@ -1028,7 +1078,7 @@ From generator `0.16.0`, `validate` also warns `FUNCTIONAL_TEST_TAG_UNGROUPED` w - **`workerPlacement` ⟷ host groups (generator `0.20.0`)** — a C# group registering `azureFunctionTopicTrigger` is the Azure Functions host, one registering `hostedTopicConsumer` is the generic host, and each emits only the topic workers placed on it (§17). With no `workerPlacement`, every topic worker is on Azure Functions, so a `hostedTopicConsumer` group emits no consumer. - **`namingOverrides` ⟷ `x-enumCase`** — `x-enumCase: preserve` on a schema wins over a project-wide `enumValue` override. - **`tenancy` ⟷ `x-entity.belongsTo`** — entities whose `belongsTo` references the tenancy entities receive the corresponding `I{Tenant}Scoped` marker interface in the C# target. From generator `0.15.0`, so does an entity with a `resolve: remote` reference to the tenant entity (§4.1). The project-file loader does not validate that `tenancy.tenant.entity` resolves to a real `x-entity` schema; see §4.1 for the downstream symptoms. -- **`broker.outboxDialect` ⟷ event-runtime functions artifact** — `postgres` emits a stub in the drainer; only `sqlServer` produces a working runtime today. +- **`broker.outboxDialect` ⟷ event-runtime functions artifact** — `sqlServer` is the only dialect from generator `0.24.0`; `postgres` fails the load. - **`persistence` ⟷ `x-entity`** — every `x-entity` schema in the bundled spec must resolve to a backend descriptor (either via `persistence.entities.` or `persistence.default`). Unresolved entities fail the load. `x-value-object` schemas are excluded — they persist as part of their owning entity. - **`persistence` ⟷ `tenancy`** — orthogonal. Tenant scoping is generated against the resolved backend kind: FK column for `relational`, partition key for `document`, key prefix for `blob`, both sides for `hybrid`. - **`persistence` ⟷ `aiAccess`** — AI-scoped repository methods inherit the entity's backend kind. Generated AI surfaces use the same `kind`-specific template family as the production repository. @@ -1059,6 +1109,10 @@ Three opt-in C# ids, none in the snapshot list above. Each emits only when regis | Id | From | Group | What it emits | Conditions | |---|---|---|---|---| | `hostedTopicConsumer` | `0.20.0` | A C# group of its own (the generic host), beside `asyncEventHandler`, `asyncConsumerRegistration` and `event` | One `TopicConsumer` per topic worker placed on `genericHost` (§17), each reading the worker's subscription through one `ServiceBusProcessor` and calling the same `I` the Functions trigger calls; the `HostedTopicConsumer` base and `WorkerHostContext`; `services.AddGeneratedWorkers()` (hosted services, graceful SIGTERM stop, `live` / `ready` health checks), and from `0.22.0` `builder.AddGeneratedWorkers()`, which selects a pool by `Workers:Pool` (§17.1); `workers.json` and `WorkerManifest` listing every topic worker and its host; `WorkerProvisioning` for a `provision` command. No `Program.cs`. | Registering it beside `azureFunctionTopicTrigger` in one group is `WORKER_PLACEMENT_CONFLICT`. Needs `eventRuntimeImpl` in the project (`TRIGGER_REQUIRES_EVENT_RUNTIME_IMPL`). Scheduled jobs stay in the Functions group. With no `workerPlacement`, it emits no consumer. | +| `hostedScheduledJob` | `0.24.0` | The generic-host group | `{Job}ScheduledJob.g.cs` per job placed under `schedules.inProcess` or `schedules.cronJob`, plus `ScheduledJobRunner` for `cronJob` (§17.3). `inProcess` needs the `Cronos` package. | +| `eventRuntimeHosted` | `0.24.0` | The generic-host group, beside `hostedTopicConsumer` | `HostedEventOutboxDrainer`, `HostedEventOutboxRetention` and `HostedEventInboxRetention`, when `runtime.host` is `genericHost` (§17.3). | +| `mcpServerResources` | `0.24.0` | A C# group | An effects and a descriptions file per manifest server (§1.5). | +| `mcpHost`, `mcpEvalHarness` | `0.24.0` | **Do not register on a real manifest at `0.24.0`** (§1.5) | `AddSpecfuseMcp()` and the tool, prompt and resource classes; a harness scoring a model's tool selection. | | `tenantIsolationTest` | `0.21.0` | Test | `Generated/Unit/Infrastructure/TenantIsolationTests.g.cs`: per tenant-owned entity, a list never returns the foreign tenant's rows, a get, update or delete of a foreign row answers not found, `Unrestricted` sees both tenants, an empty visibility sees nothing, the model carries the `Tenant` filter. Adds the within-tenant facts for entities that declare `x-entity.visibility`. `TestSeed` gains the foreign tenant's graph. | Needs a `tenancy` block. Emits nothing unless a `Tenant` or `Visibility` filter is declared. The Test project must reference Infrastructure. Runs in memory under the unit-test step, not under the functional suite. | | `bareIgnoreSourceScanTest` | `0.21.0` | Test | `Generated/Unit/Infrastructure/BareIgnoreSourceScanTest.g.cs`: fails when hand-written C# calls `IgnoreQueryFilters()` with no argument, which drops the `Tenant` filter too, unless the call is listed in `bare-ignore-query-filters.baseline` at the solution root. | Needs a `tenancy` block and `tenancy.enforcement` unset; otherwise it emits nothing. The baseline is yours, never generated, and only shrinks: a missing file reads as empty, and a line for a call that is gone fails as stale. | @@ -1197,7 +1251,7 @@ See foot-gun #1 at the top of this document. - **No `$schema` URL, no JSON Schema shipped.** The project file does not carry a `$schema` field today, and no JSON Schema is published with the generator. Editors and CI lint tools get no completion, no field-name validation, and no shape checking — every constraint is enforced by the parser at load time. Combined with foot-gun #1 (silent ignore of unknown fields), this is the project file's largest authoring hazard. **A JSON Schema (with `additionalProperties: false`) ships with the persistence rollout — phase 1a.** - **`groups[].filter` is AsyncAPI-workers-only in v1.** Filtering by extension on REST operations, entities, or value objects is not supported today. -- **`broker.topology: bicep` and `broker.outboxDialect: postgres` are placeholders.** They are accepted by the parser but emit stub code in the runtime templates. +- **`broker.topology: bicep` is a placeholder.** It is accepted by the parser but emits stub code in the runtime templates. `broker.outboxDialect: postgres` was the other one until generator `0.24.0` removed it. - **Tenancy artifacts are C# only today.** Python and Dart/Flutter consume the `tenancy` block indirectly (via AsyncAPI validation rules) but do not emit marker interfaces. - **Cross-spec tenancy entity name is not validated.** A typo in `tenancy.tenant.entity` produces no load-time error; see §4.1 for the downstream symptoms. @@ -1481,6 +1535,8 @@ All three fail `Project.Load`. The CLI prints the message, not the code (`Valida **From generator `0.20.0`.** `workerPlacement` says which host runs each C# topic worker: Azure Functions (`azureFunctions`) or a generic .NET host such as a Kubernetes pod (`genericHost`). Keys under `messages` are AsyncAPI operation names, as written under `operations`. +From generator `0.24.0` it also places scheduled jobs and the event runtime (§17.3). + ```json { "workerPlacement": { @@ -1542,9 +1598,36 @@ The `WORKER_PLACEMENT_*` codes (ERROR, §9) catch a name that matches no worker, - Why: from generator `0.22.0`, `pools[].k8sName` is the bare pool name. A flat `genericHost` array is pool `default` in every pillar, so two pillars deployed into one namespace under one prefix both declare a Deployment named `default`. 3. **A named pool lists workers of one pillar only.** Use per-pillar pool names, such as `a-fast` and `b-fast`. - Why: a pool that names workers from two pillars becomes two Deployments with the same name. Each one has half the workers and half the concurrency, and `validate` does not flag it. -4. **One database per pillar** (from the next generator release, which ships C# timer workers on the generic host). +4. **One database per pillar** (from generator `0.24.0`, which runs every scheduled job under a job lock, on either host). - Why: the generated scheduled-job lock and the `ScheduledJobRun` watermark are keyed by job name. The three runtime functions, `EventOutboxDrainerFunction`, `EventOutboxRetentionFunction` and `EventInboxRetentionFunction`, have the same name in every pillar. Two pillars sharing one SQL database would contend for one lock and one watermark row, and each would see the other's runs as skipped. +### 17.3 Scheduled jobs and the event runtime + +**From generator `0.24.0`.** `schedules` and `runtime` put a scheduled job, and the event-runtime unit, on the generic host. A project declaring neither key keeps its jobs on Azure Functions. + +```json +{ + "workerPlacement": { + "schedules": { + "default": "azureFunctions", + "inProcess": { "fast": ["runSendReminders"] }, + "cronJob": ["runNightlyExport"] + }, + "runtime": { "host": "genericHost", "pool": "fast" } + } +} +``` + +| Placement | What is emitted | Needs | +|---|---|---| +| `schedules.inProcess` (array, or pools as in §17.1) | `{Job}ScheduledJob.g.cs` from `hostedScheduledJob`: a hosted service that waits on the job's cron in its declared time zone, registered by `AddGeneratedWorkers` in the pool `Workers:Pool` selects. The Functions group stops emitting its timer trigger. | `hostedScheduledJob`, the `Cronos` package, an `IDbContextFactory` | +| `schedules.cronJob` | The same `TickAsync` as a plain class, plus `ScheduledJobRunner`, the `run-job ` command a Kubernetes CronJob starts on the same image. Exit code 0 ran or skipped, 1 the handler threw or was cancelled, 2 not a `cronJob` job. | `hostedScheduledJob`, a `Program` that calls `ScheduledJobRunner.IsRunJobCommand(args)` and `RunAsync(host.Services, ScheduledJobRunner.JobNameOf(args), flush)` | +| `runtime.host: genericHost` | `HostedEventOutboxDrainer`, `HostedEventOutboxRetention` and `HostedEventInboxRetention` from `eventRuntimeHosted`. The three move together, and the Functions group emits none of them. | `eventRuntimeHosted` beside `hostedTopicConsumer`, an `IDbContextFactory` | + +- **Every job runs under a lock, on either host.** From `0.24.0` each tick takes a SQL Server job lock and a `ScheduledJobRun` watermark: a held lock is a skipped tick, and a job that started less than half its minimum period ago counts as the same tick. So `ScheduledJobRun` needs a migration even if you place nothing here ([`csharp-component-guide.md` §9.2](../docs/csharp-component-guide.md)). +- **Do not put placement on the job.** `host`, `placement` or `pool` on `x-scheduled-job` fails the parse with `ASYNC_FORBIDDEN_SCHEDULED_JOB_PLACEMENT`. +- **`workers.json` gains a `schedules[]` array**, also in `WorkerManifest.g.cs`, listing every scheduled job on whichever host with its timer fields. A project that places nothing gets it too, so a copy of `workers.json` kept beside a chart must be refreshed. + --- ## 18. Functional Tests diff --git a/handbooks/Vendor_Extensions.md b/handbooks/Vendor_Extensions.md index f1a6489..b3bf7bc 100644 --- a/handbooks/Vendor_Extensions.md +++ b/handbooks/Vendor_Extensions.md @@ -57,7 +57,8 @@ x-entity: uniqueBy: object[] # Optional: uniqueness constraints, each { name?, properties[], nullsDistinct?, where[]? } visibility: string | object # Optional: "tenantWide" | "subTenant", or { mode, scope, unscopedRows } (generator 0.21.0) valueObjects: object # Optional: Value object storage configuration - aiAccess: object # Optional: AI agent access policy (see 1.1.1). Absent = no AI access. + fieldSets: object # Optional: named property lists for a read, inside aiAccess (generator 0.24.0) + aiAccess: object # Required from generator 0.24.0: AI agent access policy (see 1.1.1). Absent is ENTITY_AIACCESS_MISSING (ERROR) ``` **`domain` (required, leads the block).** Every entity is assigned to exactly one domain. The value is a kebab-case name that **MUST** be a key in the project's domain registry `info.x-domains` (see `API_Handbook.md §0.1` and the new-project scaffold's `openapi.yaml`). The registry is a **closed universe**: an entity may only name a registered domain, and the validator rejects an `x-entity.domain` that has no matching `info.x-domains` key (`ENTITY_DOMAIN_UNREGISTERED`, ERROR). The value also matches the entity's `domains/{domain}/` folder and lines up 1:1 with the AsyncAPI channel `x-domain` (§12.1) and Arazzo workflow `x-domain` (§13.1) — one domain vocabulary shared across all three specs. Author it first so the entity's home is unambiguous before any relationship or access metadata is read. @@ -1081,13 +1082,45 @@ documented spelling only, as it does for `delete` and `concurrency`. > accepts the key whatever the pin, so check `generator.lock` before you rely on > it. +### `fieldSets` — named reads of an entity (generator `0.24.0`) + +`x-entity.fieldSets` maps a set name to the properties a read returns. An MCP tool names one with `fieldSet` (`Project_File.md` §1.5). + +```yaml +x-entity: + fieldSets: + summary: [id, status, title.en] + aiAccess: + operations: [read] + readableProperties: [id, status, title] + reason: Agents list these to pick one to open. +``` + +An entry may be dotted. Each segment is checked: the first against the entity's own properties, each later one against the schema the previous one resolves to, through a `$ref` where there is one. + +**A field set cannot widen `aiAccess`.** An entry must be inside the effective `readableProperties`: the explicit list verbatim, or, when that is omitted, every top-level property minus the `atRest: encrypted` ones. A dotted entry is readable when it or a parent path is. A **sensitive** entry has to be listed explicitly, so the implicit expansion is not enough for it. Sensitive means `x-protection.atRest: encrypted` or any `x-classification` token except `exposed` and `untrusted`, so `pii` counts, and a dotted entry is sensitive when any segment is. Measured on `0.24.0`: `fieldSets: {summary: [id, firstName]}` with a `pii` `firstName` and no `readableProperties` fails, and passes once `readableProperties` lists `firstName`. + +| Code (all ERROR) | Fires when | +|---|---| +| `ENTITY_FIELDSETS_SHAPE_INVALID` | `fieldSets` is not a mapping, or is empty | +| `ENTITY_FIELDSET_NAME_INVALID` | A set name is outside `^[a-z][A-Za-z0-9]*$` | +| `ENTITY_FIELDSET_VALUE_INVALID` | A set is not a list of strings | +| `ENTITY_FIELDSET_EMPTY` | A set lists nothing | +| `ENTITY_FIELDSET_PROPERTY_DUPLICATE` | A set lists a property twice | +| `ENTITY_FIELDSET_PROPERTY_NAME_INVALID` | An entry is not a valid property name or path | +| `ENTITY_FIELDSET_PROPERTY_UNKNOWN` | A segment names no property | +| `ENTITY_FIELDSET_PROPERTY_NOT_READABLE` | An entry is outside the effective `readableProperties` | +| `ENTITY_FIELDSET_PROPERTY_SENSITIVE_UNLISTED` | A sensitive entry is not listed explicitly in `readableProperties` | + +An entity with `fieldSets` and no `aiAccess` reports the single `ENTITY_AIACCESS_MISSING`; the older `ENTITY_FIELDSETS_AI_ACCESS_MISSING` no longer exists. + ### 1.1.1 aiAccess (within x-entity) **Purpose**: Declares the access policy that generated repositories and service layers must enforce for AI agents interacting with this entity. Consumed by the code generator to emit AI-scoped repository methods, field allow-lists, and audit hooks. **Scope**: Required on every `x-entity` block (main resource schemas — aggregates and entities). Tier 0 entities (those the AI must not touch) use the explicit empty-operations form (see below) rather than omitting the block. -**Semantic default when absent**: No AI access — agents cannot read, create, update, or delete the entity through any generated AI-facing surface. However, **absence is a validator warning** (`ENTITY_AIACCESS_MISSING`), because it cannot be distinguished from authoring oversight. Every entity must make the AI-access decision explicit by declaring an `aiAccess` block. For entities the AI must not touch, use the canonical Tier 0 form: `operations: []` with `reason`. +**Semantic default when absent**: No AI access — agents cannot read, create, update, or delete the entity through any generated AI-facing surface. However, **absence is a validator error from generator `0.24.0`** (`ENTITY_AIACCESS_MISSING`, a warning before that), because it cannot be distinguished from authoring oversight: `validate` and `generate` both fail. Every entity must make the AI-access decision explicit by declaring an `aiAccess` block. For entities the AI must not touch, use the canonical Tier 0 form: `operations: []` with `reason`. **Schema**: ```yaml @@ -1107,14 +1140,14 @@ aiAccess: - **`writableProperties`** — must reference top-level properties of the entity. Write access is always an explicit allow-list; there is no "all fields" shortcut for writes. Required whenever `operations` includes any of `create`, `update`, `delete`. - **`immutableOnUpdate`** — subset of `writableProperties`. Fields the agent may supply on create but never modify on update (e.g., foreign keys, tenant scope). - **`ownedBy`** — records the system of record when both AI and backend can write: - - `shared` (default): either side may write; last-writer-wins with audit + - `shared` (default): either side may write; last-writer-wins with audit. From generator `0.24.0` (C#), an entity that is the subject of an event and is not `concurrency: optimistic` carries `AggregateVersion` as its concurrency token, so a generated write that loses a race answers `409` instead of winning last, with no ETag and no `If-Match` - `ai`: the AI agent is authoritative; backend writes are reconciled against AI state - `backend`: the backend is authoritative; AI writes are advisory and may be overridden - **`reason`** — required in two cases: (a) `operations` is empty (the Tier 0 justification — why this entity is intentionally outside the AI surface), and (b) `operations` contains any write verb (the audit justification for granting writes). Free-text surfaced in generated doc-comments and the AI manifest. Should not reference the target programming language — describe the business need. **Validation rules** (enforced by Spectral + DDD validator): -1. `aiAccess` is required on every `x-entity` block. Absence triggers a WARN-level finding (`ENTITY_AIACCESS_MISSING`) — present-but-empty `operations` is the canonical Tier 0 declaration. +1. `aiAccess` is required on every `x-entity` block. Absence is an ERROR from generator `0.24.0` (`ENTITY_AIACCESS_MISSING`) — present-but-empty `operations` is the canonical Tier 0 declaration. 2. `operations` must be present. An empty array (`[]`) is valid and signals deliberate Tier 0 / explicit no-access. When `operations` is empty, `reason` is required. 3. If `operations` contains any of `create`/`update`/`delete`, then `writableProperties` (non-empty) and `reason` (non-empty) are required. 4. `readableProperties`, `writableProperties`, and `immutableOnUpdate` must reference top-level properties that exist on the entity schema (BilingualText subfields like `title.en` allowed). @@ -1484,7 +1517,7 @@ ShipmentCarrier: Two things about that list are deliberate and worth knowing before you argue with it: - **It is exact-match, not substring.** An `emails` array on an `Inbox` schema does not trigger it. The narrowness is the point — a substring heuristic flags collections and unrelated fields, and a rule that fires on those gets switched off. -- **The check is presence, not correctness.** Any non-empty classification satisfies it, including `[exposed]`. Choosing the *right* member is your judgement; the validator only refuses silence. (`[exposed]` on a `dateOfBirth` therefore passes — see `compatibility.md` §26.) +- **The check is presence, not correctness.** Any non-empty classification satisfies it, including `[exposed]`, but not `[untrusted]` alone from generator `0.24.0`. Choosing the *right* member is your judgement; the validator only refuses silence. (`[exposed]` on a `dateOfBirth` therefore passes — see `compatibility.md` §26.) **Derivatives are exempt.** The rule runs on `x-entity` schemas only; `New*` / `Update*` / `Basic*` project the same fields and would double-flag every PII property. @@ -1508,6 +1541,7 @@ x-classification: | `credential` | Secrets that authenticate a principal | Never safe to return. MUST NOT co-occur with `exposed`. | | `cardholder` | Cardholder data (PAN and equivalents) | Never safe to return. MUST NOT co-occur with `exposed`. | | `sad` | PCI Sensitive Authentication Data (CVV, track, PIN block) | Never safe to return, and MUST NOT be retained after authorisation. MUST NOT co-occur with `exposed`. | +| `untrusted` | **From generator `0.24.0`.** Authored outside the tenant's authenticated principals, such as free text a customer typed | **Mark a field when ANY ingestion path can fill it with text written outside the tenant's signed-in users**: an inbound email's subject, an uploaded file's name. Text your own staff type does not qualify, even free text. Most entities have a `notes` field, so that reading would make every write tool confirm, and a gate that always fires gets switched off. States provenance, nothing about protection. It does not satisfy `PII_FIELD_MISSING_CLASSIFICATION`: an `email` classified only `[untrusted]` is still reported, so write `[pii, untrusted]`. `PROTECTION_ATREST_REQUIRED` still applies, so declare `x-protection.atRest` beside it. It triggers no snapshot acknowledgement and no retention requirement on its own (`[pii, untrusted]` still does). Inherited by a derived DTO like any token, and also accepted at `x-entity.valueObjects.

.classification`. | | ~~`encrypted`~~ | **RETIRED — generator 0.7.0.** | Rejected with `INVALID_EXTENSION_VALUE` at both the property level and the `x-value-object.classification` usage site. Declare `x-protection: {atRest: encrypted}` on the property instead; that is the axis the generator reads, and it is REQUIRED on every classified property regardless. The closed set is now the eight values above. | | `exposed` | Secret-shaped name, human-reviewed, safe to expose | Escape hatch for the `SENSITIVE_FIELD_IN_RESPONSE` validator (see below). Asserts a reviewer confirmed the field is safe on a response despite a secret-shaped name. **Grants no access** on its own; carries no encryption/masking obligation. Contradictory with `sensitive` and `confidential`, and with an `x-protection.atRest` of `encrypted` or `hashed` — MUST NOT co-occur with any of them (`PROTECTION_EXPOSED_MUTEX`). May co-occur with `pii`, `financial` or `cardholder`, which the generator accepts. Requires a `description` justifying why exposure is safe. | @@ -2167,6 +2201,8 @@ Article: **Optional**: Yes, but see "Every FK-shaped property must be classified" below — in practice an unclassified uuid property is a spec defect, not a default. +> **A client-writable reference to a global entity gets no tenant check.** From generator `0.24.0` the save-time write guard resolves a reference to a tenant-owned entity, and `TENANT_REFERENCE_NOT_WRITE_CHECKED` warns on one it cannot check. Neither covers a target with no `belongsTo` path to a tenancy root, such as a global `User`. A request body carrying any existing id of that entity passes, so a task can be assigned to a stranger, and the difference between an error and success reveals that the record exists. Until the generator has a way to declare the membership that scopes such an entity, check it in hand-written code on every write path, and answer the same way whether the record is unknown or foreign. Fields the server fills from the caller (`x-populate-from`) are not exposed to this. + **Schema**: ```yaml @@ -2995,9 +3031,9 @@ The second rule is **armed by `authorization.kioskSession.role`** in the project > **⚠ Nothing reads this extension. Do not gate anything on it.** > -> `x-ai-safe` appears nowhere in the generator: zero occurrences of the key and zero of any `aiSafe` identifier across the source, against controls in the same search that find `x-public` and `x-manual` in three files each and `x-mcp` in five. No kit Spectral rule enforces it either. **An operation marked `x-ai-safe: true` is not gated by anything** — declaring it produces a document that reads like a safety control and is not one, which is worse than declaring nothing. +> `x-ai-safe` appears nowhere in the generator: zero occurrences of the key and zero of any `aiSafe` identifier across the source, against controls in the same search that find `x-public` and `x-manual` in three files each. No kit Spectral rule enforces it either. **An operation marked `x-ai-safe: true` is not gated by anything** — declaring it produces a document that reads like a safety control and is not one, which is worse than declaring nothing. > -> The live mechanism for the same question is **`x-mcp.safeForAutoInvoke`** on an Arazzo scenario workflow (`Arazzo_Handbook.md` §4.8, parsed into the generator's `McpConfig`), which declares whether an AI agent may invoke a tool without human confirmation. It sits on the *workflow* rather than the operation, which is a real difference and a known open question — see `compatibility.md` §25. +> The live mechanism for the same question is **`x-effects`** (§4.7), from generator `0.24.0`. It sits on the operation, and the generator derives from it whether a call needs a human's confirmation. It replaces `x-mcp.safeForAutoInvoke`, which is retired (`Arazzo_Handbook.md` §4.8). > > The key is documented here rather than deleted because retiring an extension is the generator's call, not the kit's (`compatibility.md` §23). Until that answer arrives, treat this section as a record of what the key was meant to mean. @@ -3109,6 +3145,60 @@ Outside enum schemas, prefer the standard `default` keyword on its own. Use `x-d **What `default` means next to `required`** — including the `readOnly` case, where it becomes a persistence default rather than a client-side suggestion — is `API_Handbook.md` §1.9. The three diagnostics above are the enum-specific enforcement of that rule. + +### 4.7 x-effects + +**Purpose**: Declares what a write does beyond its own row, so the generator can derive whether a call needs a human's confirmation and how far it reaches. From generator `0.24.0`. + +**Scope**: A write operation. On a GET it is an error (`X_EFFECTS_ON_READ`): a read has no effects to declare. + +**Required**: No, but a write declaring none fails closed: it requires confirmation and is `destructiveHint: true`. + +```yaml +patch: + operationId: cancelBooking + x-effects: + reversibility: compensable + externalEffects: [email] + compensatedBy: reinstateBooking +``` + +| Key | Required | Values | +|---|---|---| +| `reversibility` | Yes | `reversible`, `compensable`, `irreversible` | +| `externalEffects` | Yes, `[]` when there are none | `dataExport`, `email`, `inAppNotification`, `payment`, `physicalDevice`, `push`, `sms`, `thirdParty` | +| `compensatedBy` | On `compensable` only | The `operationId` that undoes this one | +| `requiresConfirmation` | No | Only `true`. It can add a confirmation, never remove one. | + +**These facts hold for any caller**, whether an agent, a worker or a person in the app. That is why they live on the operation and not in the MCP manifest: enforcement inputs belong on the operation, presentation inputs in the manifest. + +**Getting the values right:** + +- **`reversibility` describes the business effect, not the database row.** A soft-deleted row that already sent notifications is `irreversible`. An update that changes one row and notifies every assignee is not low risk, whatever its structure suggests. +- **For an operation that starts an asynchronous job (a `202`), classify the job's eventual effect**, not the acknowledgment. Authors often get this case wrong. +- **`compensatedBy` is caller-relative.** It says an inverse exists in the system, not that this caller may invoke it. Where the caller cannot, the operation counts as `irreversible` for them. This only ever escalates. +- **`externalEffects: []` is a statement; leaving the key out is not.** The empty list is the reviewable "no effects" declaration, like `aiAccess: { operations: [] }`. `inAppNotification` stays inside your system. Everything else cannot be recalled, including `physicalDevice`: a corrective push may never reach an offline device. +- **Do not recompute the derivation in another consumer.** That is how an advertised hint and an enforced gate drift apart. Spectral does not check `x-effects` either; the generator owns it. + +**What the generator derives** (C#, rendered on each `OperationDescriptor` when a group registers `authorizationRuntime`): + +- `RequiresConfirmation`: `requiresConfirmation: true`, or `irreversible`, or any external effect other than `inAppNotification`, or `compensable` beyond `self`, or a write with no `x-effects`. +- `BlastRadius`: `self` for a `/me/` segment or `x-self-scoped`; otherwise the `tenancy` tier the target entity is or reaches through `belongsTo`, the widest when it reaches both; otherwise `platform`, including when the project declares no `tenancy`. +- `ReadOnlyHint` (GET), `DestructiveHint` (false for a GET and for a `reversible` write with `externalEffects: []`, true for every other write) and `IdempotentHint` (GET, PUT, DELETE). + +| Code (all ERROR) | Fires when | +|---|---| +| `X_EFFECTS_UNKNOWN_KEY` | A key outside the table above | +| `X_EFFECTS_REVERSIBILITY_INVALID` | Absent, or outside the three values | +| `X_EFFECTS_EXTERNAL_EFFECTS_MISSING` | `externalEffects` is absent | +| `X_EFFECTS_EXTERNAL_EFFECT_UNKNOWN` / `X_EFFECTS_EXTERNAL_EFFECT_DUPLICATE` | A value outside the set, or listed twice | +| `X_EFFECTS_COMPENSATED_BY_MISSING` | `compensable` without `compensatedBy` | +| `X_EFFECTS_COMPENSATED_BY_FORBIDDEN` | `compensatedBy` on an operation that is not `compensable` | +| `X_EFFECTS_COMPENSATED_BY_UNRESOLVED` | It names no `operationId` in the spec | +| `X_EFFECTS_REVERSIBLE_ESCAPING_EFFECT` | `reversible` with an effect other than `inAppNotification` | +| `X_EFFECTS_REQUIRES_CONFIRMATION_NOT_TRUE` | `requiresConfirmation` is anything but `true` | +| `X_EFFECTS_ON_READ` | Declared on a GET | + --- ## 5. Operation Categorization Extensions @@ -4059,7 +4149,7 @@ When present, the receiving operation's `x-subscription` should set `requiresSes x-trigger-when: "After.status == 'archived' && Before.status != 'archived'" ``` -**Why**: The action class of an event is inferred from its name suffix. State-transition events (anything not `*Created`/`*Updated`/`*Deleted`) need a declarative predicate so the generator-emitted service code knows *when* to emit the event during a save. The predicate is evaluated against `EntityEntry.OriginalValues` (Before) and `EntityEntry.CurrentValues` (After) before `SaveChangesAsync`. When matched, the transition event fires AND the corresponding `*Updated` is suppressed (mutual exclusivity). +**Why**: The action class of an event is inferred from its name suffix. State-transition events (anything not `*Created`/`*Updated`/`*Deleted`) need a declarative predicate so the generator-emitted service code knows *when* to emit the event during a save. On the generated C# update and replace paths, the predicate is evaluated against `EntityEntry.OriginalValues` (Before) and `EntityEntry.CurrentValues` (After) before `SaveChangesAsync`; on an explicit or hand-written path it is the caller's precondition (`AsyncAPI_Handbook.md`, `x-trigger-when`). When matched, the transition event fires AND the corresponding `*Updated` is suppressed (mutual exclusivity). **Predicate grammar (authoritative):** @@ -4118,6 +4208,28 @@ x-trigger-mode: explicit x-method-name: CancelOrder # imperative form of OrderCancelled ``` +#### x-envelope + +**Purpose**: Binds a message header to an event-envelope role, so the generated runtime reads and writes that role under the name your trait declares. From generator `0.24.0`. + +**Scope**: A header property in a message's `headers`, usually through a trait. Traits apply in order, and the message's own `headers` win. + +**Roles** (closed): `eventId`, `eventVersion`, `messageType`, `correlationId`, `causationId`, `traceparent`, `userId`, `tenant`, `subTenant`, `aggregateId`, `aggregateType`, `aggregateVersion`, `producedAt`. + +```yaml +headers: + type: object + required: [eventId, accountId] + properties: + eventId: { type: string, format: uuid } + accountId: + type: string + format: uuid + x-envelope: tenant +``` + +A header with no marker binds a role when its name is the role's canonical name, which for `tenant` and `subTenant` is the camelCased `tenancy.*.foreignKey`. The findings and the migration for an older trait are in `AsyncAPI_Handbook.md` §2.1. + #### x-envelope-promote **Purpose**: Marks a snapshot field that should also be stamped as an envelope ApplicationProperty so subscription filters can target it without inspecting the payload. **Scope**: A property schema inside a snapshot file (e.g., `events/NotificationJobSnapshot.yaml`) @@ -4492,39 +4604,7 @@ x-domain: order #### x-mcp -**Purpose**: Declares whether a scenario workflow is exposed as an MCP (Model Context Protocol) tool for AI orchestrators. Inputs and outputs are derived from the workflow — not redeclared in `x-mcp`. - -**Scope**: Arazzo workflow (scenario only) - -**Required**: No (default: not exposed) - -**Schema**: `schemas/arazzo-extensions/x-mcp.schema.json` - -**Shape**: -```yaml -x-mcp: - exposed: true - toolName: request-refund - description: "Request a refund for a completed order" - requiresActorAuth: true - safeForAutoInvoke: false -``` - -| Field | Required | Type | Description | -|-------|----------|------|-------------| -| `exposed` | Yes | boolean | Explicit opt-in for MCP tool exposure. Default: `false` | -| `toolName` | When exposed | string | kebab-case tool name, globally unique across all scenarios | -| `description` | When exposed | string | MCP-facing description of what the tool does | -| `requiresActorAuth` | No | boolean | Whether the tool requires actor authentication context. Default: `true` | -| `safeForAutoInvoke` | No | boolean | Whether an AI agent can invoke without human confirmation. Default: `false` | - -**Rules**: -- When `exposed: true`, both `toolName` and `description` become required -- `toolName` must be globally unique across all scenarios in the repository (Spectral-enforced) -- `safeForAutoInvoke: true` should only be set for read-only, low-risk scenarios -- Forbidden on recipe workflows — recipes are infrastructure, not user-facing tools - -**Rationale**: MCP tools allow AI orchestrators to discover and execute multi-step API workflows. Explicit opt-in (`exposed: false` default) prevents accidental exposure. Global `toolName` uniqueness is enforced from Phase 0 to prevent conflict accumulation before the MCP runtime ships (Phase 7). +**Retired, generator `0.24.0`. Delete the block.** Nothing reads it, and a document that still declares it gets one `X_MCP_RETIRED` warning (`specfuse-arazzo-x-mcp-retired` in the kit's Spectral). Tool exposure moves to the MCP manifest (`Project_File.md` §1.5), `safeForAutoInvoke` to `x-effects` (§4.7), and `requiresActorAuth` is derived from `x-actors`. **See also**: `Arazzo_Handbook.md §4.8`, `Arazzo_Handbook.md §11` @@ -4561,7 +4641,7 @@ x-recipe: - Recipe composition via `extends` has a max chain depth of 6 - No cycles in the `extends` chain - No output namespace collisions between parent and child recipes -- Recipes must not declare `x-actors`, `x-as`, `x-setup`, or `x-mcp` +- Recipes must not declare `x-actors`, `x-as`, or `x-setup` **Rationale**: Recipes provide the foundation for reproducible test execution — they provision the entities, relationships, and state that scenarios exercise. The discriminator-by-presence pattern avoids a separate `x-category` enum and lets file paths confirm classification visually. diff --git a/plugins/specfuse-authoring/agents/scenario-architect.md b/plugins/specfuse-authoring/agents/scenario-architect.md index bec7114..7c3ebaf 100644 --- a/plugins/specfuse-authoring/agents/scenario-architect.md +++ b/plugins/specfuse-authoring/agents/scenario-architect.md @@ -52,7 +52,6 @@ The `/design-scenario` command MUST provide the following context when spawning | `failureModes` | Explicitly elicited failure paths (negative tests, edge cases) | | `uiActions` | Natural-language UI actions and expected visual outcomes per step | | `tags` | Suggested tags (e.g., `critical-path`) | -| `mcpExposure` | Whether this scenario should be exposed as an MCP tool | | `setupRecipe` | Pre-selected recipe file stem; if absent, agent selects from `existingRecipes` | | `setupInputs` | Values to pass to the recipe's inputs | @@ -207,12 +206,6 @@ x-doc: personas: [{persona-list}] # Derived from actor descriptions businessOutcome: "{why-this-matters}" # Derived from intent -x-mcp: - exposed: false # Default false unless mcpExposure input says otherwise - # When exposed: true, also generate: - # toolName: {domain}-{kebab-case-verb-phrase} # Globally unique - # description: "{MCP-facing description}" - x-actors: {actorKey}: role: {Role} @@ -231,7 +224,7 @@ Rules for this section: - `x-version` is REQUIRED. Always `current: 1`, `status: draft` for new scenarios. - `x-domain` is REQUIRED. Must match the confirmed domain. - `x-doc` is REQUIRED. Derive `summary` from the PM's intent. -- `x-mcp` is REQUIRED. Default to `exposed: false`. When `exposed: true`, `toolName` must be kebab-case and globally unique. +- Do NOT write `x-mcp`. It is retired (generator 0.24.0) and exposes no tool. - `x-actors` is REQUIRED on scenarios. Every actor key is camelCase. Every role is from the closed set. Bind actors to recipe outputs via `ref: $setup.outputs.X` when a recipe is used. - `x-setup` is OPTIONAL but strongly recommended. Omit only if the scenario truly needs no pre-seeded fixtures. @@ -445,7 +438,6 @@ The generated file MUST satisfy all of the following: | `x-version` | Document | Always | `current` (integer) + `status` (draft/stable/deprecated) | | `x-domain` | Document | Always | Valid project domain or `cross-domain` | | `x-doc` | Document | Always | At minimum `summary` | -| `x-mcp` | Document | Always | At minimum `exposed: false` | | `x-actors` | Document | Scenarios only | At least one actor with valid role | | `x-setup` | Document | When fixtures needed | `recipe` field is required within | | `x-as` | Step | All scenario steps | Valid `$actorKey` reference | @@ -574,7 +566,6 @@ Before returning the generated YAML, verify every item: - [ ] `x-version` present with `current: 1` and `status: draft` - [ ] `x-domain` present and valid - [ ] `x-doc.summary` present and derived from PM intent -- [ ] `x-mcp.exposed` explicitly set (default: `false`) - [ ] `x-actors` present with at least one actor, all roles from closed set - [ ] `x-setup.recipe` set when fixtures are needed; recipe exists in inventory - [ ] Every step has `stepId` (kebab-case) and `x-as` (valid actor reference) diff --git a/plugins/specfuse-authoring/agents/scenario-reviewer.md b/plugins/specfuse-authoring/agents/scenario-reviewer.md index 4972b88..dbb0cd2 100644 --- a/plugins/specfuse-authoring/agents/scenario-reviewer.md +++ b/plugins/specfuse-authoring/agents/scenario-reviewer.md @@ -101,7 +101,7 @@ Verify that all required extensions are present and well-formed. | `x-domain` | Yes | A valid project domain or `cross-domain` | | `x-actors` | Yes | At least one actor; each has `role` registered in the OpenAPI `info.x-roles` | | `x-doc` | Yes | At minimum `summary` present | -| `x-mcp` | Yes | `exposed` explicitly set | +| No `x-mcp` | Retired | Delete it (`X_MCP_RETIRED`); nothing reads it | | `x-setup` | Recommended | `recipe` field present when fixtures are needed | | `x-as` on every step | Yes | Every step has actor binding | @@ -114,7 +114,7 @@ Verify that all required extensions are present and well-formed. | `x-domain` | Yes | Same as scenarios | | No `x-actors` | Forbidden | Recipes run as implicit `$system` | | No `x-setup` | Forbidden | Recipes compose via `extends` | -| No `x-mcp` | Forbidden | Recipes are not MCP tools | +| No `x-mcp` | Retired | Delete it (`X_MCP_RETIRED`); nothing reads it | | No `x-as` on steps | Forbidden | Recipe steps run as `$system` | | No `x-ui` on steps | Forbidden | No user-facing interactions in recipes | diff --git a/plugins/specfuse-authoring/agents/scenario-validator.md b/plugins/specfuse-authoring/agents/scenario-validator.md index 3f11266..d8a3e22 100644 --- a/plugins/specfuse-authoring/agents/scenario-validator.md +++ b/plugins/specfuse-authoring/agents/scenario-validator.md @@ -170,9 +170,7 @@ Every error is classified as **auto-fixable** (mechanical, unambiguous fix) or * | Issue | Proposed Fix | |---|---| | Missing `x-version` | Add `x-version: { current: 1, status: draft }` | -| Missing `x-mcp` | Add `x-mcp: { exposed: false }` | -| Missing `x-mcp.exposed` | Add `exposed: false` | -| `x-mcp.toolName` not kebab-case | Convert to kebab-case | +| `x-mcp` present | Delete the block (`X_MCP_RETIRED`; nothing reads it) | | `x-as` missing `$` prefix | Add `$` prefix | | ISO duration format wrong | Fix format (e.g., `10s` -> `PT10S`) | | `x-ui.platform` casing wrong | Fix casing (e.g., `Web` -> `web`) | @@ -197,7 +195,6 @@ Every error is classified as **auto-fixable** (mechanical, unambiguous fix) or * | `x-recipe.extends` depth > 6 | Composition restructuring needed. | | >= 80% step overlap with existing scenario | Granularity decision: merge, split, or override. | | 60-80% step overlap | Ambiguous zone -- user decides. | -| `x-mcp.toolName` collision | Renaming decision requires domain context. | | `x-emits` at document root | Structural misunderstanding -- user needs to use `x-async.emit`. | | Recipe/scenario extension mixing | File purpose is ambiguous -- user decides. | | File homogeneity violation | Workflows mixed categories -- user must restructure. | @@ -295,12 +292,6 @@ Every error is classified as **auto-fixable** (mechanical, unambiguous fix) or * |---|---|---| | E13: Step overlap < 80% (error), 60-80% (review) | Scenario overlap | No -- granularity is a design decision | -#### Group F -- Global Uniqueness (1 check) - -| Check | Category | Auto-Fixable? | -|---|---|---| -| F14: `x-mcp.toolName` unique | Name collision | No -- renaming requires domain context | - --- ## "Did You Mean?" Suggestions diff --git a/plugins/specfuse-authoring/skills/design-recipe/SKILL.md b/plugins/specfuse-authoring/skills/design-recipe/SKILL.md index 718a1f6..00b3a2d 100644 --- a/plugins/specfuse-authoring/skills/design-recipe/SKILL.md +++ b/plugins/specfuse-authoring/skills/design-recipe/SKILL.md @@ -33,7 +33,6 @@ Recipes are Arazzo workflows with the `x-recipe` extension. They are NOT scenari - `x-actors` -- recipes run as the implicit `$system` actor mapped to `Admin` - `x-as` -- every recipe step runs as `$system` (no actor binding on steps) - `x-setup` -- recipes compose via `extends`, not `$setup` -- `x-mcp` -- recipes are infrastructure, not user-facing tools - `x-ui` -- no user-facing interactions in fixture setup ## Input clarification @@ -199,7 +198,7 @@ These are validator-enforced. The command must check them before generating: | Max depth 6 | Count the full chain from root to this recipe. Depth > 6 = error. | | No cycles | A recipe cannot extend itself or form a circular chain (A -> B -> A) | | No output collisions | If a parent and child both declare an output with the same name, the validator errors. The flat namespace keeps `$setup.outputs.X` unambiguous. | -| No scenario extensions | Recipes must NOT have `x-actors`, `x-mcp`, `x-setup`, or `x-ui` | +| No scenario extensions | Recipes must NOT have `x-actors`, `x-setup`, or `x-ui` | | Implicit `$system` actor | All steps run as `$system` mapped to `Admin`. No `x-as` on steps. | | `$setup` forbidden | Recipes cannot reference `$setup`. Use `$steps.parent-setup.outputs.X` for parent outputs. | @@ -231,7 +230,7 @@ Verify all of these before reporting completion: - [ ] No output namespace collisions with parent outputs - [ ] Output contract clearly documented (both new outputs and inherited ones noted) - [ ] `sourceDescriptions[].url` is a correct relative path to `openapi.yaml` -- [ ] No forbidden extensions present (`x-actors`, `x-mcp`, `x-setup`, `x-ui`, `x-as`) +- [ ] No forbidden extensions present (`x-actors`, `x-setup`, `x-ui`, `x-as`) - [ ] No hardcoded dates -- date tokens used in inputs and step payloads - [ ] No language-specific references - [ ] Validation passes with zero errors diff --git a/plugins/specfuse-authoring/skills/design-scenario/SKILL.md b/plugins/specfuse-authoring/skills/design-scenario/SKILL.md index 83dfc58..c2b490e 100644 --- a/plugins/specfuse-authoring/skills/design-scenario/SKILL.md +++ b/plugins/specfuse-authoring/skills/design-scenario/SKILL.md @@ -207,7 +207,7 @@ Spawn the `scenario-validator` subagent (provided by the specfuse-authoring plug 3. Returns a structured validation report Process the report: -- **Auto-fix mechanical issues silently**: missing `x-version` defaults, missing `x-mcp: { exposed: false }`, casing fixes, ISO duration format corrections. Apply these to the YAML without asking the user. +- **Auto-fix mechanical issues silently**: missing `x-version` defaults, deleting a retired `x-mcp` block, casing fixes, ISO duration format corrections. Apply these to the YAML without asking the user. - **Surface judgment calls to the user**: unresolved operationIds, invalid actor roles, event name mismatches, granularity alerts. Present each issue and ask how to proceed. - **Re-validate after fixes** until the report shows zero errors. @@ -278,7 +278,7 @@ Verify all of these before reporting completion: - [ ] `x-version` present with `current: 1` and `status: draft` - [ ] `x-domain` present and matches the confirmed domain - [ ] `x-doc.summary` present and PM-readable -- [ ] `x-mcp.exposed` explicitly set +- [ ] No `x-mcp` block (retired at generator 0.24.0) - [ ] `x-actors` present with at least one actor, all roles from the closed set - [ ] `x-as` present on every scenario step - [ ] `x-setup` recipe exists and provides all needed outputs (or gap flagged) diff --git a/plugins/specfuse-authoring/skills/update-scenario/SKILL.md b/plugins/specfuse-authoring/skills/update-scenario/SKILL.md index 9e8ba68..998dfdd 100644 --- a/plugins/specfuse-authoring/skills/update-scenario/SKILL.md +++ b/plugins/specfuse-authoring/skills/update-scenario/SKILL.md @@ -46,7 +46,7 @@ Ask the user: > - **C.** Add step(s) to a workflow > - **D.** Remove step(s) from a workflow > - **E.** Change actors (add, remove, rename, change role) - > - **F.** Update extensions (x-doc, x-mcp, x-ui, tags, x-setup, etc.) + > - **F.** Update extensions (x-doc, x-ui, tags, x-setup, etc.) > - **G.** Version bump or deprecation The user may also describe the change in plain language. Map it to the appropriate handler. @@ -184,16 +184,16 @@ Add, remove, rename, or change the role of actors. Update any workflow-level or step-level extension value. **Clarify:** -- Which extension to update (e.g., `x-doc.summary`, `x-mcp.exposed`, `tags`, `x-setup.recipe`, step-level `x-ui`). +- Which extension to update (e.g., `x-doc.summary`, `tags`, `x-setup.recipe`, step-level `x-ui`). - The new value. **Apply:** 1. Read the current value and present it for reference. 2. Apply the update. 3. For `x-setup.recipe` changes: verify the new recipe exists and provides the outputs the scenario needs. Check that all `$setup.outputs.X` references in `x-actors.*.ref` and step parameters still resolve. -4. For `x-mcp.exposed: true`: ensure `toolName` and `description` are provided. Verify `toolName` is globally unique by scanning other scenario files. +4. If the file declares `x-mcp`, delete the block: it is retired at generator 0.24.0 and exposes no tool. Tool exposure lives in the MCP manifest. 5. For `tags` changes: note that adding `critical-path` means this scenario will block PR merges once test generation is in place. -6. Extension-only updates (typos, description rewording) do NOT warrant a version bump. Semantic changes (different recipe, changed `x-mcp` exposure) DO. +6. Extension-only updates (typos, description rewording) do NOT warrant a version bump. Semantic changes (different recipe) DO. --- @@ -259,7 +259,6 @@ After applying changes, determine whether a version bump is warranted. Present t - `x-doc` text changes (summary, tutorialNote, businessOutcome) - `x-ui` action text rewording - Tag additions -- `x-mcp` description changes - Typo fixes in descriptions or summaries Present the recommendation: diff --git a/pyproject.toml b/pyproject.toml index b6492a0..ece3500 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "specfuse-authoring" -version = "0.31.0" +version = "0.32.0" description = "Specfuse authoring kit: handbooks, samples, Claude assets, project bootstrap, and the generator launcher." readme = "README.md" requires-python = ">=3.10" diff --git a/samples/recipe-samples.yaml b/samples/recipe-samples.yaml index 726ff92..25cead6 100644 --- a/samples/recipe-samples.yaml +++ b/samples/recipe-samples.yaml @@ -59,7 +59,6 @@ # x-actors — recipes run as implicit $system mapped to the project's # highest-privileged role # x-setup — recipes compose via 'extends', not $setup -# x-mcp — recipes are infrastructure, not user-facing tools # x-as — every recipe step runs as $system (no actor binding) # x-ui — no user-facing interactions in fixture setup # @@ -146,7 +145,7 @@ x-recipe: estimatedDurationMs: 500 scope: scenario -# NOTE: No x-actors, no x-setup, no x-mcp — these are FORBIDDEN on recipes. +# NOTE: No x-actors, no x-setup — these are FORBIDDEN on recipes. # Recipes execute as the implicit $system actor mapped to the project's # highest-privileged role (typically Admin). diff --git a/samples/scenario-samples.yaml b/samples/scenario-samples.yaml index 8f438ad..3525990 100644 --- a/samples/scenario-samples.yaml +++ b/samples/scenario-samples.yaml @@ -17,7 +17,7 @@ # Schema: schemas/arazzo-extensions/specfuse-arazzo-combined.schema.json # # Extensions covered in this file: -# x-version, x-domain, x-doc, x-mcp, x-actors, x-setup, x-as, +# x-version, x-domain, x-doc, x-actors, x-setup, x-as, # x-async (emit, await, poll), x-ui, x-recipe # # Extensions that DO NOT exist as standalone — by design: @@ -130,24 +130,6 @@ x-doc: Self-service refund initiation reduces support burden while maintaining oversight and constraint validation. -# --------------------------------------------------------------------------- -# x-mcp (optional, scenario workflows only) — See handbook §4.8 / §11 -# --------------------------------------------------------------------------- -# Declares whether this scenario is exposed as an MCP tool for AI -# orchestrators. Explicit opt-in; default is false. -# -# Fields: -# exposed — boolean (required). Must be explicitly set. -# toolName — kebab-case, globally unique across all scenarios -# description — MCP-facing description -# requiresActorAuth — whether actor auth context is needed (default: true) -# safeForAutoInvoke — can AI invoke without human confirmation? (default: false) -# -# Inputs/outputs are DERIVED from the workflow's inputs/outputs — -# never redeclared in x-mcp. -x-mcp: - exposed: false # Explicit opt-out for this sample - # --------------------------------------------------------------------------- # x-actors (REQUIRED on scenario workflows, forbidden on recipes) # See handbook §4.6 @@ -526,9 +508,6 @@ x-doc: Self-service refund initiation reduces support burden while maintaining constraint validation and management oversight. -x-mcp: - exposed: false - x-actors: customer: role: Customer @@ -716,7 +695,7 @@ workflows: # - x-actors (required on scenario documents) # - workflows with at least one step using x-as # -# Everything else (x-doc, x-mcp, x-setup, x-ui, x-async, tags, +# Everything else (x-doc, x-setup, x-ui, x-async, tags, # onFailure) is optional and can be added incrementally. # ============================================================================= --- @@ -832,9 +811,6 @@ x-doc: businessOutcome: >- Prevents clutter from abandoned draft orders. -x-mcp: - exposed: false - # Scheduled-job scenarios: the actor is an OBSERVER, not an initiator. # The job runs as the system. The observer verifies results. x-actors: @@ -951,9 +927,6 @@ x-setup: inputs: orderPlacedDate: "@today-1d" -x-mcp: - exposed: false - workflows: # ========================================================================= @@ -1094,7 +1067,6 @@ workflows: # - x-actors FORBIDDEN (recipes run as implicit $system mapped to the project's # highest-privileged role) # - x-setup FORBIDDEN (recipes compose via 'extends', not $setup) -# - x-mcp FORBIDDEN (recipes are infrastructure, not user-facing tools) # - x-as FORBIDDEN on steps (every step runs as $system) # - x-ui FORBIDDEN on steps (no user-facing interactions) # diff --git a/schemas/arazzo-extensions/specfuse-arazzo-combined.schema.json b/schemas/arazzo-extensions/specfuse-arazzo-combined.schema.json index 33f06d8..e4e9aeb 100644 --- a/schemas/arazzo-extensions/specfuse-arazzo-combined.schema.json +++ b/schemas/arazzo-extensions/specfuse-arazzo-combined.schema.json @@ -20,7 +20,8 @@ }, "x-mcp": { "$ref": "x-mcp.schema.json", - "description": "MCP tool exposure declaration. Only valid on scenario documents." + "deprecated": true, + "description": "Retired at generator 0.24.0 (X_MCP_RETIRED): nothing reads it. Delete it." }, "x-actors": { "$ref": "x-actors.schema.json", @@ -74,12 +75,11 @@ "description": "Reference to an actor defined in x-actors, using the $actorName convention" }, "recipeDocumentRules": { - "description": "Rules for recipe documents: x-actors, x-setup, and x-mcp are forbidden. Steps must not use x-as or x-ui.", + "description": "Rules for recipe documents: x-actors and x-setup are forbidden. Steps must not use x-as or x-ui.", "not": { "anyOf": [ { "required": ["x-actors"] }, - { "required": ["x-setup"] }, - { "required": ["x-mcp"] } + { "required": ["x-setup"] } ] }, "properties": { diff --git a/schemas/arazzo-extensions/x-mcp.schema.json b/schemas/arazzo-extensions/x-mcp.schema.json index f5e9e55..c142306 100644 --- a/schemas/arazzo-extensions/x-mcp.schema.json +++ b/schemas/arazzo-extensions/x-mcp.schema.json @@ -2,7 +2,8 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.specfuse.dev/arazzo/x-mcp.schema.json", "title": "x-mcp", - "description": "Declares whether an Arazzo scenario workflow is exposed as an MCP tool for AI orchestrators. Inputs and outputs are derived from the workflow — not redeclared here. Optional on scenario workflows, forbidden on recipes. See Arazzo Handbook §4.8 and §11.", + "deprecated": true, + "description": "Retired at generator 0.24.0 (X_MCP_RETIRED): nothing reads this block, and a spec declaring it gets one warning. Delete it. Tool exposure moves to the MCP manifest; safeForAutoInvoke moves to x-effects. See Arazzo Handbook §4.8.", "type": "object", "required": ["exposed"], "properties": { diff --git a/schemas/spectral/fixtures/classification.yaml b/schemas/spectral/fixtures/classification.yaml index 44908cf..a6be52c 100644 --- a/schemas/spectral/fixtures/classification.yaml +++ b/schemas/spectral/fixtures/classification.yaml @@ -15,6 +15,7 @@ # taxId [pii] + x-protection.atRest: encrypted clean # creditScore [sensitive] clean # avatarHash [exposed] + a description clean +# guestNote [untrusted], provenance not protection clean # emails an array named like PII but NOT an exact # match for the jar's narrow name list clean — must # stay clean, this is the case the narrow list exists for @@ -110,6 +111,10 @@ components: safe on responses despite the secret-shaped name. x-classification: [exposed] x-protection: { atRest: none, rationale: A public content address; nothing to protect. } + guestNote: + type: string + x-classification: [untrusted] + x-protection: { atRest: none, rationale: Fixture control. } emails: type: array items: { type: string } diff --git a/schemas/spectral/fixtures/given-coverage-arazzo.yaml b/schemas/spectral/fixtures/given-coverage-arazzo.yaml index c601d57..4d5531e 100644 --- a/schemas/spectral/fixtures/given-coverage-arazzo.yaml +++ b/schemas/spectral/fixtures/given-coverage-arazzo.yaml @@ -24,9 +24,9 @@ sourceDescriptions: url: ../../openapi.yaml type: openapi -# The x-mcp block is document-level and hello-orders declares none. +# The retired x-mcp block is document-level and hello-orders declares none. x-mcp: - toolName: coverage-tool + exposed: false workflows: - workflowId: coverageWorkflow diff --git a/schemas/spectral/fixtures/xentity-shape-keys.yaml b/schemas/spectral/fixtures/xentity-shape-keys.yaml index d993329..59a8aed 100644 --- a/schemas/spectral/fixtures/xentity-shape-keys.yaml +++ b/schemas/spectral/fixtures/xentity-shape-keys.yaml @@ -68,6 +68,15 @@ # ValueObjectProtectionUnknownSubKey flagged: typo'd sub-key matches neither shape # ValueObjectProtectionMemberNotObject flagged: a member's value must be a declaration # +# Generator 0.24.0 adds `fieldSets` and the `untrusted` classification token: +# +# FieldSetsDeclared no finding — named sets, dotted entry allowed +# ValueObjectClassifiedUntrusted no finding — `untrusted` at the usage site +# FieldSetsEmpty flagged: an empty block (ENTITY_FIELDSETS_SHAPE_INVALID) +# FieldSetBadName flagged: name outside the jar's pattern +# FieldSetNotAList flagged: a set is a list of property names +# FieldSetNoEntries flagged: a set with no entries +# # And, by `specfuse-xentity-concurrency-unprotected-needs-reason` only — these # two must NOT appear in any specfuse-xentity-shape finding: # @@ -339,6 +348,62 @@ components: # additionalProperties: false, so each shape the jar accepts and the kit # does not is a spec that lints clean nowhere and generates fine. # See handbooks/Vendor_Extensions.md §2.1. + FieldSetsDeclared: + type: object + x-entity: + domain: orders + type: aggregate + fieldSets: + summary: [id, title.en] + properties: + id: { type: string, format: uuid } + ValueObjectClassifiedUntrusted: + type: object + x-entity: + domain: orders + type: aggregate + valueObjects: + residenceAddress: + storage: 'flatten' + queryable: [] + classification: [untrusted] + properties: + id: { type: string, format: uuid } + FieldSetsEmpty: + type: object + x-entity: + domain: orders + type: aggregate + fieldSets: {} + properties: + id: { type: string, format: uuid } + FieldSetBadName: + type: object + x-entity: + domain: orders + type: aggregate + fieldSets: + Summary: [id] + properties: + id: { type: string, format: uuid } + FieldSetNotAList: + type: object + x-entity: + domain: orders + type: aggregate + fieldSets: + summary: id + properties: + id: { type: string, format: uuid } + FieldSetNoEntries: + type: object + x-entity: + domain: orders + type: aggregate + fieldSets: + summary: [] + properties: + id: { type: string, format: uuid } ValueObjectProtectionScalar: type: object x-entity: diff --git a/schemas/spectral/functions/arazzoRecipeScenarioExclusion.js b/schemas/spectral/functions/arazzoRecipeScenarioExclusion.js index b92e19e..859fd63 100644 --- a/schemas/spectral/functions/arazzoRecipeScenarioExclusion.js +++ b/schemas/spectral/functions/arazzoRecipeScenarioExclusion.js @@ -6,18 +6,16 @@ // When x-recipe is present (at document root or workflow level), the following // must not appear: // - x-actors (recipes use implicit $system actor) -// - x-mcp (recipes are not MCP tools) // - x-setup (recipes compose via extends, not $setup) // - x-ui on steps (UI hints are for scenarios only) // // Handbook: Section 3.3, 4.6, 4.8, 7.4, 13 Do NOT #6, #8, #18 // Given: $ -const SCENARIO_ONLY_FIELDS = ["x-actors", "x-mcp", "x-setup"]; +const SCENARIO_ONLY_FIELDS = ["x-actors", "x-setup"]; const FIELD_REASONS = { "x-actors": "Recipes execute as the implicit $system actor (mapped to Admin).", - "x-mcp": "Recipes are infrastructure, not user-facing MCP tools.", "x-setup": "Recipes compose via 'extends', not $setup. Only scenarios reference recipes via $setup.", }; diff --git a/schemas/spectral/rule-renames.yaml b/schemas/spectral/rule-renames.yaml index e020bd1..24a8386 100644 --- a/schemas/spectral/rule-renames.yaml +++ b/schemas/spectral/rule-renames.yaml @@ -346,7 +346,7 @@ rulesets: arazzo: kit: schemas/spectral/specfuse-arazzo.yaml - # 22 pure prefix swaps + 0 non-mechanical = 22 mapped; 2 retained. + # 19 pure prefix swaps + 0 non-mechanical = 19 mapped; 2 retained; 3 retired. renames: rm-arazzo-as-actor-exists: specfuse-arazzo-as-actor-exists rm-arazzo-as-format: specfuse-arazzo-as-format @@ -356,9 +356,6 @@ rulesets: rm-arazzo-domain-required: specfuse-arazzo-domain-required rm-arazzo-emits-forbidden: specfuse-arazzo-emits-forbidden rm-arazzo-file-homogeneity: specfuse-arazzo-file-homogeneity - rm-arazzo-mcp-exposed-required: specfuse-arazzo-mcp-exposed-required - rm-arazzo-mcp-fields-when-exposed: specfuse-arazzo-mcp-fields-when-exposed - rm-arazzo-mcp-toolname-format: specfuse-arazzo-mcp-toolname-format rm-arazzo-recipe-purpose-enum: specfuse-arazzo-recipe-purpose-enum rm-arazzo-recipe-scenario-exclusion: specfuse-arazzo-recipe-scenario-exclusion rm-arazzo-recipe-scope-enum: specfuse-arazzo-recipe-scope-enum @@ -385,4 +382,12 @@ rulesets: is the project's. # No rule has been deleted without a successor on this surface yet. - retired: [] + retired: + rm-arazzo-mcp-exposed-required: &x-mcp-retired + note: >- + Generator 0.24.0 retired Arazzo x-mcp and reads nothing in the block, + so no shape rule survives it. `specfuse-arazzo-x-mcp-retired` warns on + any x-mcp block, but is no successor: it fires on a block these rules + passed. + rm-arazzo-mcp-fields-when-exposed: *x-mcp-retired + rm-arazzo-mcp-toolname-format: *x-mcp-retired diff --git a/schemas/spectral/specfuse-arazzo.yaml b/schemas/spectral/specfuse-arazzo.yaml index 0eb7378..50667c7 100644 --- a/schemas/spectral/specfuse-arazzo.yaml +++ b/schemas/spectral/specfuse-arazzo.yaml @@ -6,7 +6,7 @@ # # This ruleset validates: # - Document-level extensions (x-version, x-domain, x-emits guard) -# - Workflow-level extensions (x-actors, x-setup, x-mcp, x-recipe) +# - Workflow-level extensions (x-actors, x-setup, x-recipe) # - Step-level extensions (x-as, x-async, x-ui) # - Cross-concern rules (recipe/scenario exclusion, file homogeneity) # @@ -168,45 +168,20 @@ rules: field: recipe function: truthy - specfuse-arazzo-mcp-exposed-required: - description: "When x-mcp is present, the 'exposed' field is required" - message: "x-mcp is missing 'exposed'. Set exposed: true to expose as an MCP tool, or exposed: false to opt out. See Arazzo Handbook §4.8." - severity: error + # Generator 0.24.0 retired Arazzo x-mcp: the jar raises X_MCP_RETIRED once per + # document and reads nothing, even when the block is malformed. A warning, so + # this rule stays level with the jar. The three shape rules it replaces would + # have failed lint on a block nothing reads. + specfuse-arazzo-x-mcp-retired: + description: "x-mcp is retired (generator 0.24.0): nothing reads it" + message: "x-mcp is retired and exposes no tool. Delete it; tool exposure moves to the MCP manifest and safeForAutoInvoke to x-effects. See Arazzo Handbook §4.8." + severity: warn given: $.x-mcp then: - field: exposed function: defined - specfuse-arazzo-mcp-fields-when-exposed: - description: "When x-mcp.exposed is true, toolName and description are required" - message: "x-mcp.exposed is true but 'toolName' and/or 'description' are missing. Both are required when a scenario is exposed as an MCP tool. See Arazzo Handbook §4.8." - severity: error - given: $.x-mcp - then: - function: schema - functionOptions: - schema: - if: - properties: - exposed: - const: true - then: - required: - - toolName - - description - - specfuse-arazzo-mcp-toolname-format: - description: "x-mcp.toolName must be kebab-case" - message: "x-mcp.toolName '{{value}}' must be kebab-case (e.g., 'place-order', 'get-order-status')." - severity: error - given: $.x-mcp.toolName - then: - function: pattern - functionOptions: - match: "^[a-z][a-z0-9]*(-[a-z0-9]+)*$" - specfuse-arazzo-recipe-scenario-exclusion: - description: "x-recipe must not coexist with scenario-only extensions (x-actors, x-mcp, x-setup, x-ui)" + description: "x-recipe must not coexist with scenario-only extensions (x-actors, x-setup, x-ui)" message: "{{error}}" severity: error given: $ diff --git a/schemas/spectral/specfuse-openapi.yaml b/schemas/spectral/specfuse-openapi.yaml index ae21b46..4137e68 100644 --- a/schemas/spectral/specfuse-openapi.yaml +++ b/schemas/spectral/specfuse-openapi.yaml @@ -1226,7 +1226,7 @@ rules: uniqueItems: true items: type: string - enum: [pii, sensitive, confidential, exposed, financial, credential, cardholder, sad] + enum: [pii, sensitive, confidential, exposed, financial, credential, cardholder, sad, untrusted] # Anchored at its first use in document order and aliased # by `specfuse-xprotection-shape` below. One definition, two # call sites — a YAML alias cannot drift the way two @@ -1529,6 +1529,22 @@ rules: # forbidden characters `" \ :`, and `where` on an encrypted # property. Those are generator `validate` and build-time checks; # Vendor_Extensions.md lists them. + # Generator 0.24.0: a read sub-key naming the subsets of properties + # an MCP tool may return. The jar resolves each entry and checks it + # against aiAccess; this guard holds the shape it also rejects + # (ENTITY_FIELDSETS_SHAPE_INVALID, ENTITY_FIELDSET_NAME_INVALID, + # ENTITY_FIELDSET_VALUE_INVALID, ENTITY_FIELDSET_EMPTY, + # ENTITY_FIELDSET_PROPERTY_DUPLICATE). + fieldSets: + type: object + minProperties: 1 + additionalProperties: false + patternProperties: + "^[a-z][A-Za-z0-9]*$": + type: array + minItems: 1 + uniqueItems: true + items: { type: string } uniqueBy: type: array uniqueItems: true @@ -2457,7 +2473,7 @@ rules: specfuse-classification-values: description: >- x-classification must be a non-empty array drawn from [pii, sensitive, - confidential, exposed, financial, credential, cardholder, sad]. + confidential, exposed, financial, credential, cardholder, sad, untrusted]. # Not scoped to x-entity schemas: a stray x-classification on a DTO or a # value object is still a typo worth naming, and the value set is the same # wherever it is written. @@ -2502,6 +2518,9 @@ rules: - credential - cardholder - sad + # Generator 0.24.0. Provenance, not protection: the jar raises no + # snapshot acknowledgement or retention finding for it alone. + - untrusted # ===== x-protection (generator 0.6.0) ==================================== # diff --git a/specfuse/authoring/__init__.py b/specfuse/authoring/__init__.py index c2ea78d..ce355ff 100644 --- a/specfuse/authoring/__init__.py +++ b/specfuse/authoring/__init__.py @@ -3,7 +3,7 @@ from importlib.resources import files from pathlib import Path -__version__ = "0.31.0" +__version__ = "0.32.0" def kit_root() -> Path: diff --git a/templates/project-init/scripts/specfuse/check-extension-vocabulary.py b/templates/project-init/scripts/specfuse/check-extension-vocabulary.py index b37d4c4..7792f40 100644 --- a/templates/project-init/scripts/specfuse/check-extension-vocabulary.py +++ b/templates/project-init/scripts/specfuse/check-extension-vocabulary.py @@ -269,8 +269,12 @@ def published_vocabulary(doc: dict) -> dict[str, set[str]] | None: for member in entry.get("members") or []: if isinstance(member, dict) and isinstance(member.get("key"), str): keys.update(member["key"].split(".")) + # A surface with no keyed entries is not this comparison's. Generator + # 0.24.0's `mcp-manifest` prints `{"toolKeys": [...]}` groups for a file + # no guard covers; voiding the whole document over it dropped x-entity + # to the constant-pool scan and blamed the jar for predating `extensions`. if not keys: - return None + continue out[surface] = keys return out or None diff --git a/templates/project-init/scripts/specfuse/validate-arazzo-spectral.sh b/templates/project-init/scripts/specfuse/validate-arazzo-spectral.sh index 3baafa8..c9927b8 100755 --- a/templates/project-init/scripts/specfuse/validate-arazzo-spectral.sh +++ b/templates/project-init/scripts/specfuse/validate-arazzo-spectral.sh @@ -115,7 +115,7 @@ if [ $FAILED_FILES -eq 0 ]; then echo "" echo "All files comply with:" echo " - Document-level extensions (x-version, x-domain)" - echo " - Workflow-level extensions (x-actors, x-setup, x-mcp, x-recipe)" + echo " - Workflow-level extensions (x-actors, x-setup, x-recipe)" echo " - Step-level extensions (x-as, x-async, x-ui)" echo " - Recipe/scenario exclusion rules" echo " - File homogeneity rules" diff --git a/templates/project-init/scripts/specfuse/validate-arazzo.sh b/templates/project-init/scripts/specfuse/validate-arazzo.sh index ee8e173..e8e65b8 100755 --- a/templates/project-init/scripts/specfuse/validate-arazzo.sh +++ b/templates/project-init/scripts/specfuse/validate-arazzo.sh @@ -11,7 +11,6 @@ # C8: Cross-spec event name resolution # D9-D12: Recipe reference resolution (x-setup, extends, depth, cycles) # E13: Step overlap detection (Jaccard similarity) -# F14: x-mcp.toolName global uniqueness set -euo pipefail @@ -646,46 +645,6 @@ else: for w in e13_warnings: detail(f" REVIEW: {w}") -# ═══════════════════════════════════════════════════════════════════════════════ -# GROUP F — Global uniqueness -# ═══════════════════════════════════════════════════════════════════════════════ - -# F14: x-mcp.toolName must be globally unique across all scenario files -tool_names = defaultdict(list) -for fpath in scenario_files: - doc = parsed.get(fpath) - if not doc: - continue - # Check root-level x-mcp - x_mcp = doc.get("x-mcp") - if isinstance(x_mcp, dict): - tn = x_mcp.get("toolName") - if tn: - tool_names[tn].append(rel(fpath)) - # Check workflow-level x-mcp - workflows = doc.get("workflows", []) - if isinstance(workflows, list): - for wf in workflows: - if not isinstance(wf, dict): - continue - wf_mcp = wf.get("x-mcp") - if isinstance(wf_mcp, dict): - tn = wf_mcp.get("toolName") - if tn: - tool_names[tn].append(f"{rel(fpath)}:{wf.get('workflowId', '?')}") - -f14_errors = [] -for name, locations in tool_names.items(): - if len(locations) > 1: - f14_errors.append(f"toolName '{name}' appears in: {', '.join(locations)}") - -if not f14_errors: - emit("F14", "PASS", "x-mcp.toolName values are globally unique") -else: - emit("F14", "FAIL", f"{len(f14_errors)} duplicate toolName(s)") - for e in f14_errors: - detail(f" {e}") - PYEOF ) @@ -756,7 +715,6 @@ if [ $EXIT_CODE -eq 0 ]; then echo " - Event names resolved against AsyncAPI messages" echo " - Recipe references and extends chains validated" echo " - Step overlap within acceptable thresholds" - echo " - x-mcp.toolName uniqueness confirmed" else echo -e "\033[0;31m$TOTAL checks: $PASSED passed, $FAILED failed, $WARNED warnings\033[0m" echo ""