diff --git a/.changeset/metric-leaf-identities.md b/.changeset/metric-leaf-identities.md new file mode 100644 index 0000000000..2b62bdced1 --- /dev/null +++ b/.changeset/metric-leaf-identities.md @@ -0,0 +1,7 @@ +--- +"adcontextprotocol": minor +--- + +Add leaf metric identities so nested delivery values are individually declarable, committable, aggregatable, and sortable: `quartile_25`–`quartile_100` (resolving to `quartile_data.q1_views`–`q4_views`) and `viewable_rate`, `viewable_impressions`, `measurable_impressions`, `viewed_seconds` (resolving to the same-named `viewability` fields) join `available-metric` and `sort-metric`. This closes an existing contradiction: `committed-metric` qualifier rules and `delivery-metric-aggregate` conditionals already referenced these metric_ids, and the shipped `committed_metrics` / `metric_aggregates` examples were invalid against their own schemas. Also adds the missing flat transactional scalars (`commissionable_value`, `plays`, `cost_per_completed_view`, `cpm`, `downloads`, `units_sold`, `new_to_brand_units`) to `sort-metric`, with survey/model-based lift scalars documented as intentionally sort-excluded. Leaf identities resolve to the nested canonical values — no duplicate flat response fields are introduced. A metric-identity coherence contract test now enforces enum/schema/example agreement. + +**Seller conformance note:** the `viewability` description now requires (MUST) that sellers populate `standard` on reported viewability objects when `committed_metrics` carry a `viewability_standard` qualifier — upgraded from a SHOULD. The trigger is any `committed_metrics` entry with `qualifier.viewability_standard` set, including the container `metric_id: "viewability"` (already in the enum). Sellers with an existing `{metric_id: "viewability", qualifier: {viewability_standard: "mrc"}}` commitment who were sometimes omitting `standard` become non-conformant. Sellers who never use the `viewability_standard` qualifier are unaffected. diff --git a/docs/media-buy/media-buys/optimization-reporting.mdx b/docs/media-buy/media-buys/optimization-reporting.mdx index 3f01619b8a..85e0a7ec5e 100644 --- a/docs/media-buy/media-buys/optimization-reporting.mdx +++ b/docs/media-buy/media-buys/optimization-reporting.mdx @@ -322,6 +322,8 @@ Buyers receive the intersection of both. `impressions` and `spend` are always re - **`new_to_brand_units`**: Unit-volume parallel to `new_to_brand_rate` — count of units sold to first-time brand buyers - **`plays`**: Raw play count for DOOH/broadcast inventory (mirrors `forecastable-metric.plays`); distinct from `dooh_metrics.loop_plays` (per-screen rotation) and `impressions` (multiplied audience figure) +The numeric values inside `viewability` and `quartile_data` are individually addressable through leaf metric identities — `viewable_rate`, `viewable_impressions`, `measurable_impressions`, `viewed_seconds`, and `quartile_25`/`quartile_50`/`quartile_75`/`quartile_100` (which resolve to `quartile_data.q1_views`–`q4_views`). Leaf identities appear in `available_metrics` declarations, `committed_metrics` contracts, `metric_aggregates` rows, and breakdown `sort_by`; the nested object remains the only carrier of the values in delivery reports. In every metric set operation (declaration, filtering, intersection, selection) a container token subsumes its leaf identities — declaring `viewability` satisfies `viewable_rate`. Note that `quartile_100` counts 100%-of-duration completions (VAST `complete`), which is distinct from `completed_views` when the buy defines a shorter billable view threshold. + Buyers can optionally request a subset via `requested_metrics` to reduce payload size and focus on relevant KPIs. For `completion_rate` and `quartile_data`, sellers MAY return `null` to signal the metric does not apply (e.g. on a non-video buy), and clients MUST accept `null` as a valid value for these two fields. Every other metric signals "not applicable" by omission — sellers omit it rather than sending `null`. diff --git a/docs/media-buy/task-reference/get_media_buy_delivery.mdx b/docs/media-buy/task-reference/get_media_buy_delivery.mdx index 99b84c0a0d..11ee035952 100644 --- a/docs/media-buy/task-reference/get_media_buy_delivery.mdx +++ b/docs/media-buy/task-reference/get_media_buy_delivery.mdx @@ -895,7 +895,7 @@ When you include `reporting_dimensions` in the request, the response includes di } ``` -Geo, device, audience, demographic, and placement dimensions accept optional `limit` and `sort_by` (any value from the `sort-metric` enum; defaults to `spend`). Geo requires `geo_level` (`country`, `region`, `metro`, `postal_area`). Demographic accepts optional canonical `age_ranges`. Each requested range must be supported by the product's `supports_demographic_breakdown.age`: `exact_predicates` accepts ranges within its declared bounds, while `enumerated_intervals` accepts only exact declared intervals. Unsupported ranges return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature); the seller never substitutes a wider or narrower bucket. Omitting `age_ranges` requests the product's native declared breakdown. The spot log accepts only `limit`; its canonical order is `aired_at` ascending. Breakdowns are per-dimension only — cross-dimensional intersections are not supported. +Geo, device, audience, demographic, and placement dimensions accept optional `limit` and `sort_by` (any value from the `sort-metric` enum; defaults to `spend`). The sort vocabulary includes leaf metric identities that order rows by a value nested inside an object-shaped metric — `sort_by: "quartile_100"` orders by `quartile_data.q4_views`, `sort_by: "viewable_rate"` orders by `viewability.viewable_rate` — so queries like "top placements by in-view rate" need no flat duplicate fields. Survey/model-based lift scalars are excluded from sorting; see the `sort-metric` enum description. Geo requires `geo_level` (`country`, `region`, `metro`, `postal_area`). Demographic accepts optional canonical `age_ranges`. Each requested range must be supported by the product's `supports_demographic_breakdown.age`: `exact_predicates` accepts ranges within its declared bounds, while `enumerated_intervals` accepts only exact declared intervals. Unsupported ranges return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature); the seller never substitutes a wider or narrower bucket. Omitting `age_ranges` requests the product's native declared breakdown. The spot log accepts only `limit`; its canonical order is `aired_at` ascending. Breakdowns are per-dimension only — cross-dimensional intersections are not supported. ### Available dimensions diff --git a/package.json b/package.json index a23b8f61cd..aa775359a9 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "deploy:cdn-artifacts-cutover:dry-run": "wrangler deploy --config workers/artifact-cdn/wrangler.cutover.toml --dry-run", "verify:cdn-artifacts-cutover": "node scripts/verify-cdn-artifacts-cutover.mjs", "typecheck": "tsc --project server/tsconfig.json --noEmit", - "test:schemas": "node tests/schema-validation.test.cjs && node --test tests/outcome-target.test.cjs tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/compact-product-lifecycle-storyboards.test.cjs tests/timezone-resolution-storyboards.test.cjs tests/schema-deprecation-metadata.test.cjs tests/products-only-brief-compatibility.test.cjs tests/async-identity-convergence.test.cjs tests/creative-rotation.test.cjs tests/lint-schema-enum-drift.test.cjs tests/synthetic-depiction.test.cjs tests/creative-rendering-authority.test.cjs && npm run test:premium-display-formats && npm run test:geo-region-targeting", + "test:schemas": "node tests/schema-validation.test.cjs && node --test tests/outcome-target.test.cjs tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/compact-product-lifecycle-storyboards.test.cjs tests/timezone-resolution-storyboards.test.cjs tests/schema-deprecation-metadata.test.cjs tests/products-only-brief-compatibility.test.cjs tests/async-identity-convergence.test.cjs tests/creative-rotation.test.cjs tests/metric-identity-coherence.test.cjs tests/lint-schema-enum-drift.test.cjs tests/synthetic-depiction.test.cjs tests/creative-rendering-authority.test.cjs && npm run test:premium-display-formats && npm run test:geo-region-targeting", "test:performance-feedback": "node --test --test-force-exit --test-timeout=30000 tests/performance-feedback-contract.test.cjs", "test:dist-schema-version-ids": "node --test --test-force-exit --test-timeout=30000 tests/dist-schema-version-ids.test.cjs", "test:examples": "node tests/example-validation-simple.test.cjs && npm run test:tmp-context-merge", diff --git a/scripts/precommit-server-unit.cjs b/scripts/precommit-server-unit.cjs index 5add7be21a..86fdb23202 100644 --- a/scripts/precommit-server-unit.cjs +++ b/scripts/precommit-server-unit.cjs @@ -53,7 +53,11 @@ function pathsFromNameStatus(output) { } function stagedFiles() { - const output = execFileSync('git', ['diff', '--cached', '--name-status', '--diff-filter=ACMRD', '-z']); + // Merge commits with main can stage tens of thousands of dist/ files; + // Node's default 1 MB maxBuffer overflows (ENOBUFS) on the -z listing. + const output = execFileSync('git', ['diff', '--cached', '--name-status', '--diff-filter=ACMRD', '-z'], { + maxBuffer: 64 * 1024 * 1024, + }); return pathsFromNameStatus(output); } diff --git a/static/schemas/source/core/delivery-metrics.json b/static/schemas/source/core/delivery-metrics.json index a193f5095f..469ec16c69 100644 --- a/static/schemas/source/core/delivery-metrics.json +++ b/static/schemas/source/core/delivery-metrics.json @@ -189,7 +189,7 @@ }, "quartile_data": { "type": ["object", "null"], - "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).", + "description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy). Individual quartiles are addressable via the leaf metric identities `quartile_25` (q1_views), `quartile_50` (q2_views), `quartile_75` (q3_views), and `quartile_100` (q4_views) for declaration, commitments, aggregates, and breakdown sorting; this object remains the canonical carrier of the values. Quartiles are player-fired events (VAST firstQuartile/midpoint/thirdQuartile/complete). `quartile_100` counts 100%-of-duration completions and is distinct from `completed_views`, which counts completions at the seller's billable view threshold (`view_duration_seconds`) when one is set.", "properties": { "q1_views": { "type": "number", @@ -288,7 +288,7 @@ }, "viewability": { "type": "object", - "description": "Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable.", + "description": "Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable. The numeric leaves are addressable via the leaf metric identities `viewable_rate`, `viewable_impressions`, `measurable_impressions`, and `viewed_seconds` for declaration, commitments, aggregates, and breakdown sorting; this object remains the canonical carrier of the values. When a buy reports under more than one standard, contract a specific standard via the `viewability_standard` qualifier on `committed_metrics`; when the package's `committed_metrics` carry a `viewability_standard` qualifier, sellers MUST populate `standard` on reported viewability objects so reconciliation can match the qualifier.", "properties": { "vendor": { "$ref": "/schemas/core/brand-ref.json", diff --git a/static/schemas/source/core/format.json b/static/schemas/source/core/format.json index dc96b39121..0ad2665f8f 100644 --- a/static/schemas/source/core/format.json +++ b/static/schemas/source/core/format.json @@ -687,7 +687,7 @@ }, "reported_metrics": { "type": "array", - "description": "Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. If omitted, the format defers entirely to product-level metric declarations.", + "description": "Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. The intersection is evaluated under the container-subsumption rule in `enums/available-metric.json` — a container token (e.g. `viewability`) matches its leaf identities (e.g. `viewable_rate`), so mixed container/leaf declarations do not produce an empty intersection. If omitted, the format defers entirely to product-level metric declarations.", "items": { "$ref": "/schemas/enums/available-metric.json" }, diff --git a/static/schemas/source/core/product-filters.json b/static/schemas/source/core/product-filters.json index 645ec206b9..b9b8a9aa6b 100644 --- a/static/schemas/source/core/product-filters.json +++ b/static/schemas/source/core/product-filters.json @@ -500,7 +500,7 @@ }, "required_metrics": { "type": "array", - "description": "Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`.", + "description": "Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). Superset evaluation follows the container-subsumption rule in `enums/available-metric.json` — a product declaring the `viewability` container satisfies a requirement for the `viewable_rate` leaf. The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`.", "items": { "$ref": "/schemas/enums/available-metric.json" }, diff --git a/static/schemas/source/core/reporting-webhook.json b/static/schemas/source/core/reporting-webhook.json index c50244d3cd..dfa7e0998e 100644 --- a/static/schemas/source/core/reporting-webhook.json +++ b/static/schemas/source/core/reporting-webhook.json @@ -52,7 +52,7 @@ }, "requested_metrics": { "type": "array", - "description": "Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's available_metrics.", + "description": "Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's available_metrics. Subset evaluation and leaf resolution follow the container-subsumption rule in `enums/available-metric.json`: requesting a leaf identity selects its canonical carrier object in the payload.", "items": { "$ref": "/schemas/enums/available-metric.json" }, diff --git a/static/schemas/source/enums/available-metric.json b/static/schemas/source/enums/available-metric.json index f21dd004c4..24e8c1706e 100644 --- a/static/schemas/source/enums/available-metric.json +++ b/static/schemas/source/enums/available-metric.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/enums/available-metric.json", "title": "Available Metric", - "description": "Standard delivery and performance metrics available for reporting", + "description": "Standard delivery and performance metrics available for reporting. Most values name flat numeric fields of `core/delivery-metrics.json`. Leaf identities address a single numeric value nested inside an object-shaped metric so it can be declared, committed, aggregated, and sorted individually: `quartile_25`/`quartile_50`/`quartile_75`/`quartile_100` resolve to `quartile_data.q1_views`–`q4_views`, and `viewable_rate`/`viewable_impressions`/`measurable_impressions`/`viewed_seconds` resolve to the same-named fields of `viewability`. The nested object remains the canonical carrier of the reported value — leaf identities never introduce duplicate flat response fields. The object-shaped tokens (`viewability`, `quartile_data`, `dooh_metrics`) declare the whole nested surface at once. **Container subsumption**: in every set operation over this enum — capability declaration, `required_metrics` filtering, format `reported_metrics` intersection, and `requested_metrics` selection — a container token subsumes its leaf identities: declaring `viewability` satisfies a requirement or request for `viewable_rate`, and requesting a leaf identity selects its canonical carrier object in the response (never a flat duplicate). A leaf declaration does not imply sibling leaves or the carrier's non-numeric fields.", "type": "string", "enum": [ "impressions", @@ -28,7 +28,15 @@ "saves", "profile_visits", "viewability", + "viewable_rate", + "viewable_impressions", + "measurable_impressions", + "viewed_seconds", "quartile_data", + "quartile_25", + "quartile_50", + "quartile_75", + "quartile_100", "dooh_metrics", "cost_per_click", "cost_per_completed_view", diff --git a/static/schemas/source/enums/sort-metric.json b/static/schemas/source/enums/sort-metric.json index 6ee1283aba..7582d4c769 100644 --- a/static/schemas/source/enums/sort-metric.json +++ b/static/schemas/source/enums/sort-metric.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/enums/sort-metric.json", "title": "Sort Metric", - "description": "Numeric delivery metrics available for sorting breakdown rows. Subset of delivery-metrics fields that are flat numeric values (excludes nested objects like quartile_data, dooh_metrics, viewability, by_event_type, by_action_source).", + "description": "Numeric delivery metrics available for sorting breakdown rows. Contains the flat numeric fields of `core/delivery-metrics.json` plus leaf identities that resolve to a single numeric value nested inside an object-shaped metric: `quartile_25`–`quartile_100` order rows by `quartile_data.q1_views`–`q4_views`; `viewable_rate`, `viewable_impressions`, `measurable_impressions`, and `viewed_seconds` order rows by the same-named fields of `viewability`. Sorting by a leaf identity orders on the nested value — the nested object remains the only carrier of the value in responses; no duplicate flat fields exist. Excludes object/array containers (`quartile_data`, `dooh_metrics`, `viewability`, `by_event_type`, `by_action_source`, `vendor_metric_values`) and the survey/model-based lift scalars (`incremental_sales_lift`, `brand_lift`, `foot_traffic`, `conversion_lift`, `brand_search_lift`), which are package-grain estimates rather than row-grain orderings.", "type": "string", "enum": [ "impressions", @@ -26,6 +26,21 @@ "saves", "profile_visits", "engagement_rate", - "cost_per_click" + "cost_per_click", + "commissionable_value", + "plays", + "cost_per_completed_view", + "cpm", + "downloads", + "units_sold", + "new_to_brand_units", + "viewable_rate", + "viewable_impressions", + "measurable_impressions", + "viewed_seconds", + "quartile_25", + "quartile_50", + "quartile_75", + "quartile_100" ] } diff --git a/tests/metric-identity-coherence.test.cjs b/tests/metric-identity-coherence.test.cjs new file mode 100644 index 0000000000..611c25cd29 --- /dev/null +++ b/tests/metric-identity-coherence.test.cjs @@ -0,0 +1,168 @@ +const fs = require("fs"); +const path = require("path"); +const Ajv = require("ajv"); +const addFormats = require("ajv-formats"); +const { describe, it, before } = require("node:test"); +const assert = require("node:assert/strict"); + +const SCHEMA_ROOT = path.join(__dirname, "..", "static", "schemas", "source"); + +function readSchema(uri) { + assert.match(uri, /^\/schemas\//); + return JSON.parse( + fs.readFileSync(path.join(SCHEMA_ROOT, uri.slice("/schemas/".length)), "utf8") + ); +} + +async function compile(schema) { + const ajv = new Ajv({ + allErrors: true, + strict: false, + loadSchema: async (ref) => readSchema(ref), + }); + addFormats(ajv); + return ajv.compileAsync(schema); +} + +// Leaf metric identities address one numeric value nested inside an +// object-shaped delivery metric. The nested object stays the canonical +// carrier; the identity exists so commitments, aggregates, and sorts can +// reference the single value. +const LEAF_IDENTITIES = { + quartile_25: ["quartile_data", "q1_views"], + quartile_50: ["quartile_data", "q2_views"], + quartile_75: ["quartile_data", "q3_views"], + quartile_100: ["quartile_data", "q4_views"], + viewable_rate: ["viewability", "viewable_rate"], + viewable_impressions: ["viewability", "viewable_impressions"], + measurable_impressions: ["viewability", "measurable_impressions"], + viewed_seconds: ["viewability", "viewed_seconds"], +}; + +// Package-grain survey/model-based estimates: reportable and committable, +// but excluded from row sorting per sort-metric.json's description. +const SORT_EXCLUDED_LIFT_METRICS = [ + "incremental_sales_lift", + "brand_lift", + "foot_traffic", + "conversion_lift", + "brand_search_lift", +]; + +describe("metric identity coherence", () => { + let deliveryMetrics; + let availableMetrics; + let sortMetrics; + + before(() => { + deliveryMetrics = readSchema("/schemas/core/delivery-metrics.json"); + availableMetrics = new Set(readSchema("/schemas/enums/available-metric.json").enum); + sortMetrics = new Set(readSchema("/schemas/enums/sort-metric.json").enum); + }); + + function flatNumericDeliveryMetrics() { + return Object.entries(deliveryMetrics.properties) + .filter(([, schema]) => { + const types = Array.isArray(schema.type) ? schema.type : [schema.type]; + return types.includes("number") || types.includes("integer"); + }) + .map(([name]) => name); + } + + it("declares every flat numeric delivery metric in available-metric", () => { + for (const name of flatNumericDeliveryMetrics()) { + assert.ok(availableMetrics.has(name), `${name} missing from available-metric.json`); + } + }); + + it("makes every flat numeric delivery metric sortable except lift estimates", () => { + for (const name of flatNumericDeliveryMetrics()) { + if (SORT_EXCLUDED_LIFT_METRICS.includes(name)) { + assert.ok( + !sortMetrics.has(name), + `${name} is documented as sort-excluded but present in sort-metric.json` + ); + continue; + } + assert.ok(sortMetrics.has(name), `${name} missing from sort-metric.json`); + } + }); + + it("keeps sort-metric a subset of available-metric", () => { + for (const name of sortMetrics) { + assert.ok(availableMetrics.has(name), `${name} sortable but not declarable`); + } + }); + + it("resolves every leaf identity to an existing nested numeric value", () => { + for (const [leaf, [container, field]] of Object.entries(LEAF_IDENTITIES)) { + assert.ok(availableMetrics.has(leaf), `${leaf} missing from available-metric.json`); + assert.ok(sortMetrics.has(leaf), `${leaf} missing from sort-metric.json`); + const nested = deliveryMetrics.properties[container]; + assert.ok(nested, `${container} missing from delivery-metrics.json`); + const target = nested.properties[field]; + assert.ok(target, `${container}.${field} missing from delivery-metrics.json`); + const types = Array.isArray(target.type) ? target.type : [target.type]; + assert.ok( + types.includes("number") || types.includes("integer"), + `${container}.${field} is not numeric` + ); + // No duplicate flat field: the leaf identity must not also exist as a + // top-level delivery-metrics property (the nested value is canonical). + assert.equal( + deliveryMetrics.properties[leaf], + undefined, + `${leaf} must not exist as a flat delivery-metrics field` + ); + } + }); + + it("only conditions delivery-metric-aggregate on representable metric_ids", () => { + const aggregate = readSchema("/schemas/core/delivery-metric-aggregate.json"); + const standardBranch = aggregate.oneOf.find( + (branch) => + branch.properties && + branch.properties.scope && + branch.properties.scope.const === "standard" + ); + assert.ok(standardBranch, "no standard-scope branch in delivery-metric-aggregate"); + for (const conditional of standardBranch.allOf) { + const metricId = conditional.if.properties.metric_id.const; + assert.ok( + availableMetrics.has(metricId), + `delivery-metric-aggregate conditions on unrepresentable metric_id ${metricId}` + ); + } + }); + + it("validates the shipped committed_metrics example against its own schema", async () => { + const validate = await compile(readSchema("/schemas/core/committed-metric.json")); + const pkg = readSchema("/schemas/core/package.json"); + const example = pkg.properties.committed_metrics.examples[0]; + for (const entry of example) { + assert.equal( + validate(entry), + true, + `${JSON.stringify(entry)} -> ${JSON.stringify(validate.errors)}` + ); + } + }); + + it("validates the shipped metric_aggregates example against its own schema", async () => { + const validate = await compile( + readSchema("/schemas/core/delivery-metric-aggregate.json") + ); + const response = readSchema( + "/schemas/media-buy/get-media-buy-delivery-response.json" + ); + const example = + response.properties.aggregated_totals.properties.metric_aggregates.examples[0]; + for (const entry of example) { + assert.equal( + validate(entry), + true, + `${JSON.stringify(entry)} -> ${JSON.stringify(validate.errors)}` + ); + } + }); +});