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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 80 additions & 10 deletions .github/workflows/example-regen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,75 @@ jobs:
fi
echo "==> specfuse-async-ai-must-have-entities fired."

# An inline enum wrapped in allOf beside a description passed lint and
# generator validate, then failed generate (PROPERTY_INNER_ENUM). The
# generator reports it from 0.16.0 (INLINE_ENUM_SCHEMA, #2579); so does
# the kit. A wrapped $ref must stay silent.
- name: Inline enums, bare and allOf-wrapped (both directions)
run: |
set +e
set -uo pipefail
REPORT="${RUNNER_TEMP}/inline-enum.txt"
spectral lint \
--ruleset schemas/spectral/specfuse-openapi.yaml \
schemas/spectral/fixtures/inline-enum-wrapped.yaml \
>"$REPORT" 2>&1
STATUS=$?
cat "$REPORT"
[ $STATUS -ge 2 ] && { echo "::error::Spectral crashed on inline-enum-wrapped.yaml" >&2; exit 1; }
fail=0
for prop in "properties.priority" "properties.channel.allOf"; do
if ! grep "specfuse-no-inline-enums" "$REPORT" | grep -q "Order.$prop"; then
echo "::error::specfuse-no-inline-enums did not fire on Order.$prop." >&2
fail=1
fi
done
if grep "specfuse-no-inline-enums" "$REPORT" | grep -q "Order.properties.status"; then
echo "::error::specfuse-no-inline-enums fired on an allOf-wrapped \$ref." >&2
fail=1
fi
[ $fail -eq 0 ] || exit 1
echo "==> inline enums caught bare and wrapped; a wrapped \$ref is not."

# x-ai.dataHandling (generator 0.16.0) and the closed x-ai shape. Every
# message names its operation through {{path}}, so each case is asserted
# by operation and rule, in both directions.
- name: x-ai shape and dataHandling rules (both directions)
run: |
set +e
set -uo pipefail
REPORT="${RUNNER_TEMP}/ai-data-handling.txt"
spectral lint \
--ruleset schemas/spectral/specfuse-asyncapi.yaml \
schemas/spectral/fixtures/ai-data-handling.yaml \
>"$REPORT" 2>&1
STATUS=$?
cat "$REPORT"
[ $STATUS -ge 2 ] && { echo "::error::Spectral crashed on ai-data-handling.yaml" >&2; exit 1; }

fail=0
for pair in \
badRegionMissing:specfuse-async-ai-personal-data-region \
badRegionThreeLetters:specfuse-async-ai-data-handling-shape \
badPersonalDataString:specfuse-async-ai-data-handling-shape \
badPersonalDataMissing:specfuse-async-ai-data-handling-shape \
badDataHandlingUnknownKey:specfuse-async-ai-data-handling-shape \
badMisspeltDataHandling:specfuse-async-ai-shape; do
op="${pair%%:*}"; rule="${pair#*:}"
if ! grep "$rule" "$REPORT" | grep -q "/operations/$op/"; then
echo "::error::$rule did not fire on $op." >&2
fail=1
fi
done
for op in goodPersonalWithRegion goodNoPersonalData goodLowerCaseRegion goodInformationalKeys; do
if grep -E "specfuse-async-ai-(shape|data-handling-shape|personal-data-region)" "$REPORT" | grep -q "/operations/$op/"; then
echo "::error::an x-ai rule fired on $op, which is valid." >&2
fail=1
fi
done
[ $fail -eq 0 ] || exit 1
echo "==> x-ai rules fire on all six violations and stay silent on four valid blocks."

# Regression net for authoring issue #14. Asserts two things that a
# passing hello-orders run cannot assert on its own, because the example
# contains no nulls and no duplicate enums:
Expand Down Expand Up @@ -608,6 +677,7 @@ jobs:
# The one rule in this file that BREAKS EXISTING SPECS on its pin,
# which is precisely why lint has to catch it before generate does.
"specfuse-xprotection-unavailable-properties-required|PoisonedReadMissing|an encrypted property with no unavailableProperties member was accepted"
"specfuse-xprotection-unavailable-properties-required|ValueObjectOnlyEncryption|an encrypted flattened value object with no unavailableProperties member was accepted (the jar refuses it from 0.14.0)"
# generator 0.15.0, PROTECTION_DIGEST_UNDECLARED (ERROR). The four
# cases are exactly the four the jar reports on this fixture.
"specfuse-xprotection-digest-undeclared|bareDigestHash|a *Hash with x-internal-only and no x-protection was accepted"
Expand Down Expand Up @@ -665,16 +735,16 @@ jobs:
fail=1
fi

# Value-object-only encryption: MEASURED against generator 0.13.0,
# which emits per-member ciphertext here and still requires nothing.
# The kit must not be stricter than the jar — that fails specs which
# generate cleanly, the adoption-blocking direction. If the generator
# closes this gap, this control flips to an EXPECTED row above.
if grep "$UP_RULE" "$REPORT" | grep -q "ValueObjectOnlyEncryption"; then
echo "::error::${UP_RULE} fired on ValueObjectOnlyEncryption. The jar does not" >&2
echo "require the member for value-object-only encryption; the kit must not either." >&2
fail=1
fi
# Value-object encryption the kit must stay silent on: `single_json`
# (the jar never requires the member) and an inherited storage (the
# jar may, from 0.16.0, but the kit cannot see the value object's
# defaultStorage, and guessing would fail specs that generate).
for vo in ValueObjectEncryptionSingleJson ValueObjectEncryptionInheritedStorage; do
if grep "$UP_RULE" "$REPORT" | grep -q "$vo"; then
echo "::error::${UP_RULE} fired on $vo, which the kit must not flag." >&2
fail=1
fi
done

# The retirement must not be silent on a scalar it does not apply to.
# `not: {contains: ...}` is FALSE for a non-array in JSON Schema, so
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ The kit is upstream of both: it defines *what* a Specfuse spec must look like. T

## Status

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

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

Expand Down
72 changes: 60 additions & 12 deletions compatibility.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions generator.lock
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"kit_version": "0.24.0",
"kit_version": "0.25.0",
"min_java": 21,
"generator": {
"version": "0.15.0",
"asset": "specfuse-generator-0.15.0.jar",
"sha256": "acd0a862d10c6b8146502b1197e2552aa170f6692bca91b079045c7e49efe285",
"version": "0.16.0",
"asset": "specfuse-generator-0.16.0.jar",
"sha256": "dcd7712870042b52cd5221e0f4d272aa67e28352b8cb456f5ff2cee92c7e8151",
"release_repo": "Specfuse/generator-dist",
"release_tag": "v0.15.0"
"release_tag": "v0.16.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."
}
1 change: 1 addition & 0 deletions handbooks/AI_Access_Policy_Framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ The flowchart is biased toward restriction by design.
| `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.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`. |

---
Expand Down
6 changes: 4 additions & 2 deletions handbooks/API_Handbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,9 @@ Content-Type: application/json
```

**Required fields**:
- `code`: Machine-readable error code (snake_case). A **string** naming the rule that failed, not the HTTP status. The status has its own integer field, `status`.
- `code`: Machine-readable error code (snake_case): a **string** naming the rule that failed. The kit's `Error` carries the HTTP status in its own integer field, `status`.

> **⚠ The generated C# exceptions disagree (generator `0.16.0`).** The `domainExceptions` templates set `Error.Code` to an **integer**, either the HTTP status (`Code = StatusCode`) or an `int errorCode` constructor argument, and set `Error.Id` to a string. Generated against the kit's `Error` (`code` a string, `id` a `format: uuid`), the exceptions fail to compile with `CS0029`. `hello-orders` does not register `domainExceptions`, which is why its build is clean. Until the kit and the generator agree on the shape (`compatibility.md` §52), a C# project that uses `domainExceptions` types `code` as an integer and `id` as a plain string, and carries the rule name in a separate string field. `reason` (`^[a-z][a-z0-9_]*$`) is the name one consumer chose. Never document a snake_case value as `error.code` on such a project, because the wire cannot carry it there.
- `message`: Human-readable explanation
- `retryable`: Usually `false` for business rule violations
- `requiredAction`: Guidance on how to resolve (recommended)
Expand Down Expand Up @@ -2768,7 +2770,7 @@ async function retryWithBackoff(operation, maxRetries = 3) {
- **PATCH collections are complete sets**: a collection property present in an `Update{Resource}` body replaces the stored set — value-object arrays wholesale, child-entity arrays by identity reconcile (known `id` updates in place, no `id` creates, unknown `id` is `404`, omitted children are permanently deleted). Give `Update{Child}` an optional `id`, and say in the property description that omitted children are not recoverable. See §1.5.1.

**Don't**
- Don't inline enums.
- Don't inline enums, including one wrapped in a one-item `allOf` beside a `description`. Generator `0.16.0` reports that form as `INLINE_ENUM_SCHEMA`; through `0.15.0` it passed `validate` and failed `generate`. The kit's `specfuse-no-inline-enums` catches both.
- Don't embed subresources unless explicitly asked.
- Don't place `x-entity` on derivatives or simple types.
- Don't use anonymous objects within a resource.
Expand Down
6 changes: 6 additions & 0 deletions handbooks/Arazzo_Handbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,8 @@ Date tokens are resolved in a restricted set of fields only. Tokens appearing an
- Step `requestBody.payload.*` values
- Step `parameters.*` values (inside the payload, not in path expressions)

**`in: query` values are expanded at run time from generator `0.16.0` (C#).** This works whether the token is the whole value (`@now-1h`) or embedded in a larger one, such as a filter (`date ge '@today+1w' and date le '@today+2w'`). Each token becomes an ISO-8601 value: `yyyy-MM-dd` for a calendar day, the round-trip form for an instant. Through `0.15.0` the C# emitters percent-encoded the macro text at generation time, so the server received `'@today+1w'` literally and rejected it (`clabonte/generator#2557`).

**Rationale:** restricting the resolution surface keeps the runtime resolver simple and predictable.

---
Expand All @@ -955,12 +957,16 @@ This means:
| Actor role's grants cover the step operation's `x-scopes` (`ARAZZO_ACTOR_LACKS_SCOPE`) | `x-actors.*.role` + step `operationId` | Mapping-form `info.x-roles` grants, operation `x-scopes` | Error, under `authorization.mode: scopes` only; from generator `0.15.0`, never at `0.14.0` (§4.6, `clabonte/generator#2418`) |
| `x-domain` value valid | `x-domain` | Project's active domain list + `cross-domain` | Error |
| `cross-domain` only in `scenarios/cross-domain/` | File path + `x-domain` | Directory structure | Error |
| Step payload carries every required property (`ARAZZO_STEP_PAYLOAD_MISSING_REQUIRED`) | Step `requestBody.payload` | Called operation's request schema (through `allOf`; a runtime expression satisfies a property; a server-assigned `x-primary-key` is never required) | Error, from generator `0.16.0`. Skipped when the step asserts a 4xx (`$statusCode == 4NN`) |
| Step payload declares no unknown property (`ARAZZO_STEP_PAYLOAD_UNDECLARED_PROPERTY`) | Step `requestBody.payload` | Called operation's request schema | Warning, from generator `0.16.0`; the typed request DTO drops the property |
| `$setup.outputs.X` resolves | Expression | Recipe `outputs` map | Error |
| 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.

### 9.3 Violation Example

```yaml
Expand Down
Loading
Loading