Native ads: spec

Table of Contents

1. Purpose

Native ads select a grid of recommendation items from an inventory pool: validate, score, dedupe by id and by domain, rank, and take the top n with positions. The core is total (never throws) and deterministic; the browser layer owns viewability and output encoding. Core: wal-sh.adtech.native-ads.core (pure, cross-host).

ORACLE: src/wal_sh/adtech/native_ads/core.cljc (pure), browser.cljs (window._nativeAds).

2. Requirements

Behavioral guarantees:

  • Totality: select-grid and select-top-n never throw; for any input (incl. nil, malformed items, NaN/Inf) they return a vector (possibly empty).
  • Determinism: no rand, no clock, no I/O in core. Same input ⇒ byte-identical output on JVM and CLJS.
  • Idempotency: dedupe ops are idempotent; escape-html is idempotent on already-escaped output (modulo &).
  • Monotonicity in slots: (count (select-grid p {:slots n})) ≤ (count (select-grid p {:slots (inc n)})).

I/O domains (browser):

  • IntersectionObserver for viewability. Fallback to synchronous fire-all when unavailable.
  • window._beacon.send(event, payload): optional telemetry sink.
  • window._nativeAds: public surface (set on init).

3. Contract signature

Core (wal-sh.adtech.native-ads.core, pure, cross-host):

Fn Signature Pre Post
valid-item? item → bool none (total) true ⇒ has :id :headline :url + finite numeric scoring keys
score item → double (valid-item? item) (else returns 0 or NaN; caller filter) non-negative double; deterministic
dedupe-by-id [item] → [item] none result has unique :id; first occurrence wins; preserves order
dedupe-by-domain [item] → [item] none non-sponsored items have unique :domain; sponsored bypass
rank [item] → [item] none sorted by [(- score) (- revenue) id-asc]; invalids dropped
select-top-n n [item] → [item] none (count out) ≤ min(n, count pool); n ≤ 0 ⇒ []
select-grid [item] {:slots n :category s} → [item-with-:position] none (total; invalid items silently dropped, never throws) (count out) ≤ slots; positions are 1..n; deterministic
escape-html s → string none no raw < > & " ' in output
inventory Var → [item] (8 default items) n/a every item satisfies valid-item?

Browser surface (window._nativeAds):

window._nativeAds = {
  inventory: Array<Item>,
  engine: { recommend: (count?, category?) => Array<Item>,
            render:    (recs, containerId?) => void },
  show:    (count?, category?) => Array<Item>,   // full flow + viewability
  metrics: () => { viewableImpressions, clicks, verdict }
};

4. Invariants (property tests)

  • ∀ pool p, ∀ n: |select-top-n n p| ≤ min(n, |p|)
  • ∀ pool p: dedupe-by-domain is idempotent
  • ∀ pool p: dedupe-by-id is idempotent
  • ∀ pool p: non-sticky items in (dedupe-by-domain p) have pairwise-distinct :domain
  • ∀ valid item i: (score i) ≥ 0
  • ∀ pool p: (select-grid p opts) = (select-grid (shuffle p) opts) (permutation-stable)

5. Negative-state catalog (property tests)

Every row pairs a bad input with the expected rejection behavior and a property test that asserts it. All properties run on JVM and CLJS.

# Bad input Expected behavior Property / test
1 pool = nil or [] [] returned (no throw) nil-pool-rejected-as-empty (defspec, 30 cases)
2 n ≤ 0 (incl. negative, nil) [] from both select-top-n and select-grid non-positive-n-rejected (defspec, 30 cases)
3 item with :ctr-proxy = NaN valid-item? ⇒ false; item dropped from grid nan-item-rejected-by-valid? + nan-item-dropped-from-grid
4 item with :revenue = +Inf / -Inf same; dropped at validation nan-item-rejected-by-valid? (gen over NaN/±Inf/nil/string)
5 item missing required field valid-item? ⇒ false; dropped missing-required-field-rejected (defspec over 6 fields)
6 item is not a map (string, vec, nil) valid-item? ⇒ false; never throws non-map-item-rejected (defspec, 30 cases)
7 duplicate :id in pool (n copies) only one survives (first wins, deterministic) duplicate-ids-collapsed (defspec) + duplicate-ids-first-wins
8 all items share :domain grid collapses to 1 (the highest-scoring one) all-same-domain-collapses (deftest)
9 HTML in :headline (<script>...) passes through scoring untouched; escape-html neutralizes html-in-title-not-escaped-at-score-time + escape-html-removes-angle-brackets (defspec)
10 random garbage pool [{} {:id "x"} nil] select-grid returns [], never throws select-grid-never-throws (defspec over mixed bogus input)

Design decisions (called out in code):

  • Negative scores: deferred. ctr-proxy, revenue, dwell are documented non-negative in the contract. Generator stays in [0, ∞). If a future bid model wants signed terms, revisit the score-non-negative property.
  • HTML escaping: enforced at render boundary, not in core. Rationale: score is pure on raw strings; the browser layer owns output-encoding hygiene. Same separation Sponsored Products uses.
  • Duplicate-id dedup runs BEFORE domain dedup so two copies of the same item can't accidentally outrank a competitor on a tied domain.
  • select-grid is total: it absorbs every bad input shape (nil, non-map, missing fields, NaN) silently. Callers wanting strict validation can (filter valid-item? pool) themselves and compare counts.

6. Related literature

  • The content-recommendation widget format this module reproduces is the Taboola / Outbrain "around the web" unit; their publisher documentation (Taboola, n.d.; Outbrain, n.d.) defines the grid-of-cards shape and the sponsored-bypass on domain dedupe.
  • Viewability is measured with IntersectionObserver (MDN Web Docs, n.d.); the synchronous fire-all fallback is the pre-observer posture.
  • rel"sponsored"= on rendered links follows the WHATWG link-type definition (WHATWG, n.d.).

7. Cross-references

  • sponsored-display/spec.org: the sibling format whose render-boundary escaping this module copies.
  • sponsored-formats/spec.org: the unit taxonomy the native grid sits in.
  • beacon/spec.org: the optional telemetry sink (window._beacon.send).
  • ORACLE:
    • src/wal_sh/adtech/native_ads/core.cljc (pure)
    • src/wal_sh/adtech/native_ads/browser.cljs (window._nativeAds)
    • test/wal_sh/adtech/native_ads/core_prop_test.cljc (properties)

8. Open questions

  • Signed scoring terms. The score-non-negative property holds because every scoring key is documented non-negative; a future bid model with signed terms would need the property revisited.
MDN Web Docs. n.d. “Intersection Observer Api.” https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API.
Outbrain. n.d. “Publisher Help Center.” https://www.outbrain.com/help/publishers/.
Taboola. n.d. “Publisher Documentation.” https://help.taboola.com/hc/en-us/categories/115000012407-Publishers.
WHATWG. n.d. “Html Living Standard: Link Types (Rel=Sponsored).” https://html.spec.whatwg.org/multipage/links.html#link-type-sponsored.