spec(governance): webhook selector boundary as cross-plane conformance invariant + impl-pending honesty + migration fix (v0.1.25.41) [HELD]#130
Conversation
…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.
Round 2 (4c7256b) — six findings (3 human + codex A/B/C); PR stays HELDCodex 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 modelThe 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):
F1/F3 + codex A — impl-pending status moved out of the contract; "implemented today" caveat completed everywhereVerified 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 Zero-survivor sweep ("implement…today" / "fixed in parallel" / "reference servers implement"):
Zero survivors in the target class. F2 — the invariant now SURVIVES merge generation (load-bearing)
Codex B — provisioning-neutral violation handlingAdded 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 Codex C — README governance-base bump
ValidationYAML parse OK; |
…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.
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
Final sweep of F2 — guard now fails loud on a missing END marker (and out-of-order/empty)Round 2's F1 — three accounting fixes
Validation
|
…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.
Round 3 addendum (8eb1258) — INVARIANT 2 webhook-test carve-out. PR stays HELD.The cycles-server-admin implementation (#210) surfaced that the Verified against source before wording: Exact exception wording added (INVARIANT 2,
|
|
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.
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 change — Exact clause added (replayEvents
|
|
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.
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 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
|
|
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.
Round 3 addendum (7f4699f) — two replayEvents findings. PR stays HELD.F1 (P1) — success guarantee narrowed to selection-completeness + best-effort enqueueVerified: the reference impl enqueues best-effort per event and returns a 202 with a partial Reworded success-guarantee clause verbatim (replayEvents
The F2 (P2) — removed the impossible
|
…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.
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 Corrected sentence verbatim:
Fixed in the source yaml and the changelog copy. Merged regenerated byte-stable — corrected sentence present in |
|
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.
Round 3 addendum (8e044d3) — broadened the
|
|
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. |
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.
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/mainlatest release is v0.1.25.50 without the boundary (WebhookAdminController.create/updatehave 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.summaryand 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 impl-pending wording (SPEC FAMILY CONTEXT, verbatim):
Finding 2 (P1) — the NORMATIVE boundary was outside the conformance surface
The two admin webhook ops are
x-conformance: referenceand 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'sx-conformancelabel or which provisioning mechanism a server exposes — and added it to CONFORMANCE.md's §MUST normative surface: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: normativewould (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 onlyevent_categoriesset, butWebhookCreateRequeststill requiresevent_types(minItems:1, kept on create in .39) — so that create is rejected. Corrected recipe (verbatim,createWebhookSubscriptionprose):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.pygreen; admin merged regenerated byte-stable (second run identical → merge-check passes). Spectral runs in CI.