Skip to content

feat(reporting): buyer-computable coverage for reporting subscriptions (#3504, 1/3) - #3505

Merged
bokelley merged 14 commits into
mainfrom
bokelley/reporting-subscription-coverage
Oct 31, 2026
Merged

bokelley merged 14 commits into
mainfrom
bokelley/reporting-subscription-coverage

Conversation

@bokelley

@bokelley bokelley commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

First of a short series for #3504 (AdCP 3.3 reporting subscriptions in the reliable-reporting runtime). This slice is the buyer-side pure function, the capability builder, and the typed immutable-configuration error. The seller principal runtime and obligation engine follow in a stacked PR.

What ships

  • expectedSubscriptionPeriods(input) (exported from @adcp/sdk/reporting/consumer and @adcp/sdk/reporting/ledger). A pure function of plain JSON the buyer already holds. It returns every owed (subscription_version, media_buy_id, offering_id, period). groupExpectedSubscriptionPeriodsV1 groups those rows into the obligations that must carry them, with frozen, byte-sorted media_buy_ids.
  • createReportingPeriodCalendarV1 implements the normative period_generation for utc, account_timezone, source_timezone and billing_cycle.
  • resolveReportingSubscriptionOfferingsV1 handles exact, finest and all, with ties going to the lowest offering_id in byte order. reportingSubscriptionScopeIncludesV1 decides scope membership.
  • reportingConfigurationCapabilitiesV1 builds configuration_tasks, the deprecated configuration_task (emitted only when sync_accounts is listed) and the subscriptions block. It is a pure builder: nothing in this PR changes what the SDK advertises.
  • ReportingConfigurationImmutableError (code: 'REPORTING_CONFIGURATION_IMMUTABLE', with accountId, deliveryConfigId and deliveryConfigVersion) plus isReportingConfigurationImmutableError. The message text is unchanged, so hosts that match on it keep working.

Cross-SDK parity table

test/fixtures/reporting-subscriptions/coverage-cases.json holds 81 cases. Each has an input and either expected_error, or the exact expected rows plus expected_obligations (rows grouped by version, offering and period, with byte-sorted media_buy_ids). A case whose input needs data no AdCP response carries lists that data in non_wire_inputs. The expected rows were derived by hand from the spec rule, not by running this code. Compare instants, not strings: TS emits toISOString() form. Rows are ordered by period_start, subscription_version, offering_id and media_buy_id (byte order). Expected rows leave out subscription_id, account_id and report_definition_id, which come straight from the input. test/lib/reporting-subscription-coverage.test.js runs the table strictly: it checks rows and obligations, and every case must have one or the other. It also adds invariant, calendar and selection tests.

The table covers:

  • mid-period confirmed_at, and a buy confirmed long before its flight
  • cancellation, and early completion on a boundary
  • an extension (an elapsed period never becomes owed) and reinstatement
  • now cut-off and an unconfirmed buy
  • the authorized_at refinement
  • version cutovers with aligned and misaligned boundaries, deactivation, reactivation, and an account that is new in v2
  • exclude_account_ids and operator_domains
  • revocation, and re-grant with and without backfill or after retention expiry
  • seller gaps, and exceptions that only remove periods
  • max_accounts capacity, and precedence (overridden)
  • exact / finest / all over package, package × creative, package × country and package × unmapped device
  • product eligibility, and finality, purpose and duration filters
  • a resolution change as a cutover
  • account timezone, through DST and through a timezone cutover
  • the period_timezone override
  • hourly and unclipped periods, a monthly billing cycle with month-end clamping, and destination method matching
  • obligation grouping
  • error cases

The rule as implemented (for parity)

Per buy (confirmed only), split time at every version accepted_at / superseded_at, deactivation and reactivation instant, offering available_from / withdrawn_at, and timezone cutover. Each segment that has an active, in-scope version and an unchanged resolution forms a regime. Resolution is the union, over the buy's packages, of the selector resolution against that package's reporting_delivery_offering_ids.

For each regime and resolved offering o with calendar c:

  1. Find handoff. With no adjacent predecessor regime, or a predecessor that resolved nothing, it is the regime start. Otherwise it is the cutover end E: c_o_old.ceil(T) when o continues, else the max of ceil(T) over the predecessor's calendars. When two cutovers fall inside one period, the incoming handoff is max(E, the predecessor's own handoff), so the owing start carries forward.
  2. Owe periods [s, e) of c where all of the following hold:
    • s ≥ max(c.ceil(handoff), c.floor(confirmed_at)). Under adcp#8593 every period ending after confirmed_at is owed, so the period in which the buy is confirmed is owed in full. This is firstOwedBoundary, the one place the start rule lives.
    • s < regime.until
    • e ≤ now
    • e > s
    • the period overlaps the flight in effect at e (s < stop, e > start_time, where stop = min(end_time, canceled_at, completed_at)). A prior flight applies while e < replaced_at. A flight whose stop is at or before its start overlaps nothing.
    • authority holds (below)
    • no excluded[] entry without package_ids, and no gaps[] entry, has from ≤ s < until. An exception without media_buy_id is account-wide.

Authority comes from the ordered grants. A re-grant before unauthorized_at + status_retention_days continues the chain; a later one starts a fresh chain. A period is authorized when, for some chain:

  • s ≥ c.ceil(chain.first_authorized_at)
  • s < c.floor(final_unauthorized_at), when the chain has ended
  • without backfill, s is not in [c.floor(unauthorized_at), c.ceil(regranted_at)) for any interruption

Under utc alignment, and for any duration with a date component, years, months and days are local civil arithmetic: the day is clamped from the origin's day, a gap advances, and a fold takes the earlier instant. Under utc, hours, minutes and seconds are exact elapsed time. Under a local alignment, a sub-daily duration restarts at each local midnight on wall-clock multiples (adcp#8588). A DST gap yields no boundary, and a fold yields one boundary per offset. The duration must divide 24 hours, and mixing date and time components is refused. Interval zero of a billing_cycle is the anchor itself. Zero-length periods (a skipped civil date) are never owed.

Deviations from the issue

  • Issue item 7's start rule omits authorized_at. This follows the spec as amended by adcp#8593, the owner decision for beta.2. The start bound is the first boundary at or after the later of accepted_at and authorized_at, and a buy is owed every period that ends after its confirmed_at.
  • Issue item 7 says the function returns "the consumer runtime's expected-period type". ExpectedReportingPeriod needs destination, verification and schema pins that a subscription doesn't carry. So it returns a narrower JSON row type plus the obligation grouping; wiring into the consumer runtime is in the third slice.
  • Item 5 (capabilities) ships as a pure builder. The next PR wires it into createReliableReportingService and adds media_buy.reporting_subscriptions to experimental_features, together with the runtime and the adcp.principal block, so the SDK never advertises subscriptions it cannot serve or a 3.3-invalid capabilities document. This follows review: the first push added the feature id here.

Conformance to adcontextprotocol/adcp#8585 (commit f8b2fddd)

#8585 settled the ambiguities this PR first listed. Changes:

  • Matching reads the wire. It uses each offering's published standard_dimensions and unmapped_dimensions; the host-supplied dimensions field is gone. An offering without standard_dimensions fails closed (TypeError).
  • period_timezone. A utc offering matches only UTC or Etc/UTC (the earlier alias list was broader). A source_timezone offering matches only its own published zone, so an account-resolved one never matches (it used to). An account_timezone offering uses the override; a buyer-configured billing_cycle never matches.
  • Reactivation comes from the readback: the input takes top-level inactive_intervals in wire shape ({deactivated_at, reactivated_at?}) in place of the per-version, buyer-recorded intervals.
  • Timezone cutovers carry previous_timezone (required; TypeError without it) and an optional offering_id. account.timezone is now the account's current zone, as list_accounts shows it. Gaps may carry offering_id. An entry without offering_id applies to every offering.

Rules re-confirmed against the normative text, unchanged: match: exact without dimensions (lowest offering_id wins), years/months/days civil and hours/minutes/seconds elapsed, zero-length periods never owed, per-offering E (including "a predecessor that resolved nothing starts at T"), authorization chains, and scope_resolved_at = period end.

Parity table: the Python copy must be re-vendored. coverage-cases.json now has 54 cases:

  • Fix. Three cases (the monthly billing cycle, the period_timezone override, and "account new in a later version") had a scope with both keys, which maxProperties: 1 forbids. A merge in my generator caused it; scope is now replaced, never merged.
  • Changed inputs. Offerings use standard_dimensions. Reactivation uses top-level inactive_intervals. The timezone-cutover case uses previous_timezone, with account.timezone as the current zone.
  • New cases: a cutover scoped to one offering; a gap scoped to one offering; exact without dimensions; period_timezone against account-resolved and fixed source_timezone offerings; a utc offering with GMT; and error cases for a missing previous_timezone and a missing standard_dimensions.
  • Guard. A test now validates every subscription in the table against the subscription schema, so an invalid scope can't slip in again.

Generated types. The new fields are hand-modeled in src/lib/reporting/subscriptions/wire.ts, with TODO(3.3.0-beta.2) to regenerate. I tried the repo's existing unreleased-bundle mechanism: a commit-addressed PR bundle plus schemas/codegen-provenance.json, which works with the #8585 head commit's bundle. I backed it out for two reasons:

  • release.yml syncs with ADCP_REQUIRE_SIGNATURE=1, and PR bundles are unsigned, so merging it would block the beta release until someone re-syncs.
  • It would also pull unrelated unreleased spec changes (ACCOUNT_CHANGE_REQUIRES_NEW_ACCOUNT, allow_separate_account) into the published SDK.

Expert review round (2026-10-29), commits a99597a4 and f8b48c0c

Findings fixed in this PR:

  • Empty flight. A flight whose stop is at or before its start owes nothing.
  • Two cutovers in one period. The incoming offering carries the predecessor's own handoff (max(E, carried)), so the second cutover never re-owes an overlapping period.
  • Unknown dimensions parse. reporting-dimension is x-extensible, so strict request validation no longer rejects a dimension the bundle doesn't list. The schema loader relaxes that one enum to its x-pattern; other x-extensible enums are unchanged (flagged below). A selector value an offering doesn't publish matches nothing.
  • Selection. utc matches only UTC and Etc/UTC. A source_timezone offering matches a period_timezone only when its own published zone equals it.
  • Bounded work. The loop steps by instant with calendar.next, skips whole removed stretches, and stops at max_iterations (default 2,000,000) and max_periods.
  • adcp#8588. Sub-daily local periods restart at local midnight. Mixing date and time components is refused under a local alignment.
  • adcp#8593. The confirmation period is owed (see the rule above). The rule is isolated in firstOwedBoundary.

Parity table, from 54 to 81 cases:

  • The runner is strict. It checks expected_obligations as well as rows.
  • New cases:
    • empty flight
    • two cutovers in one period
    • 31st and Feb 29 billing anchors
    • DST gap and fold at local midnight
    • zero-length period
    • PT24H vs P1D
    • sub-daily local periods (Singapore, and the New York fall-back day)
    • a re-grant chain of 3+ grants
    • simultaneous version and timezone change at the same T
    • the reviewer's cases (a) to (i)
    • an ASAP buy confirmed mid-day
    • a monthly billing buy confirmed on the 12th
  • #8593 changes 9 existing expectations, listed in the parity comment on this PR.

Spec ambiguities left open (please confirm):

  • Under #8588's literal reading, PT24H with a local alignment equals P1D. The table encodes that.
  • Only reporting-dimension gets the x-extensible relaxation. Validating x-extensible enums in general is an SDK-wide question.
  • The table models several inputs no AdCP response carries, each listed in non_wire_inputs: prior_flights, offering available_from/withdrawn_at, and source_timezone. Buyers must retain them.
  • excluded[] has no offering_id, so with several calendars one exception's from is account-wide.
  • E when the outgoing side owed nothing is taken as T itself.
  • Boundaries depend on ICU tzdata, which can drift between runtimes.

Local checks

  • npm run typecheck, build:lib, lint:boundaries and check:package-size all pass. The file budget is +20, for five dual-format module sets.
  • CI was green on the first push (43 pass, 2 skipped).
  • Prettier passes on the changed files.
  • reporting-subscription-coverage.test.js passes 106/106, and reliable-reporting-service.test.js passes.

Refs #3504

🤖 Generated with Claude Code

Add the pure expectedSubscriptionPeriods rule for AdCP 3.3 principal-level
reporting subscriptions (experimental media_buy.reporting_subscriptions),
the normative period_generation calendar it uses, selector resolution
(exact, finest, all with byte-order ties), scope membership, and a builder
for the 3.3 registration capabilities (configuration_tasks, the deprecated
configuration_task, and the subscriptions block).

createAdcpServerFromPlatform adds media_buy.reporting_subscriptions to
experimental_features only beside a subscriptions block.

installConfiguration now throws a typed ReportingConfigurationImmutableError
with the unchanged message.

The cross-SDK parity table is
test/fixtures/reporting-subscriptions/coverage-cases.json.

Refs #3504

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 29, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — buyer-computable reporting-subscription coverage (slice 1/3).

Checked: five new source modules read in full (coverage.ts, calendar.ts, selection.ts, capabilities.ts, index.ts), from-platform.ts, changeset, package-size guard, and both test files. No no-auto-approve team match; gated_paths false; high_risk false; no prior decision.

Verified the unbounded ordinal loop at coverage.ts:794 is bounded by start >= latestStop and fails closed on max_periods (L814-816); authority/grant chain, cutover handoff, flight-at-resolution, timezone/exception-window logic are consistent and byte-order-stable; the 32-entry experimental_features guard rewrite is logically equivalent; changeset correctly minor for purely additive exports; no witness/fabrication issues (buyer-side pure computation). 67-case parity table plus invariant/calendar/selection tests are thorough.

Medium findings

  • from-platform.ts:3169 — media_buy.reporting_subscriptions advertised before the required adcp.principal block (allOf[11]) is emitted, so an opt-in adopter could emit a 3.3-invalid capabilities response (documented ambiguity #11).

Single medium finding, no blocking issues, no escalation trigger — falls through to row 9.

Medium findings

  • src/lib/server/decisioning/runtime/from-platform.ts:3169 — reporting_subscriptions advertised before required adcp.principal block exists

…al ships

Advertising media_buy.reporting_subscriptions also requires
adcp.principal.supported_sections to list reporting_subscriptions, which the
SDK does not emit yet. Keep the capability builder pure in this slice and add
the feature id together with the principal block in the runtime slice, so an
adopter can never produce a 3.3-invalid capabilities response.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 29, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — prior Medium resolved, no current findings.

This is a subsequent push responding to the earlier review. The sole prior Medium (from-platform.ts:3169 — media_buy.reporting_subscriptions advertised before its required adcp.principal companion block existed) has been fixed: from-platform.ts now advertises only media_buy.reporting_delivery in experimental_features and drops the premature id, which is fail-closed. The 32-cap guard recomputes correctly for the single-id case, and the changeset prose and test were updated to match.

Current run surfaces no findings. Decision table: rows 1–8 do not fire (no critical/high, gated_paths false, high_risk false, no medium findings, no team gate, <3 mediums), so this falls through to row 9 → approve. Note: gated_paths is false, so the REVIEW_REQUIRED status does not force a gate here.

bokelley added a commit to adcontextprotocol/adcp-client-python that referenced this pull request Oct 29, 2026
… SDK does

Align expected_subscription_periods with adcontextprotocol/adcp-client#3505 so
that both SDKs owe identical periods. The spec leaves each of these choices
open:

- A dedicated period calendar. Years, months and days are local civil
  arithmetic with the day clamped from the origin. Hours, minutes and seconds
  are exact elapsed time. Calendar months (P1M billing cycles) are supported,
  and numeric UTC offsets are refused.
- Cutovers are paired per buy over the union of its packages. A continuing
  offering hands off at E of its own outgoing calendar. A newly resolved one
  starts at the latest E of everything the previous regime resolved.
- A period_timezone override moves account_timezone and account-resolved
  schedules into its zone, while utc and fixed-zone schedules match only
  their own zone.
- Versions may carry in-place inactive intervals and their destination
  method. The coverage entry's timezone_cutovers are applied.
- A re-grant continues authority only before unauthorized_at plus
  status_retention_days, and a re-grant without retention is an error.

The TypeScript SDK's 46-case table is vendored and run as a parity test.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adopt the rules adcontextprotocol/adcp#8585 settled:

- Selection reads each offering's published standard_dimensions and
  unmapped_dimensions, and fails closed on an offering without them.
- period_timezone: a utc offering matches only UTC or Etc/UTC; a
  source_timezone offering only its own published zone (never an
  account-resolved one).
- The coverage function takes the readback's inactive_intervals in wire
  shape, per-offering timezone_cutovers with previous_timezone
  (account.timezone is now the current zone), and per-offering gaps.
- The parity table fixes the billing-cycle case's scope (both keys, which
  maxProperties 1 forbids), adds cases for the settled rules, and its
  subscriptions are now schema-validated.

The new wire fields are hand-modeled in subscriptions/wire.ts until
3.3.0-beta.2 ships.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 29, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean pass, no blocking or medium findings.

Checked:

  • Wire-shape fidelity: new wire.ts aligns hand-modeled 3.3 reporting-subscription types to settled spec #8585 (per-offering standard_dimensions/unmapped_dimensions, readback inactive_intervals, per-offering timezone_cutovers/gaps, previous_timezone). Witness-not-translator holds — no fabrication or silent normalization.
  • coverage.ts: timezoneFor (L234-248) fail-closes on missing previous_timezone; resolveForBuy (L1235-1237) routes selector override vs account zone correctly; global inactive_intervals merge (L939) sound given buildRegimes selects one version per boundary.
  • selection.ts standard() throws when standard_dimensions absent — no guessing. Every new error branch has a fixture case (L2079/L2158/L2246).
  • Changeset present and minor-correct: the expectedSubscriptionPeriods surface is new in this unmerged PR, so reshaping its input breaks no published API.

Decision table: no critical/high (row 1 n/a); gated_paths false (row 2 n/a); high_risk false (rows 3,5 n/a); no medium findings (rows 4,8 n/a); prior decision was approve (row 6 n/a); no no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve. (review_decision REVIEW_REQUIRED does not force escalate because gated_paths is false.)

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: buyer-computable coverage (#3505), checked against adcontextprotocol/adcp main (42dc2467, includes #8585)

I reviewed calendar.ts, selection.ts, coverage.ts, wire.ts, capabilities.ts and the parity table against core/reporting-subscription.json x-adcp-validation, core/reporting-schedule.json period_generation, core/reporting-subscription-{state,coverage}.json, core/reporting-schedule-offering.json, and the get_reporting_status / sync_principal docs. I also ran the calendar and coverage function directly against cases I worked out from the spec.

Summary. The pure function matches the spec on every rule I checked:

  • Start rule. Coverage starts at ceil of the latest of accepted_at, confirmed_at and authorized_at. The code takes the max of the three ceilings, which is equivalent because ceil is monotone.
  • Flight. The flight is evaluated at the period end. Periods are never clipped.
  • Cutover. E is computed per incoming offering. I verified a match: all v1→v2 change where B (New York) is added beside A (UTC): A hands off at 11-05T00Z and B starts at 11-05T05Z.
  • Reactivation, chains and gaps. Reactivation is a fresh start. Authorization chains and retention work, and gaps follow floor(U)/ceil(R). Exceptions only ever remove periods.
  • Selection. exact, finest and all work, and ties go to the lowest offering_id in byte order (B beats a). Unmapped dimensions are handled, as are the period_timezone matrix and the configurable billing_cycle exclusion.
  • Period arithmetic. Civil and elapsed arithmetic are correct. I checked midnight DST gaps and folds (Havana, Santiago, Beirut), month-end clamping, and Apia's zero-length day.

The problems are at the edges: inputs the buyer cannot get from the wire, a few untested spec corners, and parity-table claims that go further than the table does.

Confirmed issues

1. Medium: "computable from the wire" does not hold for three inputs, and the function silently assumes the favorable default when they are missing.

  • selection.ts:24-26,124-127 and coverage.ts:625-628: available_from and withdrawn_at are SDK-invented offering fields. No AdCP schema carries them, and a withdrawn offering simply disappears from reporting_delivery.offerings. Failure scenario: finest [package], and the seller adds package x creative on 11-04T12. A buyer passes the offerings exactly as published today, with no available_from. It expects off_b from 11-03 and never expects off_a (11-03, 11-04), while the seller owes off_a for 11-03 and 11-04, then off_b from 11-05. That is the table case "resolution change is a cutover" with the non-wire field removed.
  • coverage.ts:94-95: prior_flights is not on the wire either, because get_media_buys shows only the current flight. Failure scenario: a buy ends 11-06 and is extended to 11-08 at 11-07T12. A buyer without history expects 11-06, which the seller never owes (table case "flight extension adds only unresolved periods"). Reinstatement is the same.
  • coverage.ts:117-121: source_timezone for account_resolved offerings can only be learned from an obligation's period.source_timezone, so the buyer cannot know it before the first obligation exists.

Fix. Make these inputs explicit rather than optional-with-a-default. For example, require prior_flights: [] and an offering_history acknowledgment, or document that the buyer MUST retain them. Raise a spec issue so that offering availability and flight history can be recovered from the wire. Otherwise "the seller owes exactly what the buyer computes" is true only for buyers that kept local history.

2. Medium: the parity table is narrower than the PR body says, and the cross-SDK corners are not in it. The cases I checked match the spec; I spot-checked the misaligned cutover, DST, timezone cutover, reactivation, resolution change and billing cycle. The gaps are these:

  • "a monthly billing cycle with month-end clamping": the table's billing case is anchored on the 1st (2026-01-01T05:00:00Z), so nothing clamps. Clamping is tested only in the TypeScript unit test (reporting-subscription-coverage.test.js:230), and the Python copy never sees it.
  • Several calendar edge cases are TypeScript-only or missing entirely:
    • a DST gap or fold at a midnight boundary (Havana, Santiago, Beirut); the table's New York cases never put midnight in a gap
    • a zero-length period (unit test only)
    • PT24H against P1D across DST (unit test only, and it uses an elapsed duration without comparing it to the civil one)
  • Missing cases I would add, all derived from the normative text:
    • (a) match: all with a new offering on a different alignment (per-incoming-offering E for a non-continuing offering, with a stretch owed by neither)
    • (b) a version change with T exactly on a boundary
    • (c) reactivation exactly on a boundary, and an inactive interval spanning a version change
    • (d) two authorization chains (expired re-grant, then a second revocation)
    • (e) several interruptions inside one chain, with and without backfill
    • (f) exception from/until values that fall between boundaries
    • (g) a finest tie between B and a (byte order rather than locale order)
    • (h) a billing anchor on the 31st and on the 29th of February
    • (i) a stop time equal to the flight start, and a buy canceled before start_time
  • The table cases for resolution change and flight extension depend on the non-wire fields in item 1. Label them as such so the Python implementation does not treat them as wire-derivable.

3. Low: a billing_cycle anchor inside a DST fold does not reproduce the anchor. calendar.ts:201-202,238: the anchor is converted to wall time and back, and the fold rule picks the earlier offset. Verified: period_anchor: 2026-11-01T06:30:00Z (01:30 EST, the second 01:30) in America/New_York gives boundary(0) = 05:30Z, and ceil(anchor) jumps to December. The spec makes interval zero the anchor instant. Fix: return the anchor itself for ordinal 0. More generally, when a computed wall time equals the origin's wall time, resolve the fold with the anchor's own offset.

4. Low: building grants from wire values throws on a continued chain. The doc comment says authorized_at is "when this grant began" (coverage.ts:99-104). Under #8585, authorization.authorized_at stays at the chain's first grant after a re-grant within retention. A buyer that appends the re-grant using the wire value gets [{A, U}, {A}] and TypeError: Grants must not overlap (coverage.ts:439). Fix: document that the field is the re-grant instant the buyer observed, or treat authorized_at equal to the chain start after a revocation as a continuation with an unknown re-grant instant, relying on the seller's gaps[].

5. Low: the iteration count is unbounded even though output is capped. max_periods caps pushed rows, but the ordinal loop (coverage.ts:264-287) still walks every boundary up to min(regime.until, latestStop, now), including removed or unauthorized ones. Each non-UTC boundary costs two to four Intl.formatToParts calls, and the cache is cleared every 4096 entries. A PT1S or PT1M timezone-aligned offering over a long flight with an open exception does millions of Intl calls. Fix: skip ahead with ceilOrdinal past an exception's or gap's until, or add an iteration budget.

Questions (spec ambiguities both SDKs need to pin)

  • Stop time on a boundary. coverage.ts:271 uses half-open overlap, so completed_at = 11-04T00:00Z owes nothing for the 11-04 period. The normative text also says "the period containing the stop time is owed in full", and the instant 11-04T00:00 is contained in [11-04, 11-05). Your reading is the sensible one, since an end_time at midnight is usually exclusive. A zero-length flight (start == stop at 11-03T12) owes the 11-03 period under the current code, which is consistent with "containing" but not with "overlap". Worth a one-line spec clarification and a table case for each.
  • E when the outgoing side resolved something but owed nothing. For example, its flight had not started, or it was accepted mid-period. The code uses the schedule-based ceil(T), which can create a stretch owed by neither. The spec says "the end of the last of them", meaning the owed periods. Please confirm the Python SDK reads it the same way.
  • ICU tzdata drift. Boundaries come from the runtime's ICU tz database, and the buyer and seller may run Node versions with different tzdata, for example after a zone's rules change. Is that acceptable, or should the obligation's echoed boundaries take precedence when they disagree?

I have nothing to report on capabilities.ts or ReportingConfigurationImmutableError.

- An empty flight (stop at or before start) overlaps nothing.
- A cutover carries the outgoing stretch's own owing start into E, so two
  cutovers inside one period never owe a period twice.
- Sub-daily periods under a local alignment restart at each local midnight
  on wall-clock multiples (adcp#8588); calendars gain next() and the walk
  iterates by instant with an iteration budget and skip-ahead.
- A billing anchor in a DST fold is interval zero itself.
- A wire authorized_at equal to the chain start continues the chain.
- The start rule lives in one function for the pending owner decision.
- Schema validation accepts unknown reporting-dimension values.
- The parity table grows to 79 cases with obligations on every case and
  non-wire inputs labelled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

Parity table update for the Python SDK (a99597a4). test/fixtures/reporting-subscriptions/coverage-cases.json now has 79 cases. Please re-vendor it.

  • Strict runner. Every case now carries expected_obligations: the rows grouped by (subscription_version, offering_id, period), with byte-sorted media_buy_ids, in row order. The TS runner compares both rows and obligations in order.
  • non_wire_inputs labels cases whose input uses data no AdCP response carries (buys[].prior_flights, offerings[].available_from/withdrawn_at). A buyer must retain these itself.
  • New cases:
    • an empty flight (canceled before start_time inside the same period; end_time == start_time)
    • two cutovers inside one period (E carries the predecessor stretch's own start)
    • simultaneous version and timezone change at the same T
    • billing anchors on the 31st and on Feb 29
    • Santiago midnight DST fold and gap
    • Apia zero-length day
    • PT24H vs P1D (utc, and local under #8588)
    • sub-daily local periods (Singapore hours, New York 25-hour day)
    • re-grant chains of 3 with and without backfill, and a re-grant seen only through the chain-start authorized_at
    • two authorization chains
    • match: all adding an offering on another alignment
    • a version change, and reactivation, exactly on a boundary
    • an inactive interval spanning a version change
    • exception bounds between boundaries
    • a finest tie B vs a
    • unknown dimension values
  • Rules to align on:
    1. An empty flight (stop <= start) owes nothing.
    2. E carry-forward: E = max(ceil_outgoing(T), predecessor's own handoff), using the same offering's handoff when it continues, else the max over the predecessor's handoffs.
    3. #8588: a local alignment with a duration made only of H/M/S must divide 24h; boundaries are local wall-clock multiples from each local midnight (a gap yields none, a fold yields one per offset); date+time mixed is a TypeError. Read literally, this makes PT24H under a local alignment equal to P1D; I've flagged that reading on the spec side.
    4. A grant whose authorized_at is at or before the previous unauthorized_at continues the chain with an unknown re-grant instant: no internal gap is computed, and the seller's gaps[] applies.
    5. An unknown selector dimension matches nothing unless an offering publishes that same value in standard_dimensions.
    6. The start rule is in one function (firstOwedBoundary) pending the owner decision on the confirmed_at period.

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 29, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — subsequent pass on a previously clean-approved PR, no blocking or medium findings.

Checked (decision table, top-down):

  • Row 1 (critical/high): no critical/high findings.
  • Row 2 (gated_paths): gated_paths is false — does not fire despite review_decision=REVIEW_REQUIRED.
  • Row 3 (deleted high-risk): high_risk is false; no deletions.
  • Row 4/5 (medium + category/high-risk): no medium findings.
  • Row 6 (sticky escalate): prior decision was approve, not escalate.
  • Row 7 (no-auto-approve team): no team match.
  • Row 8 (≥3 medium): zero medium findings.
  • Row 9: approve.

What was reviewed: 7 delta files on the reporting-subscriptions coverage work. calendar.ts sub-daily walk (instantsForWall, next()=ceil(instant+1), ordinal-0→anchorInstant override) preserves boundary monotonicity and guarantees walk progress. coverage.ts next()-based walk is equivalent to the old ordinal stepping, coveringRemoval skip-ahead skips only in-window starts, the authorizedAt<=previousEnd continuation defers to seller gaps[] (not an auth gate), and the iteration budget bounds sub-daily walks. schema-loader.ts enum opening is idempotent and intentional per the changeset with a mirroring parity test. Changeset is minor — correct for additive exports (satisfies changeset-vs-wire-impact). No witness-not-translator violations and no version-pathway gaps surfaced.

The start bound is the first boundary at or after the later of the
version's accepted_at and the account's authorized_at; a buy is owed every
period that ends after its confirmed_at and overlaps its flight, so the
period in which the buy is confirmed is owed in full. The rule stays in
firstOwedBoundary. Parity table: the mid-period confirmed_at and hourly
cases change, plus every case whose first period contains confirmed_at;
new cases for an ASAP buy confirmed mid-day and a monthly billing buy
confirmed on the 12th.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

Parity update for adcp#8593 (owner decision on the start rule), pushed as f8b48c0:

  • Start bound: the first boundary at or after the later of the version's accepted_at and the account's authorized_at.
  • A buy is owed every period that ends after its confirmed_at and overlaps its flight. The period in which a buy is confirmed is owed in full; confirmed_at no longer moves the start to the next boundary. In code this is max(ceil(handoff), floor(confirmed_at)) in firstOwedBoundary.
  • A flight whose stop is at or before its start owes nothing (unchanged).

test/fixtures/reporting-subscriptions/coverage-cases.json now has 81 cases.

Changed expectations (the first period containing confirmed_at is now owed):

  • baseline (rule text only)
  • mid-period confirmed_at
  • hourly periods are never clipped
  • account timezone
  • multiple buys group into obligations
  • period_timezone matches a fixed source_timezone offering in the same zone
  • two cutovers inside one period
  • simultaneous version and timezone change at the same T
  • sub-daily local periods start on the local hour (Asia/Singapore)

New cases:

  • ASAP buy confirmed mid-day owes the confirmation day
  • monthly billing buy confirmed on the 12th owes that month

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 29, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean, surgical start-rule refinement in reporting subscription coverage.

What I checked:

  • The adcp#8593 change: firstOwedBoundary now returns max(ceil(handoff), floor(confirmedAt)) instead of max(ceil(handoff), ceil(confirmedAt)), correctly making the period in which a buy is confirmed owed in full. The handoff floor and downstream flight-overlap filter are preserved, and the on-boundary case is unchanged.
  • Changeset present and correctly typed as minor with updated prose — satisfies changeset-vs-wire-impact for the src/lib/** touch.
  • Extensive new data-driven fixture coverage (ASAP, monthly billing, account-tz, hourly, sub-daily local) — new branch is tested, no silent regression risk.
  • No witness-not-translator concerns, no version-translation pathway gap, no official-transport-client issues.

No critical/high/medium findings. Prior pass was also clean. gated_paths is false, high_risk is false, no author team gate. review_decision is REVIEW_REQUIRED but gated_paths is false, so row 2 does not fire. Falls through to row 9 → approve.

bokelley added a commit to adcontextprotocol/adcp-client-python that referenced this pull request Oct 29, 2026
…r capabilities (#1696)

* feat(reporting): compute owed reporting-subscription periods and their capabilities

Add adcp.reporting.subscriptions, the shared half of AdCP 3.3 principal-level
reporting subscriptions (media_buy.reporting_subscriptions):

- expected_subscription_periods(): the coverage rule from
  core/reporting-subscription.json as a pure function over plain data. It
  owes each confirmed buy and resolved offering the complete periods that
  start at or after the latest of the version's accepted_at, the buy's
  confirmed_at and the account's authorized_at, and that overlap the flight
  as of each period's scope_resolved_at. It applies cutovers for versions,
  deactivation, resolution changes and account timezone changes, removes
  exceptions and gaps over [from, until), and handles revocation and
  re-grant with and without backfill_on_regrant. Results are
  ExpectedReportingPeriod values, so they feed evaluate_reporting_ledger and
  the consumer-status planner unchanged.
- resolve_subscription_offerings(): exact / finest / all selection with ties
  to the lowest offering_id in byte order.
- subscription_capabilities() and advertise_reporting_subscriptions():
  configuration_tasks, the deprecated configuration_task only when
  sync_accounts is listed, the subscriptions block, the feature id and the
  principal section.

Civil-time schedules carry the normative local-midnight origin explicitly, so
they do not depend on the ledger's legacy anchor default.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(reporting): derive subscription periods exactly as the TypeScript SDK does

Align expected_subscription_periods with adcontextprotocol/adcp-client#3505 so
that both SDKs owe identical periods. The spec leaves each of these choices
open:

- A dedicated period calendar. Years, months and days are local civil
  arithmetic with the day clamped from the origin. Hours, minutes and seconds
  are exact elapsed time. Calendar months (P1M billing cycles) are supported,
  and numeric UTC offsets are refused.
- Cutovers are paired per buy over the union of its packages. A continuing
  offering hands off at E of its own outgoing calendar. A newly resolved one
  starts at the latest E of everything the previous regime resolved.
- A period_timezone override moves account_timezone and account-resolved
  schedules into its zone, while utc and fixed-zone schedules match only
  their own zone.
- Versions may carry in-place inactive intervals and their destination
  method. The coverage entry's timezone_cutovers are applied.
- A re-grant continues authority only before unauthorized_at plus
  status_retention_days, and a re-grant without retention is an error.

The TypeScript SDK's 46-case table is vendored and run as a parity test.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(reporting): adopt the reporting-subscription rules settled in adcp#8585

adcontextprotocol/adcp#8585 settles the ambiguities both SDKs hit.

- Offerings publish standard_dimensions and unmapped_dimensions.
  SubscriptionOffering.from_offering reads them from the wire, and sellers use
  with_subscription_dimensions() to publish them.
  subscription_capabilities() refuses an offering without them.
- Coverage consumes the readback's inactive_intervals.
- timezone_cutovers[] carry previous_timezone and an optional offering_id, and
  gaps[] carry an optional offering_id. An entry with an offering_id applies to
  that offering only, and previous_timezone supplies the zone before the first
  cutover.
- period_timezone follows the normative text: a utc offering matches only UTC
  or Etc/UTC, a source_timezone offering only its published zone (never an
  account-resolved one), and an account_timezone offering takes the override.

The beta.1 models close these objects, so _reporting_subscription_preview adds
the fields to the generated classes until a 3.3.0-beta.2 bundle carries them.
The vendored TypeScript parity table keeps a scope valid under maxProperties 1
in three cases, matching the fix pending upstream.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(reporting): take each offering's prior zone from its own timezone cutover

Follow adcp#8585: the account's timezone is its current zone, as list_accounts
shows it. Before a timezone change, an offering's account-timezone periods use
that change's previous_timezone, considering only the changes that apply to
the offering. An offering with no applicable change uses the current zone
throughout. A timezone change without previous_timezone is now refused, and so
is a wire offering without standard_dimensions.

Previously the first cutover's previous_timezone became the base zone for
every offering, so a cutover scoped to one offering could shift another
offering's boundaries.

Re-vendor the TypeScript parity table at f8b2fddd (54 cases, with the scope
fix now upstream).

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(reporting): serve adcp#8585 subscription fields only to 3.3.0-beta.2 callers

The server validates responses against the release it serves, and the pinned
3.3.0-beta.1 schemas close the objects adcp#8585 extends. The new wire module
gates the new fields on 3.3.0-beta.2 or later, as the TypeScript SDK does:

- standard_dimensions and unmapped_dimensions on offerings;
- inactive_intervals on the readback;
- previous_timezone and offering_id on timezone_cutovers[] and gaps[].

For older callers, per-offering gaps collapse into one account-wide gap
spanning them, and per-offering cutovers into one entry at the latest
effective_at.

The parity test now also validates every subscription in the TypeScript table
against core/reporting-subscription.json.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(reporting): address the coverage-rule review and adopt adcp#8588 periods

Fixes from the expert review of the coverage rule:

- An empty flight (stopped at or before it started) overlaps no period, so a
  buy canceled before its flight owes nothing.
- Each regime carries its per-offering handoff. E for a continuing or new
  offering is the latest of the outgoing calendar's boundary and the outgoing
  regime's own handoff, so two cutovers inside one period never owe a period
  twice. A timezone change passed at T or at its E now gives the same result.
- The extensible reporting-dimension vocabulary parses on selectors, offerings
  and the capability block; an unknown selector value matches nothing.
- Zone names must be IANA zones (posixrules and right/... are refused), and
  offerings with a year or month delivery_sla are refused at construction.
- The beta.1 projection keeps distinct timezone changes distinct and drops
  prior_versions[].configuration. The feature id is refused without the
  principal section. Stale docstrings are corrected.

adcp#8588 (pending for 3.3.0-beta.2):

- Sub-daily periods under a local alignment restart at each local midnight on
  wall-clock multiples, so a DST day has 23 or 25 hourly periods. The duration
  must divide 24 hours and cannot mix date and time components.
- prior_versions[].configuration is consumed, and
  require_prior_configurations makes a missing one an error. A version
  accepted while inactive has an empty window.

The parity runner now checks row order, expected_obligations and the error
class.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(reporting): accept undated continuation re-grants and re-vendor the parity table

Re-vendor the TypeScript coverage table at a99597a4: 79 cases, adding
- empty flights;
- two cutovers inside one period;
- sub-daily local periods;
- DST gaps and folds at midnight;
- a skipped civil date;
- PT24H versus P1D;
- month-end clamps;
- three-grant chains;
- simultaneous changes;
- unknown dimensions.

All pass under the strict runner.

Match TS on one rule the table pins: a grant whose authorized_at is at or
before the previous loss continues the chain at an unknown instant. This
happens because adcp#8585 keeps authorized_at at the chain start after a
re-grant inside retention. The buyer removes no gap of its own; the seller's
gaps[] decide. Grants are read in input order.

The schema check accepts dimension values outside the beta.1 enum, because
the vocabulary is x-extensible.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(reporting): owe the period containing confirmed_at in full, per adcp#8593

Owner decision for 3.3.0-beta.2:

- The start bound is the first boundary at or after the later of the
  version's accepted_at and the account's authorized_at.
- A buy is then owed every period that ends after its confirmed_at, so the
  confirmation period is owed in full.
- An empty flight still owes nothing.

The rule lives in the one function that holds it, _first_owed_start.

Re-vendor the TypeScript parity table at f8b48c0c. It has 81 cases,
conformed to the same decision, and adds an ASAP buy confirmed mid-day and a
monthly billing buy confirmed mid-month. All pass under the strict runner.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bokelley and others added 2 commits October 30, 2026 04:05
…bscription-coverage

# Conflicts:
#	scripts/check-package-size.mjs
…dules after merging main

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 30, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — subsequent pass, clean.

Prior decision was approve (clean, no findings). This pass surfaces no critical/high/medium findings. The large diff is explained by main advancing between the prior approve and now: the envelope-status, pricing-currency, product-page-collection, and Zod-generator changes already exist at base SHA 8eddda7 (ENVELOPE_FIELD_SCHEMAS present at schema-loader.ts:693 in base), so they are already-merged, already-reviewed content rather than this PR's authored work. The PR's own content (reporting-subscription coverage feature) was reviewed on the prior pass.

Genuinely new since prior: version bump beta.5→beta.7, changelog/doc stamps, and a documented check-package-size.mjs budget raise (26.36M→26.41M) — mechanical and sound. A .changeset/reporting-subscription-coverage.md is present, satisfying changeset-vs-wire-impact. Version bumps are via Changesets tooling, not hand-edited.

No no-auto-approve team match; gated_paths is false; high_risk is false. Decision table rows 1–8 do not fire → row 9 approve.

bokelley and others added 2 commits October 30, 2026 16:51
…e budget

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean pass, no blocking or medium findings.

Checked:

  • MCP shared/cached schema validator + ≥64 KiB schema-factoring: fully fail-safe (returns original schema on every unsupported-graph condition, operates on an acyclic cloned tree) and validation-only — no witness/wire-shape violation.
  • Forked/watchdogged performance regression harness.
  • Mechanical 3.1.29→3.1.31 maintained-schema bump: consistent Changesets output, not a hand-edit.
  • Two guarded storyboard refinements.
  • Changesets present and correctly patch-typed.

Decision table: Rows 1–8 do not fire. No critical/high findings (row 1), gated_paths is false (row 2), high_risk is false (rows 3/5), no medium data-loss/schema/infra findings (row 4), prior decision was approve so no sticky escalation (row 6), no no-auto-approve team match (row 7), zero medium findings (row 8). Falls through to row 9 → approve. Note: review_decision is REVIEW_REQUIRED but gated_paths is false, so row 2 does not apply.

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 30, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean pass, no blocking findings.

Checked: delta is compliance-pin/version alignment (3.1.29→3.1.31, beta.7→beta.9) plus additive, fail-closed work — a content-keyed LRU shared MCP schema-validator (dual byte+entry budget, per-entry Ajv), MCP schema factoring in compact-schema.ts that returns the original document on every unsupported graph (duplicate IDs, dynamic scopes, unvisited ref targets, URI-resolver throws), an isolated performance-regression harness with out-of-loop watchdog, and compliance-runner behavior fixes. Witness-not-translator holds — factoring alters only reference codegen; constraints preserved. Changesets present and correctly scoped.

Decision trace: no critical/high findings (row 1 n/a); gated_paths false (row 2 n/a); high_risk false (rows 3, 5 n/a); no medium findings (rows 4, 8 n/a); prior decision was approve (row 6 n/a); no no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve. Note review_decision is REVIEW_REQUIRED but gated_paths is false, so the hard gate does not apply.

bokelley and others added 2 commits October 30, 2026 18:56
…ndle

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 30, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean pass, no blocking or Medium findings.

The reviewer confirmed the delta is a mechanical beta.1→beta.2 bump across generated types/docs/version constants plus three well-tested source changes (legacy ref constraint reconciliation, typed RequestValidationError marker, and the ESM envelope-status loader fix). Changesets are present and correctly typed; pathway-back/negotiation tests updated; witness-not-translator and doc-link integrity hold.

Decision-table walk: no critical/high findings (row 1 n/a); gated_paths is false (row 2 n/a); high_risk is false, no deletions/modifications triggers (rows 3–5 n/a); prior decision was approve, not escalate (row 6 n/a); no no-auto-approve team match (row 7 n/a); zero medium findings (row 8 n/a). Falls through to row 9 → approve.

Note: review_decision is REVIEW_REQUIRED, but row 2 only applies when gated_paths is true — which it is not here — so no escalation is triggered.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@aao-secretariat

Copy link
Copy Markdown

Ladon cannot review this PR until merge conflicts are resolved.

…bscription-coverage

Brings in #3548. The packed-byte cap keeps the higher of main's and this branch's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 30, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — subsequent pass, clean.

This is a follow-up pass (prior decision: approve, no findings). The delta is 66 lines across 4 CI/test-only files: an empty-frontmatter CI changeset, a packed-tarball budget bump (26.41M→26.42M), a test swap from the retired '3.3.0-beta.1' pin to live ADCP_VERSION (behavior matches the exact-preview negotiation comment), and a one-line reformat. No src/lib/** wire changes, so no changeset obligation triggers.

Reviewer surfaced no findings. Decision table: no critical/high (row 1 n/a), gated_paths false (row 2 n/a), high_risk false (rows 3/5 n/a), no medium findings (rows 4/8 n/a), prior decision was approve so sticky escalation row 6 n/a, no no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.

Note: review_decision is REVIEW_REQUIRED, but gated_paths is false, so the row 2 hard gate does not apply.

A sub-daily billing_cycle was routed to the local sub-daily calendar,
which aligns to local midnight and ignores period_anchor, so every
boundary was off by the anchor's time of day. adcontextprotocol/adcp#8622
makes a sub-daily billing_cycle invalid; refuse it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — delta pass, no new findings.

This is a subsequent push on top of a prior clean approve (head 62473e9 → 3f5bf10). The reviewer examined a 41-line delta across calendar.ts and its test: a new guard that refuses sub-daily billing_cycle alignment (adcp#8622). It is correctly placed after the mixed date/time refusal and before the sub-daily local-calendar return, so a PT6H billing_cycle now fails closed instead of silently misaligning to local midnight and ignoring the anchor's time-of-day — consistent with the witness-not-translator posture (refuse rather than silently re-shape). Unreachable cases are excluded by the duration regex, and a matching test was added. The changeset is present and correctly typed (minor).

No critical/high/medium findings in this run. high_risk is false; gated_paths is false; no no-auto-approve team match. Although review_decision is REVIEW_REQUIRED, row 2 only applies when gated_paths is true — it is not here — so no hard gate fires. None of rows 1–8 match, so the outcome falls through to row 9: approve.

@bokelley
bokelley merged commit fc2a507 into main Oct 31, 2026
45 checks passed
@bokelley
bokelley deleted the bokelley/reporting-subscription-coverage branch October 31, 2026 14:17
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