spec(governance): allow category-only webhook subscriptions (v0.1.25.39)#127
Conversation
Reconciles the spec to shipped-and-released reference admin behavior (cycles-server-admin 0.1.25.50). A reviewer finding on the v0.1.25.38 work exposed that its empty-both note was wrong: it claimed WebhookUpdateRequest.event_types carried minItems:1, but the shipped admin update DTO has NO bean-validation on event_types (only WebhookCreateRequest.event_types is @notempty), so PATCH accepts event_types:[] and the server's only gate is an empty-both guard (rejects a resulting state with BOTH event_types and event_categories empty, 400 "must retain at least one event_type or event_category"). Decision (Option 1): category-only subscriptions are a legitimate feature; the spec's minItems:1 on the UPDATE schema is the stale side. Verified against admin source (cycles-server-admin main): - WebhookCreateRequest.eventTypes: @notempty (create still requires non-empty event_types) — spec unchanged, matches shipped create. - WebhookUpdateRequest.eventTypes: no annotation — PATCH accepts []. - WebhookService.update empty-both guard on BOTH planes. - WebhookSubscription.eventTypes model field has no @JsonInclude(NON_NULL) → always serialized, including as [] — so required-present holds for a persisted category-only subscription. governance v0.1.25.39 (schema diff): - WebhookUpdateRequest.event_types: REMOVE minItems:1 (already optional — no required list on the update schema). New SELECTOR CLEARING note: MAY be set to [] to convert to category-only provided event_categories is/remains non-empty. - WebhookSubscription.event_types: REMOVE minItems:1; KEEP the field in WebhookSubscription.required (an empty array satisfies required presence; required-non-empty is what minItems enforced and is now wrong for category-only). Description records the ALWAYS-PRESENT / MAY-be-empty rule. - WebhookCreateRequest.event_types: UNCHANGED (required + minItems:1) — matches shipped create; NOT relaxed (that'd be a code change). - SUBSCRIPTION SELECTOR INVARIANT (NORMATIVE) added to WebhookSubscription.event_types and the createTenantWebhook prose: a persisted subscription MUST match >=1 selector (>=1 of event_types / event_categories non-empty); create or update leaving BOTH empty MUST be rejected 400 INVALID_REQUEST. This — not minItems:1 — is what makes the empty-both match-ALL state unreachable. Also: - Corrects the v0.1.25.38 changelog empty-both note (its minItems:1 reasoning was wrong for the update path; guarantee is the server-side guard, now spec-normative). - OPEN DESIGN note: create/update asymmetry (create requires event_types, update allows category-only) is real + matches shipped behavior; allowing category-only on CREATE too is a candidate future enhancement, deliberately out of scope (coordinated server+spec change). - info.summary / SPEC FAMILY CONTEXT self-references bumped to 0.1.25.39; summary chronicle extended. Additive/relaxing on the wire — removing minItems:1 only widens what validates, gated by the selector invariant; no field/operation/status removed or renamed. semantic_base stays 0.1.25.9. Changelog entry added; spec-index pins bumped (base_governance -> 0.1.25.39, spec_index -> 0.1.38); admin merged artifact regenerated (byte-stable second run; source metadata picks up 0.1.25.39 dynamically). Validation: YAML parse OK, validate_changelogs.py green. Spectral runs in CI.
….1.25.38 entry Codex review editorial fix on PR #127: the corrected v0.1.25.38 changelog entry pointed at "the CORRECTION in v0.1.25.39 below", but in newest-first (Keep a Changelog) order the .39 entry sits ABOVE .38. Changed "below" to "above". No other changes.
Editorial fix (6bf0286)Codex's one editorial finding applied: the corrected v0.1.25.38 changelog entry said "see the CORRECTION in v0.1.25.39 below", but in newest-first (Keep a Changelog) order the .39 entry sits above .38 — changed "below" → "above". Open question (considered and declined): schema-level enforcement of the empty-both invariantCodex noted (non-blocking) that an
So the invariant stays normative prose, enforced server-side; no schema |
|
External review (codex): SHIP after the editorial fix. Normative behavior verified to match cycles-server-admin 0.1.25.50 exactly (codex ran representative schema validations: category-only valid, create-empty invalid, missing invalid, update-empty valid); wire-compat confirmed (relaxing minItems only widens accepted cardinality); no regressions in the merged artifacts. The anyOf machine-enforcement suggestion was considered and declined (recorded above). Reconciles the spec to the shipped category-only webhook behavior. |
Summary
Governance spec revision v0.1.25.39: reconciles the spec to the shipped-and-released reference admin behavior (cycles-server-admin 0.1.25.50) by allowing category-only webhook subscriptions —
event_types: []with a non-emptyevent_categories("subscribe to allbudget.*by category"). Additive/relaxing on the wire; semantic_base stays 0.1.25.9.Reviewer finding this fixes
The v0.1.25.38 changelog claimed the empty-both "match-ALL" state was unreachable because
event_typescarriedminItems: 1on all three webhook schemas. That was wrong for the update path: the shipped adminWebhookUpdateRequest.eventTypeshas no bean-validation annotation, soPATCHacceptsevent_types: []. The real guarantee is a server-side empty-both guard. Decision (Option 1): category-only subscriptions are a legitimate feature, and the spec'sminItems: 1on the UPDATE schema is the stale side.Verified against admin source (cycles-server-admin main)
WebhookCreateRequest.eventTypes→@NotEmpty(create still requires non-emptyevent_types).WebhookUpdateRequest.eventTypes→ no annotation (PATCHaccepts[]).WebhookService.updateempty-both guard on both planes: rejects a resulting state with bothevent_typesandevent_categoriesempty — 400 INVALID_REQUEST "Subscription must retain at least one event_type or event_category" (comment cites governance v0.1.25.38).WebhookSubscription.eventTypesmodel field has no@JsonInclude(NON_NULL)→ always serialized, including as[], so keeping the spec fieldrequired-present is correct.Exact schema diff
WebhookUpdateRequest.event_typesminItems: 1, optional (no required list)minItemsremoved, optional; SELECTOR CLEARING note (MAY be[]for category-only, providedevent_categoriesnon-empty)WebhookSubscription.event_typesrequired,minItems: 1minItemsremoved, stays inrequired(required-present, MAY be empty)WebhookCreateRequest.event_typesrequired,minItems: 1@NotEmptycreate; not relaxed — that would be a server code change)Normative invariant text (verbatim)
Added to
WebhookSubscription.event_typesand thecreateTenantWebhookprose:WebhookUpdateRequestSELECTOR CLEARING note (verbatim):WebhookSubscription.requireddecisionevent_typesstays inrequired. A persisted category-only subscription carriesevent_types: []— an empty array still satisfiesrequiredpresence (it wasminItemsthat forbade empty), and the reference admin model serializes the field unconditionally (no@JsonInclude(NON_NULL)). So the field is required-present-but-possibly-empty; only theminItems: 1was removed.required: [subscription_id, tenant_id, url, event_types, status, created_at]is unchanged.Also in this revision
minItems: 1reasoning was wrong for the update path; the guarantee is the server-side guard, now spec-normative).event_types, update allows clearing to category-only — is real and matches shipped behavior. Allowing category-only on create too is a candidate future enhancement (coordinated server + spec change), deliberately out of scope.info.summary/ SPEC FAMILY CONTEXT document-revision self-references bumped to 0.1.25.39; summary chronicle extended.Validation
validate_changelogs.pygreen (v0.1.25.39 heading matchesinfo.version).merged/cycles-openapi-admin-merged.yamlregenerated (byte-stable second run — merge-check passes; source metadata picks up v0.1.25.39 dynamically). Protocol merged artifact unaffected.