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-gridandselect-top-nnever 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-htmlis 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):
IntersectionObserverfor 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-domainis idempotent - ∀ pool p:
dedupe-by-idis 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,dwellare documented non-negative in the contract. Generator stays in[0, ∞). If a future bid model wants signed terms, revisit thescore-non-negativeproperty. - 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-gridis 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-negativeproperty 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.