Skip to content

feat(media-buy): static OOH channel contract — ooh_metrics, posting records, channel guide - #6241

Open
bokelley wants to merge 2 commits into
mainfrom
ooh-channel-draft
Open

feat(media-buy): static OOH channel contract — ooh_metrics, posting records, channel guide#6241
bokelley wants to merge 2 commits into
mainfrom
ooh-channel-draft

Conversation

@bokelley

@bokelley bokelley commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Draft for Media Buy WG review — the acceptance shape #6140 states for its option (c), which review on that issue converged on. Design narrative and standards grounding in #6146 (specs/static-ooh.md); mergeable prerequisites (reporting cadence, print nav) in #6240.

Static OOH (printed bulletins, posters, transit, street furniture) has no play event: nothing fires when a vinyl face is displayed. The channel's delivery number is a period-level modeled audience estimate from a measurement currency (Geopath US / Route UK / MOVE AU / COMMB CA), and its settlement artifact is the posting record — proof the posting period ran, not an airing log.

ooh_metrics (delivery-metrics + available-metric)

Sibling to dooh_metrics, carrying what a seller natively knows plus provenance-declared estimates:

  • panels[] — typed unit identifiers: id_type ∈ geopath | route_frame | plant_face | other (the enum proposed by @EvgenyAndroid on Static OOH: measurement shape + proof-of-posting (the no-play-event channel) #6140; plant_face is the operator's own panel number that OAAA model contracts key line items on, alongside the Geopath/TAB panel number)
  • posting_period_start/end — the in-charge period the row covers
  • share_of_voice_contracted, illuminated_hours — rotary and illumination contract facts (illumination determines the 12/18/24-hour measured basis and the credit remedy)
  • estimated_impressions + estimation_basis ∈ geopath_dec | route_frame_data | seller_modeled — the delivery number with declared provenance; seller_modeled is the honest fallback for markets without a currency
  • postings[] — per-unit posting records: posted_at, verified_at, evidence_url, event_type ∈ posted | rotated | repaired | removed. Codifies the OAAA model-contract proof-of-performance conventions (photo per unit within five days of posting for bulletins and after each rotary rotation; representative photo per creative for poster showings; display terms run from average posting date).

Billing rides the existing broadcast pattern unchanged: vendor declared in measurement_terms.billing_measurement, maturation via is_final/measurement_window.

docs/creative/channels/ooh.mdx

Canonical-style channel guide: custom format kind like print (production PDF/TIFF at effective print resolution — 18–25 ppi roadside per OAAA production specs), physical constraints on the product declaration, posting-period flighting, and ooh_metrics-based reconciliation. Nav entry deferred until merge timing is known (snapshot redirects require the page to exist in the pinned docs snapshot).

Open questions for WG (also in #6146)

Refs #6140, #6138, #6146. Related: #5684 (tearsheet/proof-of-insertion is the same evidence-artifact class), #5537 (audience multiplier placement), #5538 (structured geo).

🤖 Generated with Claude Code

…ecords, channel guide

Static OOH has no play event: the channel's delivery number is a
period-level modeled audience estimate and its settlement artifact is
the posting record, not an airing log. Adds the option-(c) shape
converged on in #6140:

- ooh_metrics delivery block: panels[] with typed identifiers
  (geopath | route_frame | plant_face | other), posting period,
  contracted share of voice, illuminated hours, estimated_impressions
  with declared estimation_basis (geopath_dec | route_frame_data |
  seller_modeled), and postings[] — per-unit posting dates and photo
  evidence per OAAA proof-of-performance conventions
- ooh_metrics in available-metric; delivery doc tables updated
- docs/creative/channels/ooh.mdx: canonical custom-format contract for
  printed units (physical live-copy dimensions at effective print
  resolution), posting-period flighting, billing_measurement-based
  reconciliation

Design narrative in specs/static-ooh.md (#6146). Closes the schema
half of #6140; grounded in OAAA model-contract conventions.

Refs #6140, #6138

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread static/schemas/source/core/delivery-metrics.json
@aao-secretariat aao-secretariat Bot added the ladon/needs-human-review Ladon has escalated this PR for human review. label Aug 6, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Escalate to human review

Escalate — gated paths touched without required approval.

This PR modifies static/schemas/source/** (delivery-metrics.json and available-metric.json), which are under a hard, deterministic approval gate (gated_paths: true). The current review_decision is REVIEW_REQUIRED, not APPROVED, so the gate is not satisfied. Per decision rule row 2, the outcome must be escalate — human/CODEOWNERS review is required before merge.

The change itself is additive and clean: a new ooh_metrics delivery block wired alongside dooh_metrics (additionalProperties:true per DR-0009), a matching available-metric enum value, and a coherent channel guide doc. Schema↔docs coherence checks out, no oneOf regression, correct minor changeset, fictional companies (Acme Outdoor) used. This appears to be a non-breaking Normative-class change.

Medium findings

  • static/schemas/source/core/delivery-metrics.json:336 — estimated_impressions provenance MUST stated in description prose but not enforced by required[].

Escalation reasons

  • Gated files static/schemas/source/core/delivery-metrics.json and static/schemas/source/enums/available-metric.json require human/CODEOWNERS approval; review_decision is REVIEW_REQUIRED.

Medium findings

  • static/schemas/source/core/delivery-metrics.json:336 — estimation_basis provenance MUST not enforced by required[]

Why human review

  • Gated paths under static/schemas/source/** modified (delivery-metrics.json, available-metric.json) and review_decision is REVIEW_REQUIRED, not APPROVED — human/CODEOWNERS review required.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/delivery-metrics.json (modified) matches static/schemas/source/**; static/schemas/source/enums/available-metric.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

…resent

The ooh_metrics description asserts provenance MUST be declared, but
nothing enforced it — a seller could emit the settlement number with
no basis and stay schema-valid. Adds the draft-07 property dependency
(same pattern as the existing reach → reach_unit rule) so the MUST is
schema-enforced rather than description prose.

Addresses PR #6241 review (DR-0001).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Escalate to human review

Escalate — gated schema paths require human/CODEOWNERS approval.

This PR adds a static OOH channel contract: ooh_metrics gains a draft-07 dependencies block making estimation_basis required whenever estimated_impressions is present. The reviewer confirms this resolves the prior Medium (provenance MUST is now validator-enforced, not prose-only), that draft-07 is the correct draft for the property-dependency form, and that the pattern matches 11 other source schemas. Schema↔docs coherence looks consistent and there are no current findings.

Why this cannot auto-approve: the diff modifies files under static/schemas/source/** (core/delivery-metrics.json, enums/available-metric.json), which are under a hard, deterministic approval gate. review_decision is not APPROVED (unknown/none), so the gate is not satisfied (decision row 2). This is a hard gate enforced in code regardless of how clean the diff is.

The prior escalation was resolved on the merits (the prior Medium is gone), but the gate persists purely because a human/CODEOWNERS approval has not yet been recorded.

Escalation reasons

  • Gated paths under static/schemas/source/** modified (core/delivery-metrics.json, enums/available-metric.json) and review_decision is not APPROVED — human/CODEOWNERS review required.

Why human review

  • Gated paths under static/schemas/source/** modified (core/delivery-metrics.json, enums/available-metric.json) and review_decision is not APPROVED — human/CODEOWNERS review required.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/delivery-metrics.json (modified) matches static/schemas/source/**; static/schemas/source/enums/available-metric.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'unknown', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@bokelley bokelley added the needs-wg-review Blocked on a working-group decision — surface in WG meeting agendas label Aug 7, 2026
@bokelley

bokelley commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

HOLD — approve-direction only; ballot after amendments

Per the WG packet on #6140 (and charter ballot requirements — normative change, five-person/two-org two-thirds): this PR stays in draft and must not merge on green CI. Amendments to land before ballot:

Holding amendments until AdQuick's real posting artifacts are in hand, so postings[] is validated against actual proof-of-performance packages rather than the OAAA templates alone.

@bokelley
bokelley marked this pull request as draft August 7, 2026 06:57

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Escalate to human review

Escalate — gated schema paths modified and no APPROVED review on record.

This PR adds a static-OOH delivery contract: an ooh_metrics block in delivery-metrics.json, the ooh_metrics enum value in available-metric.json, matching doc rows, a new channels/ooh.mdx guide, and a correct minor changeset. The change is purely additive (new optional fields + enum value), Normative (non-breaking), schema↔docs coherent, with a correct changeset scope/type. No critical/high findings.

However, static/schemas/source/core/delivery-metrics.json and static/schemas/source/enums/available-metric.json fall under the static/schemas/source/** hard approval gate (gated_paths: true), and review_decision is not APPROVED (unknown/none). Per decision-table row 2, this is a non-overridable gate that requires a recorded human/CODEOWNERS approval before auto-approval is possible. The gate lifts the moment review_decision becomes APPROVED.

Medium findings

  • None.

Escalation reasons

  • Gated paths under static/schemas/source/** modified (core/delivery-metrics.json, enums/available-metric.json) and review_decision is not APPROVED — human/CODEOWNERS review required.

One non-blocking coherence follow-up (low): estimation_basis enum at delivery-metrics.json:164 is narrower than the currencies named in ooh.mdx — worth a look but not a blocker.

Why human review

  • Gated paths under static/schemas/source/** modified (core/delivery-metrics.json, enums/available-metric.json) and review_decision is not APPROVED — human/CODEOWNERS review required.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/delivery-metrics.json (modified) matches static/schemas/source/**; static/schemas/source/enums/available-metric.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'unknown', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@EvgenyAndroid

Copy link
Copy Markdown
Contributor

Pressure-testing postings[] per the ask on #6138. Four things, one of which reverses a call of mine.

Amendment 1: concede, and for a stronger reason than the Geopath/Ipsos churn. ooh.mdx names Geopath, Route, MOVE and COMMB. The enum I specified covers the first two. So an AU MOVE 2.0 or CA COMMB number is currency-measured, has no honest value available, and has to declare seller_modeled — inverting the exact provenance claim the field exists to make. That's a defect in my call, not a matter of taste. currency_measured | seller_modeled with provider identity in the measurement_source lane fixes it. What I'd hold is the principle underneath: whichever lane ends up carrying provider identity should stay a closed vocabulary with additions shipping as minors, so "which currency" never degrades into free text.

Two corrections before the packets are read Monday.

The #6140 comment says "the posting_period object already carries start/contracted_start separation." That isn't true of the shipped schema — ooh_metrics has flat posting_period_start and posting_period_end, no object and no contracted_start. Contracted-versus-actual start isn't expressible today, which matters because it's the premise for the flight-end point below.

The #6138 status map says to decide the layering "when #6239/#6241 are reviewed together." #6239 is a closed certification PR; the intended reference is #6139.

The evidence SLA landed in the wrong file. The ask on #6140 was a note in ooh.mdx. It went into the postings schema description instead, and the guide now says only "per-unit posting dates and photo evidence, per OAAA proof-of-performance conventions" — the five-day rule, the per-rotation rule, and the one-photo-per-creative-variation rule for showings are all absent from the doc a seller actually reads. Worth settling the units while it's open: the two #6140 comments say "5 days" and "5 business days", and the schema took "five days". OAAA Bulletin §3.5 is the tiebreak. Note ooh.mdx separately carries two unrelated five-business-day facts (materials due before posting, posting completed after scheduled date) that will get confused with this one.

Flight end has no normative force. posted_at is required, and the description says display terms "customarily" run from the average posting date, but nothing computes that date, nothing carries it, and no MUST or SHOULD binds it. A buyer reconciling a bulletin flight has to average postings[].posted_at client-side and hope the seller did the same arithmetic. It isn't on the amendment list and I think it should be, either as a stated SHOULD on the computation or a field carrying the resulting flight end.

On amendment 2 I don't have a settled view. Per-panel identifiers[] is right if a panel genuinely carries operator and measurement IDs at once, which matches how OAAA line items key. The question I'd want answered before ballot is whether id_type survives as the per-identifier scheme tag or dissolves into free-text keys.

Not approving this review — the HOLD says it must not merge on green CI, and an approve would satisfy the gate.

@bokelley

bokelley commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

On Amendment 1: The structural argument is correct and the stronger rationale. ooh.mdx names four currencies; the current estimation_basis enum covers two. A MOVE 2.0 or COMMB number is currency-measured but has no honest value in the enum — it must declare seller_modeled, inverting the provenance claim. currency_measured | seller_modeled with provider identity in a closed measurement_source lane resolves it. The closed-vocabulary principle (additions ship as minors) should hold regardless of which lane carries provider identity.

Factual corrections confirmed:

Evidence SLA placement: Agreed — the three rules (bulletin five-day photo, per-rotation photo, one-photo-per-creative-variation for showings) belong in ooh.mdx, not only in the schema description. Two open questions before landing the fix: (a) OAAA Bulletin §3.5 settles "5 days" vs "5 business days" — the schema and doc need to agree, and ooh.mdx already carries two unrelated five-business-day facts that will create confusion without a clear label; (b) HOLD item 5 (extract evidence artifact as shared core schema with #5684) may move the schema shape. Recommend holding the doc fix until §3.5 is checked and the extraction question is resolved, then landing both together.

Flight end normative force: Real gap — "customarily" has no enforcement weight. Two options for the amendment list:

Option A — normative language on computation (no new field):

Sellers SHOULD use the average of postings[].posted_at across units as the effective start of the display period for billing reconciliation.

Option B — explicit carrier field in ooh_metrics:

{ "average_posted_date": "2026-03-05" }

Option A is lower surface; Option B makes buyer reconciliation deterministic without client-side arithmetic and eliminates the "hope the seller did the same math" risk. Either is non-breaking. WG call — flagging for the amendment list.

Amendment 2 (identifiers[] / id_type): Moving to identifiers[] per panel means each entry needs a scheme tag. id_type as a closed enum on each identifier (not a free-text key) is the natural continuation and preserves the interop principle from Amendment 1. A wire example in the WG packet before ballot would settle the mechanics.


Generated by Claude Code

@EvgenyAndroid

Copy link
Copy Markdown
Contributor

Took the three open items. §3.5 is settled from the source documents, and it turned up a condition nobody has priced in.

OAAA §3.5 — five calendar days, and the two clauses are unrelated

From the Bulletin Net Contract, §3.5 Proof of Performance, verbatim:

A photograph will be provided for every bulletin in a campaign, at the campaign start provided within five (5) days after posting. A photograph is provided within five (5) days after a scheduled rotation of a bulletin.

Plain days, not business days. The schema's "five days" is right and the "5 business days" reading was wrong. The five-business-day figure comes from §2.2, which is a different obligation entirely:

If Acceptable Materials are timely delivered, Company will complete posting no later than five (5) business days after the scheduled posting date.

So the two five-day facts already in ooh.mdx aren't just confusable with the evidence SLA, they're a genuinely separate clause about posting completion. Labelling them by clause number when the doc fix lands would keep them apart.

The Poster Net Contract §3.5 reads:

Company will provide one representative close-up photograph for each creative design variation that is part of a poster campaign.

No deadline attached, which means the schema description is already correct in not asserting one for showings. Only refinement is "close-up" — the contract specifies it and the schema doesn't.

Flight end — Option B, and §2.2 changes what it has to carry

Option B, for the reason this thread has landed on repeatedly: flight end decides when money stops, and under Option A the buyer and seller each compute it from the same array, so any divergence in rounding, timezone, or which postings count surfaces as a billing dispute with no artifact to point at. Publishing the date the seller actually used makes it the declared value and postings[] the evidence, which is the same declare-plus-evidence shape as the rest of this contract, and it stays auditable because a third party can recompute it.

But the source clause is conditional, and that isn't in either option as written. §2.2 of the bulletin contract:

Advertiser will have the benefit of the full term of display from the average date of posting, unless the Materials are not timely delivered.

The poster contract §3.2(a) carries the identical carve-out. So the average-posting-date rule is not unconditional even in the contract we're encoding: it lapses when the buyer missed its own materials deadline. A bare average_posted_date says when the display term started but not whether the rule that sets it actually applied, which is exactly the case a reconciliation dispute turns on.

Two ways to handle that, and I'd take the first:

  1. average_posted_date plus a boolean for whether the materials-timeliness condition held, so a buyer can see both the date and its basis.
  2. Ship average_posted_date alone for v1 and note the carve-out in ooh.mdx as a known unencoded condition.

Either way the field should be documented as derivable from postings[].posted_at so the declared number stays checkable against the evidence.

Amendment 2 — wire example

{
  "panels": [
    {
      "identifiers": [
        { "id_type": "plant_face", "id": "LAM-4471-N" },
        { "id_type": "geopath", "id": "30412345" }
      ],
      "estimated_impressions": 184200,
      "estimation_basis": "currency_measured"
    },
    {
      "identifiers": [
        { "id_type": "plant_face", "id": "OUT-8891-S" }
      ],
      "estimated_impressions": 96400,
      "estimation_basis": "seller_modeled"
    }
  ]
}

The first panel is the case Amendment 2 exists for: an operator face ID and a Geopath ID on the same physical unit, neither derivable from the other, both needed — the operator's to reconcile against the line item, Geopath's to join to the currency. The second shows the shape degrading cleanly for an unmeasured face, one identifier and a modeled basis, with no null-padding.

id_type stays a closed enum per identifier under this shape, which is what keeps "which scheme is this" from drifting to free text. Worth stating in the packet that identifiers is unordered and that no entry is privileged, otherwise implementers will read position zero as canonical.

@bokelley

bokelley commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

§3.5 — settled; doc fix still waits for HOLD item 5.
Bulletin §3.5 is plain calendar days ("within five (5) days after posting"), confirming the schema description was correct. The five-business-day figures in ooh.mdx are both §2.2 (posting-completion obligation — a separate clause); labeling by clause when the doc fix lands will keep them distinct. Poster §3.5 specifies "close-up" which is absent from the schema description; no deadline, so the schema is otherwise correct for showings. Both fixes (clause labels for bulletin, "close-up" for poster) should land when HOLD item 5 (evidence-artifact extraction with #5684) resolves, so they target the final schema home rather than a shape that's about to move.

Flight end — Option B, but the materials-timeliness condition is a new element.
The §2.2 carve-out ("unless the Materials are not timely delivered" — poster §3.2(a) carries the identical language) was in neither original option: a bare average_posted_date declares when the display term started but not whether the conditional rule applied, which is exactly what a billing dispute turns on. Two encodings for the WG to decide between:

Sub-option B-1 — declared date + materials-timeliness flag:

{ "average_posted_date": "2026-03-05", "materials_timely": true }

Machine-readable; buyer sees both the date and whether the §2.2 rule applied.

Sub-option B-2 — declared date alone, carve-out in ooh.mdx:

{ "average_posted_date": "2026-03-05" }

Simpler wire surface; §2.2 condition documented in prose only.

Either way: document the field as derivable from postings[].posted_at so the declared value stays auditable against the evidence. Flagging average_posted_date (plus materials_timely for B-1) for the amendment checklist.

Amendment 2 — wire example settles the mechanics.
Shape confirmed: identifiers[] per panel, id_type closed enum per entry, multi-id panels represented naturally, unmeasured faces degrade cleanly to one identifier. One point worth calling out explicitly in the WG packet: identifiers is unordered and no entry is privileged — implementers must not read position zero as canonical.


Generated by Claude Code

@EvgenyAndroid

Copy link
Copy Markdown
Contributor

On B-1 vs B-2: B-1.

A prose-only carve-out on the rule that decides when billing stops is the shape this thread has already rejected twice — the number is true and unverifiable at the same time, and the party who benefits from ambiguity is the one who computed it. materials_timely is one boolean, it is knowable by the seller at posting time without any new measurement, and it turns "was the §2.2 rule in force" from something a buyer infers into something it reads.

The asymmetry is what decides it. Under B-2 a buyer seeing average_posted_date cannot tell whether the seller applied §2.2 or fell back to the contracted start because materials were late, and those produce different flight ends from the same evidence. That difference only surfaces at reconciliation, which is the expensive place to find it.

One drafting note if B-1 lands: materials_timely should be described as the seller's assertion about the buyer's delivery, not a derived value, so nobody expects it to be recomputable from postings[]. It is the one field in this block that isn't checkable against the evidence, and saying so keeps it honest.

@bokelley
bokelley marked this pull request as ready for review August 8, 2026 09:29

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Escalate to human review

Escalate — gated schema paths modified and review not yet APPROVED.

This PR is an additive static-OOH contract: a new ooh_metrics delivery block, an ooh_metrics available-metric enum value, an OOH channel guide, and delivery-doc updates, with a correct minor changeset. The reviewer found the schema↔docs coherent, no oneOf regression, additionalProperties: true preserved (DR-0009), and the estimated_impressions→estimation_basis provenance MUST correctly encoded. No Critical/High/Medium findings.

However, this PR modifies files under static/schemas/source/**core/delivery-metrics.json and enums/available-metric.json — which are under a hard, deterministic approval gate (gated_paths: true), and the current review_decision is REVIEW_REQUIRED, not APPROVED. Per decision rule row 2, the outcome must be escalate regardless of how clean the diff is. Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded.

Escalation reasons

  • Gated paths under static/schemas/source/** modified (core/delivery-metrics.json, enums/available-metric.json) and review_decision is REVIEW_REQUIRED, not APPROVED — human/CODEOWNERS review required.

Why human review

  • Gated paths under static/schemas/source/** modified (core/delivery-metrics.json, enums/available-metric.json) and review_decision is REVIEW_REQUIRED, not APPROVED — human/CODEOWNERS review required.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/delivery-metrics.json (modified) matches static/schemas/source/**; static/schemas/source/enums/available-metric.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ladon/needs-human-review Ladon has escalated this PR for human review. needs-wg-review Blocked on a working-group decision — surface in WG meeting agendas

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants