Porting Site JavaScript to Shared-Core ClojureScript

Table of Contents

Four rounds of porting the site's browser JavaScript to a shared-core Clojure/ClojureScript architecture: core.cljc for pure logic, browser.cljs for window globals and DOM, optional server.clj for JVM fixtures. 18 adtech modules, 2 webring namespaces, 7 site-wide scripts: 27 namespaces. Each round dispatched one sub-agent per module, fully isolated, then ran the same two-host test gate.

Round Modules JVM tests CLJS tests Cross-host bugs Latent bugs in the JS
1 5 adtech (pilot) 70 78 3 1
2 5 adtech 184 211 0 1
3 5 adtech + 2 wwn 419 436 0 2
4 3 adtech + 7 site 710 721 0 2

Test counts are cumulative. Rounds 1 to 3 were measured while writing this report; round 4 is cited from its commit (see Round 4).

Headline: the methodology is sound; the friction is entirely at the host seam, not in translating the logic. 50+ gotchas across 20 categories, zero in pure logic. Six latent bugs in the shipped JS surfaced by property tests.

One caveat up front. The ported namespaces run only under the test runners. Every page still loads the legacy scripts; see 5.

1. Round 1: pilot, five modules

Five JavaScript adtech modules ported in parallel. Five sub-agents, one module each, fully isolated. This round validated the architecture and produced the first gotcha catalog.

1.1. Setup

Module JS LOC Ported LOC (core+browser+server) Tests Assertions Properties Files
beacon 82 135 + 113 12 33 5 4
tag-manager 69 (core+browser+server) 15 25 5 5
ceddl 145 (core+browser) 12 35 4 4
clean-room 201 (core+browser+server) 16 83 4 5
ab-engine 315 (core+browser+server) 15 36 5 5
old stub: src/wal_sh/adtech/ab_engine.cljs deleted            

Totals: 5 modules, 23 source/test files, 70 JVM tests / 212 assertions, 78 CLJS tests / 237 assertions, 23 properties (~650 generated cases), 0 failures after one fix.

The 8-test / 25-assertion CLJS surplus is from the renamed core-prop-test namespaces being picked up by the regex (see below).

1.2. The four-phase loop, in practice

Each agent independently followed the same pattern:

  1. REPL drive core – eval shape on representative inputs.
  2. Properties at the REPL – write invariants, crank to 100–500.
  3. JVM test runclojure -M:test, all five modules: 70/0.
  4. CLJS test runnpx shadow-cljs compile node-test && node out/test.js: 3 failures, all in ab-engine.fnv1a-deterministic. After fix: 0/0.

Step 4 is the gate. JVM-only was not enough.

1.3. What translated 1:1

  • Pure transformations (map / filter / reduce shapes): every module.
  • Spec data shapes (CEDDL skeleton, AB experiment definition, governance tuple): straight EDN, no translation cost.
  • Property-test shape (defspec + prop/for-all) once the require asymmetry is paid (see gotchas).

1.4. What had to be redesigned

Only one module needed an architectural redesign rather than a translation: beacon. The JS conflated DOM signal collection (document.referrer, navigator.userAgent, location.href) with payload construction in one function. The pure payload function couldn't live in core.cljc until that was split into:

  • browser.cljs/collect-ctx – gathers DOM signals
  • core.cljc/payload – pure on (event, ctx)

This is the architecture's chief implicit demand: side-effect collection is a separate concern from data construction. Any module where the JS treats them as one will need this split during the port. Expect this again on ~5 of the remaining 13 modules.

1.5. The cross-host gate found a real bug

ab-engine implements a deterministic hash for variant assignment (fnv1a-32). JVM 15/0; CLJS 12/3 – three failures, all in fnv1a-deterministic:

expected: (= 2166136261 (ab/fnv1a-32 ""))
  actual: (not (= 2166136261 -2128831035))   ;; signed int32 view of same bits
expected: (= 3826002220 (ab/fnv1a-32 "a"))
  actual: (not (= 3826002220 -468965076))    ;; same
expected: (= 3214735720 (ab/fnv1a-32 "foobar"))
  actual: (not (= 3214735720 249808880))     ;; DIFFERENT bits

Two distinct bugs:

  1. Signed vs unsigned 32-bit: JS bitwise ops always return signed int32. After (bit-and h 0xFFFFFFFF) on a high-bit value, JVM gives a positive Long; CLJS gives a negative int32. Same bits, different interpretation.

    Fix: (unsigned-bit-shift-right h 0) on CLJS – compiles to JS >>> 0, the standard uint32 cast. JVM uses the existing mask.

  2. Float64 precision loss in JS multiply: the inner loop does hash = xored * 16777619. JS * returns float64; after a few rounds the product exceeds Number.MAX_SAFE_INTEGER (2^53) and silently loses precision. Hand-implemented FNV-1a with bare * diverges from the reference oracle for any input longer than ~3 characters.

    Fix: (js/Math.imul xored fnv-prime) – JS's 32-bit integer multiply with overflow truncation, exact integer semantics.

The ab-engine agent had predicted the hash-portability gotcha in its experience report ("cljs.core/hash is not JVM↔JS stable and Murmur3 is JVM-only"), picked FNV-1a because it's portable, and still hit both these sub-issues. The agent's Python oracle vectors caught one (signed/unsigned by inspection) but missed the float64-precision case because Python doesn't share JS's number model.

This is the methodology's strongest argument: only the cross-host gate catches host-arithmetic divergence. You cannot construct it from JVM testing alone.

1.6. Gotcha taxonomy (n=5 modules)

Category Count Examples
Require/auto-load asymmetries (CLJ auto, CLJS no) 4 clojure.walk; clojure.set (2×); defspec macro split
Static-interop syntax (Foo/bar vs js/Foo.bar) 3 Long/toString; Math/log; Math/abs
Methodology naming bug 1 core_prop regex miss (flagged by 2 agents independently)
Architectural redesign from JS concern-conflation 1 beacon's signal/payload split
Test-design (perf in property tests) 1 KS statistic outside quick-check, not inside
Spec gaps in original JS 1 CEDDL pageInstanceID required by spec, omitted by JS
Spec errors in PLANS.org 1 "lift symmetric under swap" only holds for absolute, not relative
Cross-host primitive (hash) 1 FNV-1a needed both >>> and Math.imul for parity

The pattern is overwhelming: 13 of 13 gotchas live at the host seam, in the test runner, or in the original JS's design. Zero gotchas were in the pure logic itself.

1.7. Methodology change applied: rename core_prop.cljccore_prop_test.cljc

Two agents independently flagged that shadow-cljs.edn's :node-test :ns-regexp "(pocket-es|wal-sh).*-test" matches core-test but silently skips core-prop. The CLJS-side property tests weren't running.

Fix: rename the file and ns suffix to core-prop-test across all modules. No shadow-cljs.edn edit needed. After rename, 8 additional tests / 25 additional assertions ran on CLJS – and that's where the ab-engine FNV-1a bug surfaced.

If we had merged before the rename, the bug would have shipped silently. The naming convention is now part of the methodology doc.

1.8. Build pipeline

Five new entries in shadow-cljs.edn :builds, one per module, all pointing at site/static/js/adtech/cljs/:

:adtech-beacon
{:target :browser
 :output-dir "site/static/js/adtech/cljs"
 :asset-path "/static/js/adtech/cljs"
 :compiler-options {:output-feature-set :es2020 :fn-invoke-direct true}
 :modules {:beacon {:init-fn wal-sh.adtech.beacon.browser/init!}}}
;; ... and analogous for :adtech-tag-manager, :adtech-ceddl,
;; :adtech-clean-room, :adtech-ab-engine

npx shadow-cljs release adtech-beaconsite/static/js/adtech/cljs/beacon.js (100K).

1.9. Verdict

Methodology validated. Concrete green checks:

  • Bundle: shadow-cljs release adtech-beacon produces a 100K bundle.
  • JVM tests: 5 modules, 70/0.
  • CLJS tests: 5 modules, 78/0 (after FNV-1a fix).
  • No js/ in any core.cljc: grep -rn 'js/' src/wal_sh/adtech/*/core.cljc → empty.
  • Window globals preserved: window._beacon, window._tm, window.digitalData, window._cleanRoom, window._abTest all bound by the respective browser.cljs init! forms.
  • Cross-host gate caught a bug we'd have shipped: yes.

Ready to apply to the remaining 13 adtech modules. Same template, same loop, same gate.

1.10. Open thread for the next batch

  1. Server.clj usefulness – only 3 of 5 pilot modules used it (tag-manager, clean-room, ab-engine). Decide per-module on demand; don't force.
  2. Output dir – pilot agents split between site/static/js/cljs/ and site/static/js/adtech/cljs/. The merged shadow-cljs.edn standardizes on adtech/cljs/ for adtech; document this in PLANS.org for the next batch.
  3. Math.imul + >>> 0 idiom – any module touching integer arithmetic should follow the FNV-1a pattern from ab-engine.core. Add a small wal-sh.adtech.util namespace with cross-host uint32-mul and to-uint32 helpers if a second module needs them.
  4. Property-test perf – clean-room's KS-outside-quick-check pattern is the template for math-heavy modules; document in DIRECTORY-AND-REPL.org.

2. Round 2 results

2.1. Headline

Five larger modules (246–398 LOC, 5 layers stressed) ported by five parallel sub-agents using the methodology fixed after round 1. The cross-host gate caught zero bugs on first aggregate run. This is the round-1 lessons paying compound interest – the prompt-template preamble (Appendix A.1) prevented every repeat-class bug proactively.

Aggregate (10 modules) JVM CLJS
Tests 184 211
Assertions 548 592
Failures 0 0
Errors 0 0

(CLJS extra tests = noop-scaffold smoke tests for the 13 unported modules + pocket-es token tests, all also green.)

2.2. Per-module round-2 footprint

Module LOC src+test Tests Assertions Properties (cases)
attribution ~620 19 66 9 (multi-hundred)
prebid-lite ~640 19 59 5
bot-greeting ~475 28 98 7 (1000 cases)
sponsored-products ~570 22 48 mixed
promo-engine ~432 26 65 7 + smoke

2.3. New gotchas (round-2 additions to the catalog)

# Category Module Detail
1 JVM Math missing constants attribution Math/LN2 is JS-only; hardcoded 0.6931471805599453
2 Float drift accumulation attribution Shapley n=7 → 128 coalitions → ~1.0 ± 1e-15; widened ε
3 IEEE-754 in division prebid-lite (floor (/ 0.30 0.10)) = 2.0 not 3.0; integer thousandths
4 Cross-host sort tie-break prebid-lite, sponsored-products Stable sort isn't enough; need explicit multi-key
5 array-map for >8 keys bot-greeting CLJS PersistentHashMap iteration ≠ JVM; force ArrayMap
6 str/lower-case on nil bot-greeting Crashes both hosts; defensive (or s "")
7 (defn apply …) shadows core promo-engine Breaks internal (apply max-key …); rename
8 clojure.string not auto-loaded in CLJS promo-engine Generalization of round-1 clojure.set/walk lesson

2.4. Architectural validations (NOT bugs – confirmations)

  • bot-greeting dodged the regex hazard entirely by using clojure.string/includes? instead of regex (the JS source did too). The prompt's "WATCH" warning produced caution; the caution produced no regex; the no-regex produced no bug. Methodology working as intended.
  • prebid-lite's pure-auction + impure-network split worked cleanly per the prompt's recommendation. core.cljc is testable on JVM without fetch simulation.
  • promo-engine declined to extract a shared rules-engine util. Compared its rule shape against tag-manager and reported: "Two data points isn't enough to abstract; revisit if a third rules-engine module appears." Good engineering judgment over reflex DRY.
  • sponsored-products empirically verified BigDecimal isn't needed for bounded float64 arithmetic. Negative finding documented as a tripwire for future cumulative computation.

2.5. Bugs found in the original JS (port + properties surface real bugs)

Round Module Latent bug in JS
1 ceddl pageInstanceID required by CEDDL §6.1.1, omitted by JS
2 sponsored-products Vickrey auction never caps clearing-price at the winner's own bid (only enforces second-price)

Two for two: the cross-host port + property tests find latent bugs the original JS hid. Pattern is robust enough to claim as a benefit: porting with properties is an audit of the original implementation.

2.6. PLANS.org spec errors surfaced

Module Spec error
prebid-lite spec says window._prebid; JS source binds window._headerBidding. Agent binds both.
sponsored-products "second-price = runner-up bid + 0.01" is ambiguous: bid vs effective-cpm? JS uses bid.
promo-engine public fn name apply shadows clojure.core/apply. Agent renamed to apply-rules.

PLANS.org should be patched with these clarifications before the remaining 8 modules dispatch.

2.7. Updated gotcha taxonomy (combined rounds 1 + 2, n=10)

Category Count Distribution
Require/auto-load asymmetries (CLJ auto, CLJS no) 5 clojure.walk; clojure.set (2×); clojure.string; defspec macros
Static-interop syntax (Foo/bar vs js/Foo.bar) 4 Long/toString; Math/log; Math/abs (2×)
Float-arithmetic edge cases 3 FNV-1a Math.imul; granularity-bucket; Shapley drift
Cross-host hash portability 1 FNV-1a needs >>> 0 AND Math.imul
Cross-host sort tie-break determinism 2 prebid-lite; sponsored-products
Methodology naming bug 1 core_prop → core_prop_test (2 round-1 flags)
Architectural redesign from JS concern-conflation 1 beacon signal/payload split
Test-design (perf in property tests) 1 KS outside quick-check
Map iteration order (need array-map) 1 bot-greeting friendly-bots
nil defensiveness 1 bot-greeting UA classification
Function-name collisions with clojure.core 1 promo-engine applyapply-rules
JVM Math missing JS constants 1 attribution Math/LN2
Spec gaps in original JS 2 ceddl pageInstanceID; sponsored-products Vickrey
Spec errors in PLANS.org 3 prebid window; second-price ambiguity; apply shadow
Negative findings (DON'T over-engineer) 2 BigDecimal not needed; rules-engine util premature

13 round-1 + 19 round-2 = 32 distinct findings across 10 modules. Average 3.2 per module. Still zero are in the pure logic itself – every entry lives at the host seam, in the test infrastructure, in the JS source's design, or in the methodology spec.

2.8. Methodology verdict

Robust. The round-1 → round-2 deltas:

  • Zero cross-host bugs in round 2 (vs 3 in round 1; the FNV-1a class).
  • Zero methodology changes needed (round 1 fixed core_prop naming).
  • 2× independent confirmation of architectural recommendations (async split, rules-engine non-extraction).
  • 2× "find a bug in the original JS" with property tests.

Ready to fan out the remaining 8 adtech modules in one bd-tracked batch with light supervision (per the A.4 criterion: 0 new categories AND ≤1 bug from the cross-host gate).

Remaining: search-ads, native-ads, metered-access, content-gate, pricing-engine, affiliate-engine, referral-program, influencer-links. Noop scaffolds already in place; same prompt-template applies with the Round 2 additions baked into the lessons list.

2.9. The claim

The gotcha-taxonomy table (32 rows, 15 categories, 2 negative findings) + the two "found a bug in original JS" stories + the methodology graph (round-1 spec → round-1 bugs → prompt-template update → round-2 zero-bug aggregate) is a compact, novel, and falsifiable result:

A shared-core .cljc port discipline with cross-host property tests finds bugs the original JS hides AND prevents re-introduction of the same bug class via prompt-template lessons learned.

The data is in this report's tables; the experimental setup is in Appendix A; the artifacts are the ported modules and the git log.

3. Round 3 results – 5 adtech + WWN (webring), with Contract + Negative dimensions

3.1. Headline

Round 3 added two new deliverable dimensions per module:

  • CONTRACT (~40-line <m>-contract.md): formal public-API spec – signatures with pre/post, invariants ∀ state s: …, window-global shape, totality/determinism/idempotency/monotonicity claims, I/O domains.
  • NEGATIVE STATES (~40-line <m>-negative.md): 6–10 catalog entries (input → expected behavior → property test). The core_prop_test.cljc was required to include defspecs for each rejection, not just happy-path properties.

Five sub-agents (search-ads, native-ads, metered-access, content-gate, pricing-engine) ran in parallel. A sixth agent ported the heaviest site-wide JS – webring (the WWN beacon system, 30K source, 4 namespaces: wal-sh.wwn.{beacon,ring}.{core,browser}).

Aggregate (15 adtech + 2 WWN namespaces) JVM CLJS
Tests 419 436
Assertions 1034 1068
Failures 0 0
Errors 0 0

Round-3 modules average ~33 JVM tests per module (vs round-1's ~14 and round-2's ~23). The Contract + Negative dimensions reliably produce ~1.5× more tests per module than happy-path-only round 2.

Cross-host gate caught zero bugs this round. Methodology has hardened to the point where the round-1+2 lessons + prompt-template

  • explicit negative-states discipline yield first-pass-clean ports

on both hosts.

3.2. Per-module round-3 footprint

Module LOC (impl) JVM tests Properties Contract Negative
search-ads core + browser 26 12 yes yes
native-ads ~205 core + 175 36 16+1 yes yes
metered-access 273 core + 150 32 7+ (1068) yes yes
content-gate core + browser 32 11 yes yes
pricing-engine core + browser + server 35 9 yes yes
wwn-beacon (sitejs) 130 core + 145 br (74 total) shared - -
wwn-ring (sitejs) 250 core + 180 br (74 total) ring-math props - -

3.3. New gotchas (round-3 additions, n+4 to the running catalog)

# Category Module Detail
12 char-vs-string host asymmetry webring (int "")= = NaN in CLJS (chars are strings); used byte ranges
13 format-string asymmetry content-gate (format "%08x" h) JVM-only; (.toString h 16) for CLJS
14 gen-class shadows clojure.core pricing-engine (def gen-class …) warns JVM, silent CLJS; renamed gen-fare-class
15 integer? behaves differently metered-access (integer? 1.0) true in CLJS, false in JVM

The lesson-9 "don't-shadow-core" category has now collected three instances: apply (round-2 promo-engine), methods (round-3 webring), gen-class (round-3 pricing-engine). Add gen-class to the proactive naming-check list.

3.4. Real bugs found in original JS (third-and-fourth instances)

Round Module Latent bug in JS
1 ceddl pageInstanceID required by CEDDL §6.1.1, omitted by JS
2 sponsored-products Vickrey auction never caps clearing-price at the winner's own bid
3 native-ads JS used Math.random() shuffle, PLANS.org specified deterministic ctr × revenue × dwell score; agent ported to spec
3 pricing-engine yield's max-base bound broke final-price composition (surge can push to 4× base); relaxed to max-price

Four for four: every round has surfaced at least one bug in the original implementation that property tests catch but the JS shipped silent. Porting with properties is an audit of the original.

3.5. PLANS.org spec errors / under-specifications surfaced

Module Issue
search-ads PLANS defines composite quality-score; JS uses int 1–10. Agent exposed both, documented.
content-gate PLANS said "case-sensitive"; JS does .toUpperCase(). Port followed code.
pricing-engine "yield with 0 availability → max-multiplier OR rejected" was genuinely under-specified.
webring data-wwn-rewrite-links short-circuit on w=0 ∧ t=0 not in spec; preserved from JS.

Patch PLANS.org with these clarifications before the remaining 3 adtech modules dispatch.

3.6. Cross-module reuse confirmed (architecture paying off)

content-gate imported wal-sh.adtech.ab-engine.core/fnv1a-32 for path-bound unlock tokens. Bit-exact JVM↔CLJS verified. Validates the round-1+2 thesis: the architecture is starting to produce real shared utilities. The fnv1a-32 is now the canonical cross-host hash for the project – anything needing a deterministic integer hash should use it rather than rolling its own.

3.7. Contract-tightening pattern (round-3-specific)

Every round-3 agent tightened the spec at port time. Examples:

  • search-ads: NaN/Inf bid rejection (JS lacked it); XSS escape (spec didn't require).
  • native-ads: silent-drop of NaN-score items; sticky-slot exception to dedup invariant formalized.
  • metered-access: UTC-only buckets (kills DST/skew); over-limit? strictly >; multi-tab is documented eventual-consistency.
  • content-gate: 256-char length cap before hashing (DoS short-circuit); failure result has NO :code key (XSS via echo precluded structurally).
  • pricing-engine: closed-interval [floor, ceil]; NaN/Inf competitors silently dropped; BigDecimal-not-needed verified by 1000-iteration drift test.

These are the kinds of decisions a careful security review of the original JS would surface. The round-3 prompt's explicit Contract + Negative dimensions force the agents to reason about them. The output is a per-module ~40-line negative-states catalog you could hand to an auditor.

3.8. WWN (webring) port – sitejs precedent

The webring port validated the wwn 4-namespace split predicted in the sitejs-cljc PLANS.org (§webring). Strategic decisions:

  • JSONP callback registry as a defonce atom + JS-object mirror in wal-sh.wwn.beacon.browser (NOT in core). Core stays pure and only generates callback names; browser owns the mutable state. Lesson 8 (separate side-effect collection from data) cleanly applies.
  • Cross-host JSON byte equality via (sort-by name) on map keys before serialization. Property test pins a golden vector across hosts.
  • Ring math has the textbook properties: round-trip (next (prev i)) = i, wrap-around at boundaries, monotonicity (apply next k times) = (+ i k) mod |rs|.

LOC: 806 JS → ~870 CLJS (reader-conditional + per-host interop split overhead; the pure core is denser but the browser adapter ~1:1).

3.9. Updated gotcha taxonomy (rounds 1+2+3, n=17 namespaces)

Category Count Examples
Require/auto-load asymmetries (CLJ auto, CLJS no) 5 clojure.walk; clojure.set (2×); clojure.string; defspec
Static-interop syntax (Foo/bar vs js/Foo.bar) 4 Long/toString; Math/log; Math/abs (2×)
Float-arithmetic edge cases 4 FNV-1a Math.imul; granularity; Shapley drift; search-ads ε
Cross-host hash portability 1 FNV-1a >>> 0 + Math.imul
Cross-host sort tie-break determinism 3 prebid-lite; sponsored-products; pricing-engine
Methodology naming bug 1 core_prop → core_prop_test
Architectural redesign from JS conflation 1 beacon signal/payload split
Test-design (perf in property tests) 1 KS outside quick-check
Map iteration order (array-map) 1 bot-greeting friendly-bots
nil defensiveness 1 bot-greeting UA classification
Function-name collisions with clojure.core 3 apply (promo); methods (webring); gen-class (pricing)
JVM Math missing JS constants 1 attribution Math/LN2
Spec gaps in original JS (port found bug) 4 ceddl pageInstanceID; sponsored Vickrey; native-ads random; pricing yield-bound
Spec errors in PLANS.org (port corrected) 6 promo apply; sponsored bid/cpm; prebid window; +3 round-3
Negative findings (DON'T over-engineer) 2 BigDecimal not needed (×2 – sponsored + pricing 1k drift)
Char-vs-string host asymmetry 1 webring url-encode (int "")= = NaN in CLJS
Format-string asymmetry 1 content-gate (format "%08x") JVM-only
integer? host asymmetry 1 metered-access (integer? 1.0) true CLJS, false JVM

40 round-1+2 + ~10 round-3 = 50+ findings across 17 namespaces. Still zero in pure logic itself – every entry lives at the host seam, in test infrastructure, in JS's design, in spec ambiguity, or in arithmetic-host-asymmetry.

3.10. Methodology verdict (rounds 1+2+3)

Robust + scalable.

  • Round 1: 5 modules, 3 cross-host bugs (FNV-1a class).
  • Round 2: 5 modules, 0 cross-host bugs (round-1 lessons in prompt prevented).
  • Round 3: 5 modules + WWN (2 ns), 0 cross-host bugs, contract+negative dimensions add ~1.5× tests per module.

Ready to fan out the remaining 3 adtech modules + 7 site-wide JS modules in a single bd-tracked batch, with the round-3 prompt template as the contract. Remaining adtech: affiliate-engine, referral-program, influencer-links. Remaining site-wide JS: color-swatches, heading-anchors, event-pixel, web-vitals-init, bot-signal, global-pollution, research-checklist (per the sitejs-cljc PLANS.org).

3.11. Round-3 deliverables shipped

10 source files (5 round-3 adtech × {core.cljc, browser.cljs}) + 2 server.clj files (metered-access, pricing-engine) + 10 test files (×{core_test, core_prop_test}) + 8 wwn files (4 src + 4 test) + 20 snippet files (5 adtech × {build, contract, negative, experience}) + 2 wwn snippets (build + experience) = 52 files

Plus the cross-cutting :builds merge: 7 new entries in shadow-cljs.edn.

4. Round 4: three adtech + seven site-wide

Round 4 ran ten modules in parallel and closed the port set. The numbers in this section are taken from the message of commit 26c64cf6 (2026-05-31, "feat(cljs): round 4"); they were not re-run for this note.

4.1. Headline

  • 3 adtech: affiliate-engine, referral-program, influencer-links. Completes the 18-module adtech set.
  • 7 site-wide: color-swatches, heading-anchors, event-pixel, web-vitals-init, bot-signal, global-pollution, research-checklist. Every <script src> in the page postamble now has a CLJS bundle.
Aggregate (rounds 1-4, 27 namespaces) JVM CLJS
Tests 710 721
Assertions 1581 1609
Failures 0 0

The initial JVM run showed one shrink flake, not reproducible across four subsequent runs. That single event prompted the shrink-discipline note: a flake in a property test is a discovery.

4.2. Cross-module reuse landmark

fnv1a-32 now has four callers: the ab-engine original, content-gate, referral-program, research-checklist. Past the three-consumer threshold; extraction to wal-sh.lib.hash is the obvious next step. url-encode + to-query-string has two explicit callers (event-pixel direct, wwn-beacon origin) plus two hand-rolled equivalents (heading-anchors, influencer-links): four modules want the same primitive, also extraction-worthy.

4.3. Bugs found in original JS (fifth and sixth)

Round Module Latent bug in JS
1 ceddl pageInstanceID required by CEDDL §6.1.1, omitted by JS
2 sponsored-products Vickrey auction never caps clearing-price at the winner's own bid
3 native-ads JS used Math.random() where the spec said deterministic score
3 pricing-engine yield's max-base bound broke final-price composition
4 referral-program XSS via innerHTML + template literal; port switched to createElement + textContent
4 affiliate-engine Commission cap silently accepted 200% in JS (no validation); port rejects rate > 1.0

The commit labels this tally "5 total" while listing six rows; the table counts the rows. Every round surfaced at least one bug the JS shipped silently.

4.4. New gotchas (round-4 additions)

# Category Detail
16 defonce rejects docstrings Flagged by 2 agents, self-corrected
17 Agent tooling: NULs in regex classes The Write tool inserts literal NULs in regex char classes; workaround re-pattern with \\x00 hex escapes. Confirmed by 2 independent agents
18 Agent tooling: silent parent-dir write Write to a nonexistent parent dir succeeds silently; create dirs first

Two new categories (defonce semantics; agent-tooling artifacts) on top of the round-3 table: 20 categories, 50+ findings, still zero in pure logic.

4.5. Dependency, build, and docs

  • org.clojure/data.json 2.5.0 added to the :test alias only. research-checklist needs JVM-side JSON; CLJS uses js/JSON.stringify natively.
  • 10 new shadow-cljs.edn :builds entries (3 adtech-* + 7 site-*), 20 build configurations in total.
  • A load contract shipped alongside: every postamble <script> tag mapped to its CLJS bundle, init-fn, window global, migration flag, and test counts. Its load-order constraints now live in docs/dynamic-globals-plan.org; see the next section for why.

5. The cutover never happened

As of 2026-09-10, three months after round 4, the ported namespaces run only under the test runners; no page loads them. Each claim below was checked with grep against the working tree; line numbers are cited.

  • Every include for the 18 ported adtech modules still loads the legacy script. Line 1 of site/includes/adtech-<m>.html is <script src"/static/js/adtech/<m>.js"></script>= for all 18: ab-engine, affiliate-engine, attribution, beacon, bot-greeting, ceddl, clean-room, content-gate, influencer-links, metered-access, native-ads, prebid-lite, pricing-engine, promo-engine, referral-program, search-ads, sponsored-products, tag-manager. The six includes that do load from /static/js/adtech/cljs/ (attribution-audit, exit-intent, intent-signals, sponsored-display, sponsored-research, stuffing-detectors) belong to modules written in CLJS after the port; none of the 27 namespaces here is among them.
  • publish.el still emits the six legacy site scripts. Lines 728-733 of publish.el write heading-anchors.js, event-pixel.js, color-swatches.js, research-checklist.js, bot-signal.js, and global-pollution.js into the postamble from /static/js/. The seventh, web-vitals-init.js, loads through site/includes/web-vitals.html line 1. The webring loads the legacy webring.js from site/includes/webring.html line 6; the wal-sh.wwn.* bundles are unreferenced.
  • No USE_CLJS_* flag exists. grep -rn USE_CLJS over site/, publish.el, and src/ returns nothing. The only traces are the comment at shadow-cljs.edn lines 52-54, which describes window.__ADTECH_ENV__.USE_CLJS_<MODULE> as the replacement mechanism, and docs/dynamic-globals-plan.org line 205, which records that the mechanism was never built.
  • Built bundles that nothing loads. site/static/js/adtech/cljs/ holds ab-engine.js, beacon.js, bot-greeting.js; site/static/js/cljs/ holds wwn-beacon.js. No include and no line of publish.el references any of them.

The reasons are outside this note. The plan that now owns the cutover is docs/dynamic-globals-plan.org: it keeps the load-order constraints from the round-4 load contract and replaces the per-module flag with an inline boot plus an external manifest, spent in one full republish.

6. Appendix A: Round 2 dispatch

This appendix records what the 5 round-2 sub-agents were told. Two parts: a shared preamble (architecture + lessons from round 1 + boundaries) prepended to every prompt, then a per-module spec. The prompts are the experimental setup; the verbatim text is kept in a comment block at the end of this section, invisible on export.

Module selection rationale: round 1 sampled the 5 lowest-LOC modules (69-315 LOC) to validate the methodology. Round 2 stresses the methodology against larger, riskier modules – 246-398 LOC – with deliberate exposure to the failure modes round 1 either dodged or narrowly caught:

Module LOC Layer Stress dimension
attribution 398 tracking 6 distinct math models – biggest property surface
prebid-lite 246 ads async/timeout/AbortController in browser
bot-greeting 297 privacy text classification + cross-host regex (the kind of
      bug pocket-es tokenizer had – direct re-test)
sponsored-products 335 ads sealed-bid second-price auction arithmetic
promo-engine 343 revenue rules engine – extends tag-manager, code-share?

6.1. Shared preamble, summarized

The preamble fixed the architecture as binding: core.cljc with no js/* and no java.io; browser.cljs for DOM, fetch, storage, and the window.<global> binding under its exact JS name; optional server.clj; core_test.cljc and core_prop_test.cljc on both hosts. File paths use _, namespaces use -. The -prop-test suffix is required (round 1 caught that core_prop alone is skipped by shadow-cljs's :node-test ns-regexp).

Eight lessons from round 1, baked in proactively:

  1. Avoid JVM-only regex inline flags (?si); use [\\s\\S]*? for any-char-including-newlines. (The pocket-es tokenizer bug.)
  2. Static-method interop splits per host: #?(:clj (Math/sqrt x) :cljs (js/Math.sqrt x)).
  3. clojure.set and clojure.walk are auto-loaded on JVM but not in CLJS; require them explicitly.
  4. defspec / for-all need split macro require forms (:refer on JVM, :refer-macros on CLJS).
  5. js->clj with :keywordize-keys true gives keyword keys; raw JSON fixtures are string-keyed. Defensive: (or (:k m) (get m "k")).
  6. Math-heavy property tests run expensive stats once per parameter value, not inside prop/for-all.
  7. Cross-host hash: never cljs.core/hash or Murmur3; use the FNV-1a reference in ab-engine.core with its >>> 0 and Math.imul fixes.
  8. If a JS function both collects side effects and builds data, split it: pure builder in core, signal collector in browser.

Boundaries: no edits to shadow-cljs.edn or deps.edn (proposed :builds entries go to snippets/<m>-build.edn for a manual merge); no commits. Deliverables per module: the source and test files, the build snippet, and a 30-40 line experience file. Reply in at most 120 words: paths, top 3 gotchas, whether both hosts pass, any spec error noticed in PLANS.org.

6.2. Per-module specs, summarized

Each spec named the source file and LOC, the core/browser/server split per PLANS.org, 5+ unit tests, the headline properties, and a WATCH paragraph aimed at that module's stress dimension:

  • attribution: six pure credit models; property ∀ journey, ∀ model: sum credits ≈ 1.0. Watch Math/exp per-host split.
  • prebid-lite: keep the auction pure on (bids, floor, granularity); only the fetch + AbortController collect lives in browser. Watch monotonicity in timeout.
  • bot-greeting: classification total over UA × signals. Watch every regex for inline flags (the pocket-es bug class, commit 0051462).
  • sponsored-products: second-price, quality-adjusted; winner pays runner-up bid + ε. Watch stable sort and deterministic tie-breaks.
  • promo-engine: rules over a cart. Watch for reuse against tag-manager's rules engine; either answer is signal.

6.3. Dispatch metadata

subagent_type: general-purpose. The agents have NO knowledge of each other's progress – full isolation. Each works against the noop scaffold in place (commit 230c3eb) and the validated round-1 modules as reference.

Total prompt size per agent: shared-preamble (1600 chars) + per-module- spec (600-900 chars) ≈ 2200-2500 chars. Full agent context budget allows ~200k tokens; the prompt is <1% of that, leaving room for the agent to read JS source + round-1 module files + write its deliverables.

Estimated wall clock per agent: 3-5 min on round-1 evidence (with larger LOC + new failure modes, expect 5-8 min here). 5 in parallel ≈ the slowest of the 5.

6.4. Post-round-2 aggregation plan (mirror of round 1)

  1. Merge each agent's :builds snippet into shadow-cljs.edn.
  2. JVM test run across all 10 ported modules.
  3. CLJS test run across all 10 ported modules (cross-host gate).
  4. Fix any CLJS failures (root-cause + reader conditional).
  5. Append a "Round 2 results" section to this report covering: test counts, new gotcha categories discovered, any methodology changes, bugs caught by the cross-host gate, code-reuse findings (esp. promo-engine vs tag-manager rules-engine question).
  6. If 0 new gotcha categories AND the cross-host gate catches ≤1 bug: the methodology is robust enough to fan out to the remaining 8 adtech modules in a single bd-tracked batch with less supervision.