Porting Site JavaScript to Shared-Core ClojureScript
Table of Contents
- 1. Round 1: pilot, five modules
- 1.1. Setup
- 1.2. The four-phase loop, in practice
- 1.3. What translated 1:1
- 1.4. What had to be redesigned
- 1.5. The cross-host gate found a real bug
- 1.6. Gotcha taxonomy (n=5 modules)
- 1.7. Methodology change applied: rename
core_prop.cljc→core_prop_test.cljc - 1.8. Build pipeline
- 1.9. Verdict
- 1.10. Open thread for the next batch
- 2. Round 2 results
- 2.1. Headline
- 2.2. Per-module round-2 footprint
- 2.3. New gotchas (round-2 additions to the catalog)
- 2.4. Architectural validations (NOT bugs – confirmations)
- 2.5. Bugs found in the original JS (port + properties surface real bugs)
- 2.6.
PLANS.orgspec errors surfaced - 2.7. Updated gotcha taxonomy (combined rounds 1 + 2, n=10)
- 2.8. Methodology verdict
- 2.9. The claim
- 3. Round 3 results – 5 adtech + WWN (webring), with Contract + Negative dimensions
- 3.1. Headline
- 3.2. Per-module round-3 footprint
- 3.3. New gotchas (round-3 additions, n+4 to the running catalog)
- 3.4. Real bugs found in original JS (third-and-fourth instances)
- 3.5.
PLANS.orgspec errors / under-specifications surfaced - 3.6. Cross-module reuse confirmed (architecture paying off)
- 3.7. Contract-tightening pattern (round-3-specific)
- 3.8. WWN (webring) port – sitejs precedent
- 3.9. Updated gotcha taxonomy (rounds 1+2+3, n=17 namespaces)
- 3.10. Methodology verdict (rounds 1+2+3)
- 3.11. Round-3 deliverables shipped
- 4. Round 4: three adtech + seven site-wide
- 5. The cutover never happened
- 6. Appendix A: Round 2 dispatch
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:
- REPL drive core – eval shape on representative inputs.
- Properties at the REPL – write invariants, crank to 100–500.
- JVM test run –
clojure -M:test, all five modules: 70/0. - CLJS test run –
npx shadow-cljs compile node-test && node out/test.js: 3 failures, all inab-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 signalscore.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:
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.Float64 precision loss in JS multiply: the inner loop does
hash = xored * 16777619. JS*returns float64; after a few rounds the product exceedsNumber.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.cljc → core_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-beacon → site/static/js/adtech/cljs/beacon.js (100K).
1.9. Verdict
Methodology validated. Concrete green checks:
- Bundle:
shadow-cljs release adtech-beaconproduces a 100K bundle. - JVM tests: 5 modules, 70/0.
- CLJS tests: 5 modules, 78/0 (after FNV-1a fix).
- No
js/in anycore.cljc:grep -rn 'js/' src/wal_sh/adtech/*/core.cljc→ empty. - Window globals preserved:
window._beacon,window._tm,window.digitalData,window._cleanRoom,window._abTestall bound by the respectivebrowser.cljsinit!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
- 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.
- Output dir – pilot agents split between
site/static/js/cljs/andsite/static/js/adtech/cljs/. The mergedshadow-cljs.ednstandardizes onadtech/cljs/for adtech; document this inPLANS.orgfor the next batch. - Math.imul + >>> 0 idiom – any module touching integer arithmetic
should follow the FNV-1a pattern from
ab-engine.core. Add a smallwal-sh.adtech.utilnamespace with cross-hostuint32-mulandto-uint32helpers if a second module needs them. - 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 apply → apply-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). Thecore_prop_test.cljcwas 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
:codekey (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.json2.5.0 added to the:testalias only. research-checklist needs JVM-side JSON; CLJS usesjs/JSON.stringifynatively.- 10 new
shadow-cljs.edn:buildsentries (3adtech-*+ 7site-*), 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 indocs/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>.htmlis<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.elstill emits the six legacy site scripts. Lines 728-733 ofpublish.elwriteheading-anchors.js,event-pixel.js,color-swatches.js,research-checklist.js,bot-signal.js, andglobal-pollution.jsinto the postamble from/static/js/. The seventh,web-vitals-init.js, loads throughsite/includes/web-vitals.htmlline 1. The webring loads the legacywebring.jsfromsite/includes/webring.htmlline 6; thewal-sh.wwn.*bundles are unreferenced.- No
USE_CLJS_*flag exists.grep -rn USE_CLJSoversite/,publish.el, andsrc/returns nothing. The only traces are the comment atshadow-cljs.ednlines 52-54, which describeswindow.__ADTECH_ENV__.USE_CLJS_<MODULE>as the replacement mechanism, anddocs/dynamic-globals-plan.orgline 205, which records that the mechanism was never built. - Built bundles that nothing loads.
site/static/js/adtech/cljs/holdsab-engine.js,beacon.js,bot-greeting.js;site/static/js/cljs/holdswwn-beacon.js. No include and no line ofpublish.elreferences 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:
- Avoid JVM-only regex inline flags
(?si); use[\\s\\S]*?for any-char-including-newlines. (The pocket-es tokenizer bug.) - Static-method interop splits per host:
#?(:clj (Math/sqrt x) :cljs (js/Math.sqrt x)). clojure.setandclojure.walkare auto-loaded on JVM but not in CLJS; require them explicitly.defspec/for-allneed split macro require forms (:referon JVM,:refer-macroson CLJS).js->cljwith:keywordize-keys truegives keyword keys; raw JSON fixtures are string-keyed. Defensive:(or (:k m) (get m "k")).- Math-heavy property tests run expensive stats once per parameter
value, not inside
prop/for-all. - Cross-host hash: never
cljs.core/hashor Murmur3; use the FNV-1a reference inab-engine.corewith its>>> 0andMath.imulfixes. - 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. WatchMath/expper-host split. - prebid-lite: keep the auction pure on (bids, floor, granularity);
only the fetch +
AbortControllercollect 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)
- Merge each agent's :builds snippet into shadow-cljs.edn.
- JVM test run across all 10 ported modules.
- CLJS test run across all 10 ported modules (cross-host gate).
- Fix any CLJS failures (root-cause + reader conditional).
- 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).
- 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-trackedbatch with less supervision.