Skip to content

spec(governance): webhook selector boundary as cross-plane conformance invariant + impl-pending honesty + migration fix (v0.1.25.41) [HELD]#130

Merged
amavashev merged 9 commits into
mainfrom
spec/gov-webhook-invariant-conformance-v0.1.25.41
Jul 12, 2026
Merged

spec(governance): webhook selector boundary as cross-plane conformance invariant + impl-pending honesty + migration fix (v0.1.25.41) [HELD]#130
amavashev merged 9 commits into
mainfrom
spec/gov-webhook-invariant-conformance-v0.1.25.41

Conversation

@amavashev

Copy link
Copy Markdown
Contributor

Summary

Governance spec revision v0.1.25.41, correcting three reviewer findings against merged main (PR #129, v0.1.25.40). Prose/normative-surface only — no schema/operation/status-code change; semantic_base stays 0.1.25.9.

HELD: this PR is authored ready-to-merge but is not to be merged — all merges/releases are being held.

Finding 1 (P1) — implementation-status overclaim

v0.1.25.40 said the admin-plane category boundary was "fixed in parallel," and the spec's "reference servers implement the baseline today" claims implied conformance that doesn't exist. Verified against source: cycles-server-admin origin/main latest release is v0.1.25.50 without the boundary (WebhookAdminController.create/update have no category validation — grep = 0), and the implementation is in OPEN, UNRELEASED cycles-server-admin PR #210 (state: OPEN, mergedAt: null).

Fix — scoped, does not un-claim the whole baseline:

  • info.summary and SPEC FAMILY CONTEXT now state the general baseline (including INVARIANT 1 and the .38 tenant self-service half of INVARIANT 2) is implemented today, with one scoped exception: the admin-plane half of the tenant-owned category boundary (.40/.41) is normatively defined but its reference implementation is PENDING (PR #210, unreleased as of 2026-07-11; latest admin release v0.1.25.50 does not enforce it). A matching status line is in CONFORMANCE.md's normative-invariants block. Each carries a TODO to drop the caveat when #210 releases.
  • The .40 changelog "fixed in parallel" line is corrected in place.

The impl-pending wording (SPEC FAMILY CONTEXT, verbatim):

runcycles' reference servers implement that baseline today, with one scoped exception: the WEBHOOK SUBSCRIPTION INVARIANT 2 admin-plane enforcement (document revision 0.1.25.40/.41) is normatively defined but NOT YET SHIPPED — its reference implementation is prepared in cycles-server-admin PR #210 and is PENDING merge/release as of 2026-07-11 (latest admin release v0.1.25.50 does not enforce it). The rest of the baseline, including INVARIANT 1 and the tenant self-service half of INVARIANT 2 (document revision 0.1.25.38), is implemented. TODO: remove this caveat once cycles-server-admin PR #210 releases.

Finding 2 (P1) — the NORMATIVE boundary was outside the conformance surface

The two admin webhook ops are x-conformance: reference and CONFORMANCE.md lists only 8 normative ops, so a server could claim conformance while ignoring the .40 boundary. Verified how the .39 SUBSCRIPTION SELECTOR INVARIANT was made binding: it wasn't in CONFORMANCE.md either — it's a schema-level normative invariant.

Approach chosen: single cross-plane INVARIANT (not promote-ops). Defined a WEBHOOK SUBSCRIPTION INVARIANTS (NORMATIVE, CROSS-PLANE) block in info.description — a property of a persisted subscription's stored state, binding regardless of any operation's x-conformance label or which provisioning mechanism a server exposes — and added it to CONFORMANCE.md's §MUST normative surface:

  • INVARIANT 1 — selector presence (from .39)
  • INVARIANT 2 — tenant-owned selectors are tenant-accessible: a concrete-tenant-owned subscription MUST NOT carry admin-only types/categories, unifying the .38 tenant self-service half and the .40 admin write-path half into one rule that holds by ANY provisioning mechanism (tenant self-service, admin plane, admin-on-behalf-of, or future).

The §MAY "replace the reference provisioning mechanism" allowance is tightened: a substituted mechanism MUST still uphold these invariants. Both admin endpoints' boundary blocks now point at the invariant. Operation count stays 8.

Why invariant over promoting the 2 ops to normative: the boundary is a cross-cutting security property of the subscription DATA, not of a specific endpoint. Promoting the ops to x-conformance: normative would (a) force the reference endpoint shape on servers that §MAY explicitly lets substitute their provisioning mechanism, and (b) still leave the invariant unenforced for those alternative mechanisms — the exact loophole. An invariant on stored state closes it however provisioning is exposed.

Finding 3 (P2) — migration recipe was unsubmittable

The .40 recipe told operators to create a __system__ subscription with only event_categories set, but WebhookCreateRequest still requires event_types (minItems:1, kept on create in .39) — so that create is rejected. Corrected recipe (verbatim, createWebhookSubscription prose):

To monitor admin-only events, create a system-owned subscription (omit tenant_id, or set tenant_id="system") — operator-owned (operator controls its URL + signing secret), so admin selectors are permitted — with event_types listing the admin event types you want (e.g. ["api_key.created","api_key.revoked"]); that satisfies the create-time event_types minItems:1 requirement directly and needs no event_categories. (For category-level matching you must create with at least one event_type first, then PATCH event_types to [] with event_categories set — the create/update selector asymmetry of document revision 0.1.25.39.) …

Both the yaml prose and the changelog recipe are updated.

Scope / validation

Governance spec + governance spec-index keys (base_governance → 0.1.25.41, governance_base.version, spec_index → 0.1.41) + admin merged artifact + CONFORMANCE.md only. No runtime files. info.summary / SPEC FAMILY CONTEXT self-refs bumped to 0.1.25.41; chronicle extended. YAML parse OK; validate_changelogs.py green; admin merged regenerated byte-stable (second run identical → merge-check passes). Spectral runs in CI.

…e invariant + impl-pending honesty + migration fix (v0.1.25.41)

Corrects three reviewer findings against merged main (PR #129, v0.1.25.40).
This PR is HELD (ready-to-merge, not merged) — all merges/releases are
being held.

FINDING 1 (P1) — overclaimed implementation status. .40 said the
admin-plane category boundary was "fixed in parallel" and the spec's
"reference servers implement the baseline today" claims implied
conformance that doesn't exist. Verified: admin origin/main latest
release is v0.1.25.50 WITHOUT the boundary (WebhookAdminController has no
category validation); the impl is in OPEN, UNRELEASED cycles-server-admin
PR #210. Fix: info.summary + SPEC FAMILY CONTEXT baseline claims now carry
a SCOPED caveat — the general baseline (incl. INVARIANT 1 and the .38
tenant self-service half of INVARIANT 2) is implemented, but the
admin-plane half (.40/.41) is normatively defined yet PENDING (PR #210,
unreleased as of 2026-07-11). Matching status line in CONFORMANCE.md.
Each carries a TODO to drop when #210 releases. The .40 changelog
"fixed in parallel" line is corrected in place.

FINDING 2 (P1) — the NORMATIVE boundary was outside the conformance
surface. The two admin webhook ops are x-conformance:reference, and
CONFORMANCE.md lists only 8 normative ops, so a server could claim
conformance while ignoring the boundary. Verified how the .39 SELECTOR
INVARIANT was made binding: it wasn't in CONFORMANCE.md either — it's a
schema-level normative invariant. Chosen approach (INVARIANT, not
promote-ops): defined a single cross-plane WEBHOOK SUBSCRIPTION INVARIANTS
block in info.description (a property of the persisted subscription's
STORED STATE, binding regardless of any op's x-conformance or which
provisioning mechanism a server exposes) and added it to CONFORMANCE.md's
§MUST normative surface:
  - INVARIANT 1: selector presence (from .39)
  - INVARIANT 2: tenant-owned subscriptions carry only tenant-accessible
    selectors — unifying the .38 tenant self-service half and the .40 admin
    write-path half into ONE rule holding by ANY provisioning mechanism
    (tenant self-service, admin plane, admin-on-behalf-of, or future).
The §MAY "replace the reference provisioning mechanism" allowance is
tightened to require a substituted mechanism to uphold these invariants.
Both admin endpoints' boundary blocks now point at the invariant.
Rationale for invariant over promoting the 2 ops to normative: the
boundary is a cross-cutting SECURITY property of the subscription DATA,
not of a specific endpoint; promoting the ops would force the reference
endpoint shape on servers the §MAY clause explicitly lets substitute
their provisioning mechanism, AND would still leave the invariant
unenforced for alternative mechanisms. Operation count stays 8.

FINDING 3 (P2) — migration recipe unsubmittable. The .40 recipe told
operators to create a __system__ subscription with only event_categories,
but WebhookCreateRequest still requires event_types minItems:1 (kept on
create in .39), so that create is rejected. Fixed (yaml prose + changelog
recipe): create the __system__ subscription with event_types listing the
admin event types to monitor (e.g. ["api_key.created","api_key.revoked"]),
which satisfies minItems:1 directly; no event_categories needed. For
category-level matching, create with >=1 event_type then PATCH event_types
to [] with event_categories set (the .39 create/update asymmetry).

Prose/normative-surface only — no schema shape, operation, or status-code
change; adds no NEW wire constraint beyond what .38/.39/.40 defined
(makes the existing boundary conformance-binding, corrects false
impl-status claims, fixes the recipe). semantic_base stays 0.1.25.9.
Changelog entry added; spec-index governance pins bumped (base_governance
-> 0.1.25.41, governance_base version, spec_index -> 0.1.41); admin merged
artifact regenerated (byte-stable second run). Validation: YAML parse OK,
validate_changelogs.py green. Spectral runs in CI.

Scope: governance spec + governance spec-index keys + admin merged +
CONFORMANCE.md only; no runtime files.
…ves merge + impl-status honesty (v0.1.25.41)

Round 2 of the HELD PR #130. Batches three human reviewer findings and
codex's corroborating additions (codex verdict REVISE-MINOR; it confirmed
the migration recipe is already correct — unchanged). PR stays HELD.

F1 (P1) + codex A — invariants recognized as first-class in the formal
conformance-SOURCE model, everywhere it is defined. The prior round added
INVARIANT 2 only to CONFORMANCE.md's MUST section; the "what counts as the
normative surface" declarations still listed only schemas + labeled ops.
Added "cross-plane normative invariants" as a first-class category to ALL
of: CONFORMANCE.md authoritative-source rule (new item 3) AND the
mixed-document normative-content sentence; cycles-spec-index.yaml
publication_model_rationale AND the governance conformance_note; README
Mixed-tier row. The §MAY "replace provisioning mechanism" allowance is
tightened to require substituted mechanisms to uphold the invariants.

F1/F3 + codex A — impl-pending status moved OUT of the canonical contract
into the docs, and the "implemented today" caveat completed everywhere.
Per the spec's own AUTHORING CONVENTION (~373: reject impl-version markers
in normative prose — verified), removed the volatile markers (server
release version, PR #210, date, TODO) from the canonical yaml info.summary
and SPEC FAMILY CONTEXT; those now make NO implementation-status claim and
defer to CONFORMANCE.md/changelog. Added a dedicated CONFORMANCE.md
"### Reference-implementation status" section carrying the volatile
pending detail + TODO. Completed the scoped .40/.41 caveat on the stale
"implemented today" claims at CONFORMANCE.md:5, README:21, and
spec-index:157 (action_kinds conformance_note). Zero-survivor sweep for
"implement...today" / "fixed in parallel": the three baseline claims now
carry the exception; the .38 "fixed in parallel as 0.1.25.50" is accurate
(tenant-plane boundary DID ship in .50) and is left; the runtime .14
"fixed in parallel" hit is a different plane/repo (cycles-server) and out
of scope.

F2 (P2) — the invariant now survives merge generation. merge_specs.py
substitutes its own info.description, which dropped the WEBHOOK
SUBSCRIPTION INVARIANTS block and left dangling "INVARIANT 2
(info.description)" references in the admin merged artifact (the file
validators/codegen consume). Added extract_info_description_section() and
wired it to lift the block verbatim from the governance base into the
merged admin info.description (raises if the section markers are missing,
so a rename fails the merge loudly). Verified: block present in the merged
admin artifact, both endpoint "INVARIANT 2" references resolve within it,
byte-stable (md5 identical across two runs). Runtime merged carries no
webhook invariant reference — unaffected.

Codex B — invariant made provisioning-neutral. Added a VIOLATION HANDLING
clause: every provisioning mechanism MUST prevent persistence of a
violating subscription and report an equivalent validation failure; the
HTTP reference surfaces return 400 INVALID_REQUEST; a non-HTTP provisioner
(GitOps/CLI/direct write) reports the equivalent failure in its own idiom.
The per-invariant rules now say "rejected (HTTP reference surfaces: 400
INVALID_REQUEST; see VIOLATION HANDLING)".

Codex C — README stale "Governance base: v0.1.25.29" bumped to the current
governance base v0.1.25.41 (matches base_governance in the spec-index).

Prose/normative-surface + tooling (merge script) only — no schema shape,
operation, or status-code change; semantic_base stays 0.1.25.9. Governance
spec + governance spec-index + admin merged + CONFORMANCE.md + README +
merge script only; no runtime files (protocol merged unchanged). YAML parse
OK; validate_changelogs.py green; merged regen byte-stable + invariant
survives. Spectral runs in CI.
@amavashev

Copy link
Copy Markdown
Contributor Author

Round 2 (4c7256b) — six findings (3 human + codex A/B/C); PR stays HELD

Codex verdict REVISE-MINOR corroborated the human findings and confirmed the migration recipe (human F3) is already correct — unchanged. All others applied.

F1 + codex A — invariants are now first-class in the formal conformance-SOURCE model

The prior round added INVARIANT 2 only to CONFORMANCE.md's MUST section; the "what constitutes the normative surface" declarations still recognized only schemas + labeled ops. Added "cross-plane normative invariants" as a first-class category to every such declaration (F1 source-model wording):

  • CONFORMANCE.md authoritative-source rule — new item 3 (verbatim):
    1. any cross-plane normative invariant declared as such in a normative-or-mixed document — a property of persisted state or protocol behavior that binds regardless of which operation or provisioning mechanism produces it (e.g. the WEBHOOK SUBSCRIPTION INVARIANTS block in cycles-governance-admin-v0.1.25.yaml info.description). These invariants are first-class members of the normative surface alongside named schemas and x-conformance: normative operations; a server MUST uphold them even where the producing operation is x-conformance: reference.
  • CONFORMANCE.md mixed-document sentence ("eight operations, a set of schemas, and a set of cross-plane invariants … are normative"); cycles-spec-index.yaml publication_model_rationale + governance conformance_note; README Mixed-tier row — all now enumerate the invariants. The §MAY "replace provisioning mechanism" clause is tightened to require substituted mechanisms to uphold them.

F1/F3 + codex A — impl-pending status moved out of the contract; "implemented today" caveat completed everywhere

Verified the AUTHORING CONVENTION (~373): "Reviewers should reject PRs that add impl-version markers to normative prose." Removed the volatile markers (release version, PR #210, date, TODO) from the canonical yaml — info.summary and SPEC FAMILY CONTEXT now make no implementation-status claim and defer to CONFORMANCE.md/changelog. The pending status moved to a dedicated CONFORMANCE.md ### Reference-implementation status section (with PR #210, date, TODO). Completed the scoped .40/.41 exception on the stale "implemented today" claims at CONFORMANCE.md:5, README:21, spec-index:157 (action_kinds conformance_note).

Zero-survivor sweep ("implement…today" / "fixed in parallel" / "reference servers implement"):

Hit Verdict
CONFORMANCE.md:5, README:21, spec-index:157 (baseline "implemented today") Fixed — now carry the .40/.41 exception
changelog:257 — .38 "fixed in parallel as cycles-server-admin 0.1.25.50" Accurate — the tenant-plane boundary DID ship in v0.1.25.50; left
changelogs/cycles-protocol-v0.md:25 — runtime .14 "fixed in parallel" Out of scope — different plane (cycles-server, runtime), not this governance PR; left
CONFORMANCE.md:7/:122, gov yaml:320 — "reference servers do not implement it yet" Accurate — about v0.1.26 upcoming; left
"Servers implementing this extension/revision…" (many) Not in class — generic conditional behavior clauses; left

Zero survivors in the target class.

F2 — the invariant now SURVIVES merge generation (load-bearing)

merge_specs.py substitutes its own info.description, which dropped the WEBHOOK SUBSCRIPTION INVARIANTS block and left dangling "INVARIANT 2 (info.description)" references in merged/cycles-openapi-admin-merged.yaml. Added extract_info_description_section() and wired it to lift the block verbatim from the governance base into the merged admin info.description (raises if the section markers are missing → a rename fails the merge loudly rather than silently dropping a normative contract). Verified:

  • Block present in the merged admin artifact (3 section-header hits).
  • Both admin-endpoint "…WEBHOOK SUBSCRIPTION INVARIANT 2…" references now resolve within the same file.
  • Byte-stable — md5 identical across two consecutive runs (merge-check passes).
  • Runtime merged carries no webhook invariant reference — unaffected.

Codex B — provisioning-neutral violation handling

Added a VIOLATION HANDLING clause to the invariant block: every provisioning mechanism MUST prevent a violating subscription from being persisted and report an equivalent validation failure; the HTTP reference surfaces return 400 INVALID_REQUEST; a non-HTTP provisioner (GitOps/CLI/direct write) reports the equivalent failure in its own idiom. Per-invariant rules now read "rejected (HTTP reference surfaces: 400 INVALID_REQUEST; see VIOLATION HANDLING)".

Codex C — README governance-base bump

Governance base: v0.1.25.29v0.1.25.41 (matches base_governance in the spec-index; the parenthetical semantic_base 0.1.25.9 is the frozen wire identity and is unchanged).

Validation

YAML parse OK; validate_changelogs.py green; admin merged regenerated byte-stable with the invariant block preserved + references resolving; runtime files untouched (protocol merged unchanged). semantic_base stays 0.1.25.9. Spectral runs in CI. PR remains HELD.

…5.41)

Codex round 2 found all three round-2 fixes INCOMPLETE (cited spot fixed,
siblings missed — the semantic-sweep failure mode). This round does proper
zero-survivor sweeps. PR stays HELD.

F3 — volatile impl markers fully purged from the canonical yaml. Round 2
removed them only from SPEC FAMILY CONTEXT; a full case-insensitive sweep
found three more and removed all:
  - info.summary (~51): "cycles-server-admin PR #210, unreleased" caveat
  - SPEC FAMILY CONTEXT v0.1.26 block (~320): "runcycles' reference servers
    do not implement it yet" -> "see CONFORMANCE.md ... reference-
    implementation status"
  - Event.actor.type prose (~1814): "reference servers have emitted this ...
    added to the enum in revision 2026-07-04" -> behavioral, undated
Re-swept #210 / unreleased / v0.1.25.50 / 2026-07- / TODO / "reference
server ...(have|implement|do not|emitted)" / "not yet shipped" / "prepared
in": ZERO survivors. Remaining "PENDING" strings are the WebhookDelivery
status enum (unrelated). The volatile status lives only in CONFORMANCE.md
§Reference-implementation status + the changelog, per ~373 AUTHORING
CONVENTION.

F2 — merge-script guard now fails loud on a missing END marker too (round
2 only guarded the START marker; a missing/renamed end marker would have
silently lifted an unbounded/wrong block). extract_info_description_section
now raises on missing-start, missing-end, and end<=start; core logic split
into a pure _extract_section_from_text() and a _self_test() that exercises
all three failure cases plus the happy path on every merge run. Verified:
happy path + all three anomalies raise; byte-stable (md5 identical across
two runs); invariant block + both endpoint refs present/resolving in the
merged admin artifact.

F1 — three accounting fixes:
  1. Authoritative-source rule (CONFORMANCE.md item 2) claimed schemas are
     "individually labeled x-conformance: normative", but NO schema in the
     governance yaml carries x-conformance (verified: all 47 labels are on
     operations; components/schemas has zero). Reworded: operations are
     label-identified; schemas are designated normative by ENUMERATION in
     CONFORMANCE.md's Normative-schemas list. spec-index
     publication_model_rationale reworded the same way.
  2. publication_model_rationale schema list omitted WebhookRetryPolicy
     (present in the sibling enumerations; pre-existing omission) — added.
  3. Two more normative-surface enumerations omitted the invariants —
     CONFORMANCE.md summary (~21) and README reference-server note (~180) —
     now include them. Full sweep for "normative surface / what a conformant
     server must implement" enumerations: also updated CONFORMANCE.md files
     table, the §MUST section header, the transitional note, and README's
     conformance summary. Zero survivors.

Prose/normative-surface + tooling only — no schema shape, operation, or
status-code change; semantic_base stays 0.1.25.9. Governance spec +
spec-index + admin merged + CONFORMANCE.md + README + merge script only;
no runtime files (protocol merged unchanged). YAML parse OK;
validate_changelogs.py green; merged regen byte-stable + invariant survives
+ guard self-test green. Spectral runs in CI.
@amavashev

Copy link
Copy Markdown
Contributor Author

Round 3 (b87225e) — completed the three sweeps codex flagged as incomplete. PR stays HELD.

F3 — canonical-yaml volatile-marker sweep (zero survivors)

Round 2 removed markers only from SPEC FAMILY CONTEXT. A full case-insensitive sweep of cycles-governance-admin-v0.1.25.yaml found and removed three more:

Location Was Now
info.summary (~51) "…marks that boundary's reference implementation PENDING (cycles-server-admin PR #210, unreleased)…" clause removed
SPEC FAMILY CONTEXT v0.1.26 block (~320) "v0.1.26 is not yet required for conformance and runcycles' reference servers do not implement it yet" "v0.1.26 is not yet required for conformance; see CONFORMANCE.md for the active conformance statement and reference-implementation status"
Event.actor.type prose (~1814) "…reference servers have emitted this on such events since… added to the enum in revision 2026-07-04 (same additive-enum-gap class…)" "Mirrors the audit-log actor_type of the same name and is emitted on events produced by admin-key dual-auth operations." (undated, no impl claim)

Final sweep of #210 / unreleased / v0.1.25.50 / 2026-07- / TODO / reference server …(have|implement|do not|emitted) / not yet shipped / prepared inexit 1, zero matches. The residual PENDING strings are the WebhookDelivery.status enum (unrelated). Also tightened the SPEC FAMILY CONTEXT deferral pointer ("Reference-implementation status is tracked in CONFORMANCE.md and the changelog, not in this contract prose"). The volatile status lives only in CONFORMANCE.md §Reference-implementation status + the changelog, per the ~373 AUTHORING CONVENTION.

F2 — guard now fails loud on a missing END marker (and out-of-order/empty)

Round 2's extract_info_description_section() raised only on a missing START marker and silently returned the remainder when the END marker was missing. Fixed: it now raises on missing-start, missing-end, and end≤start. Core logic split into a pure _extract_section_from_text() + a _self_test() that exercises the happy path and all three failure cases; _self_test() runs at the top of every merge. Verified all three anomalies raise; byte-stable (md5 identical across two runs); invariant block + both endpoint references present and resolving in the merged admin artifact.

F1 — three accounting fixes

  1. Schemas are named, not labeled. Verified against the spec: all 47 x-conformance labels are on operations; components/schemas has zero (Event, WebhookDelivery, etc. carry none). The authoritative-source rule (CONFORMANCE.md item 2) and the spec-index publication_model_rationale claimed schemas are "individually labeled x-conformance: normative" — both reworded: operations are label-identified; schemas are designated normative by enumeration in CONFORMANCE.md's Normative-schemas list (schemas are not labeled).
  2. WebhookRetryPolicy was omitted from the publication_model_rationale schema list (present in the sibling enumerations — a pre-existing omission). Added; the enumerations now agree.
  3. Two more enumerations omitted the invariants — CONFORMANCE.md summary (~21) and README reference-server note (~180) — now include them. Sweeping all "normative surface / what a conformant server must implement" enumerations, I also updated CONFORMANCE.md's files table (~26), the §MUST section header (~65), the transitional note (~98), and README's conformance summary (~55). Re-sweep → zero survivors (remaining hits are single-op classifications like the getBalances note, not surface enumerations).

Validation

cycles-governance-admin-v0.1.25.yaml sweep zero-survivor; guard self-test green; YAML parse OK; validate_changelogs.py green; admin merged regenerated byte-stable with the invariant preserved + references resolving. Governance spec + spec-index + admin merged + CONFORMANCE.md + README + merge script only; no runtime files (protocol merged unchanged). semantic_base stays 0.1.25.9. Spectral runs in CI. PR remains HELD.

…d PR #130)

Folded into the same held v0.1.25.41 revision. The cycles-server-admin
implementation (#210) surfaced that the webhook /test endpoints are a
narrow, legitimate exception to INVARIANT 2 that the spec must document —
otherwise INVARIANT 2 reads as absolute while a conformant server has a
carve-out (the exact spec-vs-impl mismatch this revision eliminates).

Verified against source: testTenantWebhook / testWebhookSubscription POST
a synthetic system.webhook_test connectivity event directly to the
subscription's own endpoint (SYSTEM_WEBHOOK_TEST -> wire "system.webhook_
test", category SYSTEM in cycles-server-admin EventType; the spec's /test
operation definitions agree). system.* is admin-only, so an absolute
INVARIANT 2 would forbid /test on any tenant-owned subscription.

Added a SCOPE clause to INVARIANT 2 (WEBHOOK SUBSCRIPTION INVARIANTS block,
info.description): INVARIANT 2 governs delivery of governance EVENTS from
the event stream to a subscription; it does NOT apply to the owner-
triggered test probe — a point-to-point, owner-requested connectivity check
that bypasses the dispatch queue and carries no governance telemetry. A
tenant-owned subscription MAY receive its own system.webhook_test probe
WITHOUT adding system to its stored selectors (which still MUST satisfy
INVARIANT 2). The carve-out is limited to the synthetic test event on the
/test operations; no real system.* / api_key.* / policy.* / webhook.*
governance event may reach a tenant-owned subscription.

Both /test operation descriptions (testWebhookSubscription,
testTenantWebhook) gain a pointer to the exception so they don't read as
contradicting INVARIANT 2; the CONFORMANCE.md INVARIANT 2 bullet carries a
one-line carve-out. The exception rides through the F2 merge injection into
merged/cycles-openapi-admin-merged.yaml (verified present in the merged
info.description + both /test op descriptions; byte-stable).

Prose/normative only — no schema/operation/status change; semantic_base
stays 0.1.25.9. Governance spec + CONFORMANCE.md + changelog + admin merged
only; no runtime files. YAML parse OK; validate_changelogs.py green; merged
regen byte-stable. Spectral runs in CI. PR remains HELD.
@amavashev

Copy link
Copy Markdown
Contributor Author

Round 3 addendum (8eb1258) — INVARIANT 2 webhook-test carve-out. PR stays HELD.

The cycles-server-admin implementation (#210) surfaced that the /test endpoints are a narrow, legitimate exception to INVARIANT 2 the spec must document — otherwise INVARIANT 2 reads as absolute while a conformant server carves out /test (the exact spec-vs-impl mismatch this revision eliminates).

Verified against source before wording: testTenantWebhook / testWebhookSubscription POST a synthetic system.webhook_test connectivity event directly to the subscription's own endpoint. Event type confirmed as SYSTEM_WEBHOOK_TEST → wire system.webhook_test, category SYSTEM (cycles-server-admin EventType.java:65; WebhookService.test), matching the spec's /test operation definitions (event_type "system.webhook_test"). system.* is admin-only, so an absolute INVARIANT 2 would forbid /test on any tenant-owned subscription.

Exact exception wording added (INVARIANT 2, info.description)

SCOPE — WEBHOOK TEST PROBE EXCEPTION: INVARIANT 2 governs the SELECTORS a tenant-owned subscription may carry, and thereby which governance events (from the event stream) may be DELIVERED to it. It does NOT apply to the owner-triggered webhook test probe (the testTenantWebhook / testWebhookSubscription "/test" operations), which POSTs a single synthetic system.webhook_test connectivity event directly to the subscription's own endpoint at the owner's request — a point-to-point probe that bypasses the event-dispatch queue and carries NO governance telemetry. A tenant-owned subscription MAY receive its own test probe even though system.webhook_test is a system.* (admin-only) EventType, and this does NOT require adding system to the subscription's selectors (the subscription's stored event_types / event_categories are unchanged and still MUST satisfy INVARIANT 2). The exception is limited to this synthetic test event on the /test operations; it does NOT permit any real system.* / api_key.* / policy.* / webhook.* governance event from the event stream to be delivered to a tenant-owned subscription.

Consistency

  • Both /test operation descriptions (testWebhookSubscription, testTenantWebhook) gain a pointer to the exception so they don't read as contradicting INVARIANT 2. Verified neither previously contradicted it (they only describe sending the synthetic event).
  • The CONFORMANCE.md INVARIANT 2 bullet carries a one-line carve-out.
  • The exception rides through the F2 merge injection: verified present in the merged admin info.description and in both /test operation descriptions in merged/cycles-openapi-admin-merged.yaml; regenerated byte-stable (md5 identical across two runs).

Validation

YAML parse OK; validate_changelogs.py green; merged regen byte-stable + exception survives. Governance spec + CONFORMANCE.md + changelog + admin merged only; no runtime files; same v0.1.25.41 revision; semantic_base stays 0.1.25.9. Spectral runs in CI. PR remains HELD.

@amavashev

Copy link
Copy Markdown
Contributor Author

External review (codex): SHIP after 3 rounds. The invariant/conformance design was sound throughout; the rounds tightened completeness. Final state: cross-plane WEBHOOK SUBSCRIPTION INVARIANTS are first-class members of the formal normative-source model across all enumerations (CONFORMANCE.md, spec-index, README); merge_specs.py injects the invariant block into the merged admin artifact (fail-loud on missing start/end markers, self-tested each run) so endpoint references resolve in the generated contract; volatile impl-status markers removed from the canonical contract (zero-survivor sweep) and relocated to CONFORMANCE.md's Reference-implementation-status section; the invariant is provisioning-neutral (HTTP surfaces return 400, non-HTTP report equivalently); the /test probe carve-out is documented (owner-triggered synthetic connectivity event, not governance-event delivery, selectors unchanged). PARKED ready-to-merge per the hold.

…(v0.1.25.41, held PR #130)

Folded into the same held v0.1.25.41 revision. Surfaced by the
cycles-server-admin #210 replay work. No schema change: replayEvents
already declared a 400 "Invalid replay request" (ErrorResponse), so this
documents an existing-but-unspecified semantic.

Added a NORMATIVE SCAN LIMIT clause to the replayEvents LIMITS block: a
replay whose [from, to] window contains more candidate events than the
server's replay scan limit AND cannot be satisfied within it (fewer than
max_events deliverable events found within the scanned portion, with more
beyond) MUST be rejected with 400 INVALID_REQUEST rather than silently
returning a partial replay; the caller narrows the from/to range. Made
explicitly distinct from the max_events pagination cap — reaching
max_events within the window is a normal SUCCESS (advance `from` and
continue). The scan-limit VALUE stays server-defined/implementation-
specific (reference servers document their own limit; the 20000-style
number is NOT pinned in the normative spec) — the normative point is the
BEHAVIOR: no silent partial replay; a too-broad window is a 400, not a
truncated success. Extended the existing 400 response description to name
this case.

Operation prose (not info.description), so it rides the merge normally —
verified the SCAN LIMIT clause + extended 400 description are present in
merged/cycles-openapi-admin-merged.yaml; regenerated byte-stable (md5
identical across two runs).

Prose/normative only — no schema/operation/status change; semantic_base
stays 0.1.25.9. Governance spec + changelog + admin merged only; no runtime
files; same v0.1.25.41 revision. YAML parse OK; validate_changelogs.py
green. Spectral runs in CI. PR remains HELD.
@amavashev

Copy link
Copy Markdown
Contributor Author

Round 3 addendum (2202d9f) — replayEvents scan-limit → 400 (no silent partial). PR stays HELD.

Surfaced by the cycles-server-admin #210 replay work. No schema changereplayEvents already declares a 400 "Invalid replay request" (ErrorResponse), so this documents an existing-but-unspecified semantic. Verified the current LIMITS wording (the max_events 1000 cap + "use from/to to control the window") and extended it consistently.

Exact clause added (replayEvents description → LIMITS)

  • SCAN LIMIT (NORMATIVE): reaching max_events deliverable events WITHIN the window is a normal pagination cap and is a SUCCESS (the caller continues by advancing from past the last replayed event). Distinct from this: a server enforces a maximum number of candidate events it will SCAN across the [from, to] window while satisfying a request. If the window contains more candidate events than that scan limit AND the request cannot be satisfied within it — i.e. fewer than max_events deliverable events are found within the scanned portion while more candidates remain beyond it — the server MUST reject the request with 400 INVALID_REQUEST rather than silently returning a partial replay; the caller should narrow the from/to range and retry. The scan-limit VALUE is server-defined (implementation-specific; a reference server documents its own limit) — the normative point is the BEHAVIOR: no silent partial replay; a too-broad window is a 400, not a truncated success.

The existing 400 response description is also extended to name this case (and to state it is not returned merely for reaching the max_events cap).

Key distinctions encoded

  • max_events cap reached within the window → normal SUCCESS (pagination; advance from).
  • scan limit exceeded before the request can be satisfied → 400 INVALID_REQUEST; narrow the window. No silent partial replay.
  • The scan-limit number stays server-defined — the 20000-style value is not pinned in the normative spec; reference servers document their own limit. The normative point is the behavior.

Verification

Operation prose (not info.description), so it rides the merge normally — confirmed the SCAN LIMIT clause and the extended 400 description are present in merged/cycles-openapi-admin-merged.yaml. Regenerated byte-stable (md5 identical across two runs). YAML parse OK; validate_changelogs.py green. Governance spec + changelog + admin merged only; no runtime files; same v0.1.25.41 revision; semantic_base stays 0.1.25.9. Spectral runs in CI. PR remains HELD.

@amavashev

Copy link
Copy Markdown
Contributor Author

External review (codex): SHIP — replayEvents SCAN LIMIT clause confirmed (additive prose; distinguishes max_events pagination-cap success from scan-limit 400, value server-defined / behavior normative, present in merged artifact). Documents the semantic implemented in cycles-server-admin #210. Re-PARKED ready-to-merge per the hold.

…guidance) (v0.1.25.41, held PR #130)

Reviewer round on #130: the SCAN LIMIT clause I added introduced INVALID
continuation guidance. It framed max_events as a resumable pagination cap
and told callers to "advance from past the last replayed event" — but
replayEvents has NO continuation position (ReplayResponse = replay_id /
events_queued / estimated_completion_seconds only). With distinct
timestamps the caller never learns the last replayed timestamp; with
same-millisecond events, re-using inclusive `from` repeats the page while
advancing skips the rest. Unusable.

Decision (chosen over adding a continuation cursor to the schema — no
schema change): rewrite to ALL-OR-NARROW semantics, matching the parallel
cycles-server-admin #210 fix.

- A successful replay delivers EVERY deliverable event in [from, to]; it is
  COMPLETE for the window, never a partial with implied continuation.
- No continuation position in the API; max_events is NOT a page cursor.
- Window with MORE than max_events deliverable events -> 400 INVALID_REQUEST
  (narrow from/to, or raise max_events up to the 1000 cap).
- Window exceeding the server's scan limit -> 400 INVALID_REQUEST (narrow
  from/to). Scan-limit VALUE stays server-defined; the normative point is
  the BEHAVIOR (complete-or-400, no silent/implied-continuation partial).
- Removed the "advance from to continue" / paginate-by-lowering-max_events
  guidance entirely.
- Honest limitation: with no continuation, more than max_events (up to the
  1000 cap) deliverable events at a SINGLE timestamp can't be narrowed and
  must be covered by raising max_events; beyond the cap at one timestamp is
  a documented limit (effectively unreachable for governance events).

Rewrote the LIMITS block (COMPLETENESS - ALL-OR-NARROW), updated the 400
response description to match, kept consistency with the existing
max_events 1000 cap + from/to wording. Operation prose, rides the merge:
verified the ALL-OR-NARROW clause + updated 400 description present in
merged/cycles-openapi-admin-merged.yaml and the old pagination framing gone
(zero hits); regenerated byte-stable (md5 identical across two runs).

Note: exact admin error-message wording to be reconciled with a71's final
#210 semantics if a detail needs to align; the normative BEHAVIOR here is
settled.

Prose/normative only — no schema/operation/status change; semantic_base
stays 0.1.25.9. Governance spec + changelog + admin merged only; no runtime
files; same v0.1.25.41 revision. YAML parse OK; validate_changelogs.py
green. Spectral runs in CI. PR remains HELD.
@amavashev

Copy link
Copy Markdown
Contributor Author

Round 3 addendum (c79d5aa) — replayEvents rewritten to ALL-OR-NARROW. PR stays HELD.

The SCAN LIMIT clause from the prior commit introduced invalid continuation guidance: it framed max_events as a resumable pagination cap and told callers to "advance from past the last replayed event". But replayEvents has no continuation position — the accepted response is replay_id / events_queued / estimated_completion_seconds only. With distinct timestamps the caller never learns the last replayed timestamp; with same-millisecond events, re-using inclusive from repeats the page while advancing skips the rest. Unusable.

Decision (no schema change — chosen over adding a continuation cursor): rewrite to ALL-OR-NARROW, matching the parallel cycles-server-admin #210 fix.

Rewritten clause verbatim (replayEvents description → LIMITS)

COMPLETENESS — ALL-OR-NARROW (NORMATIVE): a successful replay delivers EVERY deliverable event in the [from, to] window. It is COMPLETE for that window — never a partial with an implied continuation. There is NO continuation position in the API: the accepted response (replay_id / events_queued / estimated_completion_seconds) carries no cursor or last-replayed marker, and max_events is NOT a resumable page cursor. Consequently:

  • If the window contains MORE than max_events deliverable events, the server MUST reject the request with 400 INVALID_REQUEST (no partial replay). The caller narrows the from/to range, or raises max_events (up to the 1000 cap), so the whole window fits in one request.
  • If the window exceeds the server's SCAN LIMIT (more candidate events than the server will scan while evaluating the request), the server MUST likewise reject with 400 INVALID_REQUEST. The caller narrows the from/to range. The scan-limit VALUE is server-defined (implementation-specific; a reference server documents its own limit); the normative point is the BEHAVIOR — complete-or-400, never a silent or implied-continuation partial.
  • Practical limitation (honest note): because there is no continuation position, a set of MORE than max_events (up to the 1000 cap) deliverable events sharing a SINGLE timestamp cannot be narrowed by from/to to fit and must be covered by raising max_events; a set larger than the 1000 cap at one timestamp is beyond what a single replay can deliver. For governance events this is effectively unreachable.

The 400 response description is updated to match: rejection when the window has MORE than max_events deliverable events or exceeds the scan limit; "replay is all-or-narrow (complete for the window or rejected)… There is no partial replay and no continuation cursor."

Changes from the prior draft

  • Removed the "advance from to continue" / paginate-by-lowering-max_events guidance entirely.
  • max_events is now explicitly not a resumable page cursor; reaching the deliverable-count limit is a 400, not a "page".
  • Kept the scan-limit value server-defined; kept consistency with the existing max_events 1000 cap + from/to wording.
  • Honest limitation stated (single-timestamp > max_events set), without overstating — effectively unreachable for governance events.

Verification

Operation prose, rides the merge normally — confirmed the ALL-OR-NARROW clause + updated 400 description are present in merged/cycles-openapi-admin-merged.yaml, and the old pagination framing is gone (zero hits). Regenerated byte-stable (md5 identical across two runs). YAML parse OK; validate_changelogs.py green. Governance spec + changelog + admin merged only; no runtime files; same v0.1.25.41 revision; semantic_base stays 0.1.25.9. Spectral in CI. PR remains HELD.

Note: happy to reconcile the exact admin error-message string with a71's final #210 semantics if a detail needs to align — the normative behavior here (complete-or-400, no continuation) is settled.

@amavashev

Copy link
Copy Markdown
Contributor Author

External review (codex): SHIP — replayEvents rewritten to ALL-OR-NARROW. The prior pagination/continuation framing (invalid — the API has no continuation position) is fully removed; a successful replay is complete for its window, and more-than-max_events-deliverable or over-scan-limit → 400 (narrow from/to or raise max_events). Matches the cycles-server-admin #210 implementation (being reconfirmed). Re-PARKED ready-to-merge per the hold.

…ness + best-effort enqueue; drop impossible source:replay (v0.1.25.41, held PR #130)

Two reviewer findings on the held #130 replayEvents work, folded into one
commit. Verified both against source.

F1 (P1) — narrow the SUCCESS guarantee to selection-completeness +
best-effort enqueue. The clause claimed "every deliverable event MUST be
delivered on success", but the reference impl enqueues best-effort per
event and returns a 202 with a partial events_queued on a transient
backend enqueue failure — it can't guarantee atomic complete enqueue.
Aligned the spec to that reasonable behavior (chosen over requiring atomic
enqueue):
  - On a 202 the server has SELECTED every deliverable event in [from,to];
    SELECTION is complete-or-narrow (complete, or 400 per max_events / SCAN
    LIMIT — those rules unchanged).
  - ENQUEUE is best-effort; events_queued reports deliveries successfully
    ENQUEUED and MAY be fewer than selected on transient failure. The strong
    part is complete SELECTION (or 400), not atomic enqueue.
  - Replay is NOT idempotent — each replay creates fresh WebhookDeliveries
    and re-delivers matching events, so retrying after a partial enqueue MAY
    duplicate; operators treat a low events_queued as a degraded outcome.
  Kept the ALL-OR-NARROW selection framing + 400-on-over-large-window; only
  the delivery overclaim was narrowed. Updated the events_queued field
  description and the 400 response description to match ("no partial
  SELECTION"; enqueue best-effort).

F2 (P2) — removed the impossible source:"replay" claim. BEHAVIOR said each
delivery is "a new WebhookDelivery with source 'replay'", but the
WebhookDelivery schema is additionalProperties:false with no source
property (required: [delivery_id, subscription_id, event_id, status,
attempted_at]) and the reference model has no such field. Reworded to
"each replayed event is tracked as a new WebhookDelivery" (no source field
added — replay-vs-live provenance would be a separate schema+storage
change, noted as possible future work).

Operation prose, rides the merge: verified the BEST-EFFORT ENQUEUE clause +
events_queued best-effort description present in
merged/cycles-openapi-admin-merged.yaml and the source:"replay" claim gone
(grep 0 in both source and merged); regenerated byte-stable (md5 identical
across two runs).

Prose/normative only — no schema/operation/status change (no source field
added); semantic_base stays 0.1.25.9. Governance spec + changelog + admin
merged only; no runtime files; same v0.1.25.41 revision. YAML parse OK;
validate_changelogs.py green. Spectral runs in CI. PR remains HELD.
@amavashev

Copy link
Copy Markdown
Contributor Author

Round 3 addendum (7f4699f) — two replayEvents findings. PR stays HELD.

F1 (P1) — success guarantee narrowed to selection-completeness + best-effort enqueue

Verified: the reference impl enqueues best-effort per event and returns a 202 with a partial events_queued on a transient backend failure — it can't guarantee atomic complete enqueue. Aligned the spec to that behavior (chosen over requiring atomic enqueue). The strong part (complete SELECTION or 400) stays; only the "every event MUST be delivered on success" enqueue overclaim is narrowed.

Reworded success-guarantee clause verbatim (replayEvents description → LIMITS):

COMPLETENESS — ALL-OR-NARROW SELECTION, BEST-EFFORT ENQUEUE (NORMATIVE): on a 202 accepted response the server has SELECTED every deliverable event in the [from, to] window — selection is COMPLETE for the window, never a partial selection with an implied continuation. Selection completeness is all-or-narrow: the selection is complete, or the request is rejected 400 per the max_events / SCAN LIMIT rules below. There is NO continuation position in the API: the accepted response (replay_id / events_queued / estimated_completion_seconds) carries no cursor or last-replayed marker, and max_events is NOT a resumable page cursor.
  Enqueue is BEST-EFFORT: the server enqueues the selected events for delivery best-effort and events_queued reports the number of deliveries successfully ENQUEUED. On a transient backend failure events_queued MAY be fewer than the number of events selected — the strong guarantee is the COMPLETE SELECTION (or a 400), not an atomic all-or-nothing enqueue. Operators SHOULD treat an events_queued below the expected count as a degraded outcome to investigate. Replay is NOT idempotent: each replay creates fresh WebhookDeliveries and re-delivers every matching event, so retrying after a partial enqueue MAY duplicate already-enqueued events.
Selection rules (all-or-narrow):

  • If the window contains MORE than max_events deliverable events, the server MUST reject the request with 400 INVALID_REQUEST (no partial selection)…
  • If the window exceeds the server's SCAN LIMIT … MUST likewise reject with 400 INVALID_REQUEST … the normative point is the BEHAVIOR — complete-selection-or-400, never a silently-truncated or implied-continuation selection.
  • Practical limitation … more than max_events (up to the 1000 cap) events at a SINGLE timestamp … must be covered by raising max_events … effectively unreachable for governance events.

The events_queued field description now states it reports deliveries successfully ENQUEUED, best-effort, MAY be fewer than selected. The 400 description now says "event SELECTION is all-or-narrow … no partial selection … (Enqueue of the selected events is best-effort — see events_queued)."

F2 (P2) — removed the impossible source: "replay" claim

Verified: WebhookDelivery is additionalProperties: false, required: [delivery_id, subscription_id, event_id, status, attempted_at], no source property (and the reference model has none). The BEHAVIOR line "…a new WebhookDelivery with source 'replay'" was unsatisfiable. Reworded to "each replayed event is tracked as a new WebhookDelivery" — no source field added. Replay-vs-live provenance would be a separate schema + storage change; noted as possible future work in the changelog, not done here.

Verification

source: "replay" claim gone — grep 0 in both the source yaml and merged/cycles-openapi-admin-merged.yaml. Best-effort enqueue clause + events_queued best-effort description present in the merged admin artifact. Regenerated byte-stable (md5 identical across two runs). YAML parse OK; validate_changelogs.py green. Governance spec + changelog + admin merged only; no runtime files; same v0.1.25.41 revision; semantic_base stays 0.1.25.9; no schema change (no source field added). Spectral in CI. PR remains HELD.

…ion (v0.1.25.41, held PR #130)

Codex confirm on #130: the non-idempotency note said each replay
"re-delivers EVERY matching event", contradicting the best-effort-enqueue
clause directly above it (which permits fewer than selected to be
enqueued). Reworded to match best-effort: each replay ATTEMPTS to enqueue
every selected event as a fresh WebhookDelivery, and a retry MAY duplicate
the events that were SUCCESSFULLY enqueued on the prior attempt (those
counted in events_queued) — not "re-delivers every matching event".
Consistent with the events_queued-reports-what-was-enqueued wording. Fixed
in source + changelog copy; merged regenerated byte-stable (corrected
sentence present, old contradiction gone). Prose only; no schema/status
change; same v0.1.25.41 revision; semantic_base stays 0.1.25.9. YAML parse
OK; validate_changelogs.py green. PR remains HELD.
@amavashev

Copy link
Copy Markdown
Contributor Author

Round 3 addendum (37c38d3) — non-idempotency wording contradiction fixed. PR stays HELD.

Codex caught that the non-idempotency note said each replay "re-delivers EVERY matching event", contradicting the best-effort-enqueue clause directly above it (which permits fewer than selected to be enqueued). Reworded to match best-effort and the events_queued-reports-what-was-enqueued wording.

Corrected sentence verbatim:

Replay is NOT idempotent: each replay ATTEMPTS to enqueue every selected event as a fresh WebhookDelivery, so retrying after a partial enqueue MAY duplicate the events that were SUCCESSFULLY enqueued on the prior attempt (those counted in events_queued).

Fixed in the source yaml and the changelog copy. Merged regenerated byte-stable — corrected sentence present in merged/cycles-openapi-admin-merged.yaml, old "re-delivers every matching event" gone (grep 0). Prose only; no schema/status change; same v0.1.25.41 revision; semantic_base stays 0.1.25.9. YAML parse OK; validate_changelogs.py green. PR remains HELD.

@amavashev

Copy link
Copy Markdown
Contributor Author

External review (codex): SHIP — replay success guarantee finalized (complete selection or 400 / best-effort enqueue / events_queued = accepted count / not idempotent), non-idempotency wording reconciled with best-effort, and the impossible source:replay claim removed. Re-PARKED ready-to-merge per the hold.

…(v0.1.25.41, held PR #130)

Reviewer found the events_queued<selected explanation too narrow — it
attributed a shortfall specifically to "a transient backend failure", but
events_queued can be fewer than selected for INTENDED reasons too: the
subscription concurrently deactivated (paused/disabled/deleted), or an
event filtered by a delivery-time guard (the ownership boundary re-checked
at dispatch), between selection and enqueue — not backend degradation.

Broadened the best-effort clause (COMPLETENESS block) and the events_queued
field description to attribute a shortfall to a transient backend failure
OR those intended lifecycle/guard reasons. Kept the "operators SHOULD treat
an unexpectedly low events_queued as worth investigating" spirit but no
longer asserts it's necessarily backend degradation — it may be an intended
lifecycle/guard outcome. cycles-server-admin #210 is classifying these in
parallel; the spec now describes both as possible causes. Consistent with
the best-effort clause + non-idempotency note.

Prose only; no schema/status change; same v0.1.25.41 revision; semantic_base
stays 0.1.25.9. Governance spec + changelog + admin merged only; no runtime
files. YAML parse OK; validate_changelogs.py green; merged byte-stable,
broadened wording present in both the COMPLETENESS clause and events_queued
field. PR remains HELD.
@amavashev

Copy link
Copy Markdown
Contributor Author

Round 3 addendum (8e044d3) — broadened the events_queued < selected causes. PR stays HELD.

The shortfall explanation was too narrow — it attributed events_queued < selected specifically to "a transient backend failure", but a shortfall can also have INTENDED causes (not backend degradation): a subscription concurrently deactivated, or an event filtered by a delivery-time guard, between selection and enqueue.

Broadened sentence verbatim (COMPLETENESS → best-effort clause):

Enqueue is BEST-EFFORT: the server enqueues the selected events for delivery best-effort and events_queued reports the number of deliveries successfully ENQUEUED, which MAY be fewer than the number of events selected — because of a transient backend failure, OR for an intended reason: the subscription was concurrently deactivated (paused / disabled / deleted), or an event was filtered by a delivery-time guard (e.g. the ownership boundary re-checked at dispatch), between selection and enqueue. The strong guarantee is the COMPLETE SELECTION (or a 400), not an atomic all-or-nothing enqueue. Operators SHOULD treat an unexpectedly low events_queued as worth investigating — though it may be an intended lifecycle/guard outcome rather than backend degradation.

The events_queued field description was broadened to match. Kept the "worth investigating" spirit without asserting backend degradation. cycles-server-admin #210 is classifying real-failure vs intended-lifecycle/guard in parallel; the spec now names both as possible causes.

Verification

Broadened wording present in both the COMPLETENESS clause and the events_queued field in merged/cycles-openapi-admin-merged.yaml. Regenerated byte-stable (md5 identical across two runs). YAML parse OK; validate_changelogs.py green. Prose only; no schema/status change; same v0.1.25.41 revision; semantic_base stays 0.1.25.9. PR remains HELD.

@amavashev

Copy link
Copy Markdown
Contributor Author

External review (codex): SHIP — events_queued<selected explanation broadened to attribute a shortfall to a transient backend failure OR intended lifecycle/guard causes (concurrent deactivation, delivery-time guard filtering), not necessarily degradation. Matches the cycles-server-admin #210 structured-result classification. Re-PARKED ready-to-merge per the hold.

@amavashev
amavashev merged commit 155b7cb into main Jul 12, 2026
5 checks passed
@amavashev
amavashev deleted the spec/gov-webhook-invariant-conformance-v0.1.25.41 branch July 12, 2026 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant