Repository navigation
Merge the maintained fork (v1.11 through v1.15.0) - #16
Merged
Merged
Conversation
…ture A native golden run could not fail. `clojure.java.io/resource` returns nil for every name by design (D-359: cljw has no classpath resource loader), so hive-test's `ClasspathProjectRoot` resolved to nil, `root/anchor` passed the RELATIVE path straight through, and the golden landed against the process working directory. `assert-golden` then writes a snapshot and PASSES when none exists. Composed, a session rooted at the repository wrote an unreviewed baseline at the repo root and reported success. Fixed through the seams hive-test already publishes, so no upstream change and no publish: `*project-root*` gets a strategy that cannot return nil, `*store*` gets one that refuses to create a golden unless UPDATE_GOLDEN=true and read-back-verifies one it does write. `laws/-main` calls `anchor/install`, which resolves the root AND asserts the reviewed corpus in one call. Identity and corpus are separate checks. Folding the goldens into the directory markers made a deleted golden fail identity, so the run blamed the working directory when the real fault was one missing file in the right one. Layered per Stratified Design + CPPB: `candidate-order` (where the bug actually lived) is a pure function of four strings, with narrow `Environment` and `Filesystem` ports above it, so the whole rule is exercised against an in-memory tree with no disk and no redef. Anchoring reads `java.class.path` and `user.dir`, which both runtimes implement. It must not use `io/resource`. This exposed a cljw gap: `java.io.File` published `getCanonicalPath` and `getAbsolutePath` but not their File-returning twins, so `.getCanonicalFile` raised "No implementation of method". Added both as compositions of the String halves plus `allocFile`, registered in METHODS. Evidence: - laws 21 tests / 42 assertions, native and JVM, both printing the fixture root - anchor 17 tests / 39 assertions, identical on both runtimes, no disk touched - negative, native: a removed golden fails naming that file and creates nothing in the fixture or at the repo root - SMOKE_CORE green (591 tests / 2874 assertions, 111 suites); e2e e2e_phase16_java_io_file, e2e_phase16_clojure_java_io, e2e_phase16_file_io_errors, e2e_phase14_java_static_dispatch all pass Refs ADR-0019 (no panic on user-reachable paths), D-359. Smell-audited: 3: split identity from corpus after the first cut conflated two faults; lifted the anchoring rule into a pure fn behind Environment/Filesystem ports rather than leaving decisions welded to the filesystem
…t closed Resume contract no longer points at PR #20 (merged, v1.14.4 shipped) but at draining CLJW-COMPLIANCE from the 2026-09-10 baseline, with the warning that the 43 failing namespaces are not 43 bugs: 15 assertion failures across quot/mod/rem are AD-018 on purpose, and the stale baseline files under private/notes/compliance/ list a subvec panic that is already fixed. Records that carto can now read and write .zig (the hive.shape.zig tier was never mounted), with the caveat that insert-form does not retag the rows it shifts, so a second edit below an earlier one in the same file cannot be addressed. Records the golden-root card as done and the smoke-selector `e2e_` prefix trap. Back to exactly 100 lines by condensing the wasm FFI, bench and e2e-sweep bullets, whose detail lives in the memories they cite. Smell-audited: 0: clean, docs only
A macro's return value is a runtime Value; `valueToForm` converts it back into
a Form so the analyzer can re-analyze it. That converter rebuilt a collection
from its ELEMENTS only and never read the Value's metadata, so a macro returned
`^:a []` stripped. The `.symbol` arm had carried symbol meta across since
ADR-0110, with a comment saying a macro must not silently shed it; the four
collection arms were simply missed.
Probed which direction it was lost in before touching anything: metadata
arrives INTACT at the macro (a macro that prints `(meta x)` at expansion time
sees it) and is gone on the way OUT, including metadata the macro itself
attaches with `with-meta`. So the fault is the Value->Form conversion, not
argument marshalling and not syntax-quote (a hand-built `(list 'do ...)` macro
loses it too).
One shared floor, `withValueMeta`, routed through all four collection arms
rather than four copies, reading through `runtime/meta.zig`'s SSOT meta switch,
which answers nil for a tag that cannot carry meta so `.chunked_cons`/`.range`
cost one comparison. Attaching `Form.meta` is sufficient: D-186 already lowers
a meta-bearing collection literal to `(with-meta <bare> <meta-map>)`, so both
backends attach it on the shared `with-meta` path and the dual-backend parity
contract stays unengaged.
A returned LIST is safe: `form.meta` is consulted only for `.vector`/`.map`/
`.set`, so a list's meta rides the Form and is ignored on the code path (it is
honoured by `formToValue` on the quoted-data path, which is what it is for).
`(do ...)` is NOT rewritten into `(with-meta (do ...) ...)`.
Why it matters beyond one literal: every `clojure.test` assertion lives inside
a `deftest`/`testing` body, so this made CORRECT functions fail their own
upstream suite while passing every direct probe. `clojure.core-test.group-by`
reported 6 failures whose expressions all pass under `cljw -e`, under a loaded
.clj file, and against the JVM. It now reports 0, with no change to `group-by`.
Evidence:
- the two-line repro now agrees with clj (`:thru-macro {:a true}`)
- `clojure.core-test.group-by`: 6 failures -> 0
- new gated suite `test/clj/suites/macro_literal_meta_test.clj`: 20 assertions,
0 failures, IDENTICAL on cljw and the JVM oracle. The oracle was run first,
and it rejected two assertions of mine: `^:l` on a quoted list attaches to the
quote form (so even clj yields `{:line :column}`), and cljw attaches no reader
position meta to a quoted form at all. Both are recorded in the test instead
of asserted around.
- smoke green: 22 passed / 0 failed / 778s, 112 suites, both zig backends,
corpus_regression, and e2e phase6_16_a_0_metadata, phase14_defmacro_user,
phase14_def_meta_quote_value, phase15_macro_heaplong, macro_throw_catchable,
phase14_fn_macro
Refs ADR-0110 (symbol meta round trip), D-186 (meta-coll lowering).
Smell-audited: 2: rejected the four-copy version for one shared floor at the
valueToForm switch; checked the list arm could not turn (do ...) into a
with-meta call before trusting the fix
…nt conj does
Persistent `conj` on a map was already taught that `(conj {} nil)` is a no-op
and `(conj {:a 1} {:b 2})` merges entry by entry, and that behaviour is pinned
in test/diff/clj_corpus/conj_map_merge.txt. The TRANSIENT twin was missed: it
required a [k v] pair and raised IllegalArgumentException for nil and for a map,
which also broke `(into (transient {}) {…})` since `into` builds via a
transient.
Oracle-checked before and after with bb scripts/clj_diff_sweep.clj:
(persistent! (conj! (transient {}) nil)) clj {} cljw threw
(persistent! (conj! (transient {}) {})) clj {} cljw threw
(persistent! (conj! (transient {}) {:a 1})) clj {:a 1} cljw threw
(persistent! (conj! (transient {:z 0}) {:a 1 :b 2})) clj merged cljw threw
Transient VECTOR and SET conj! were already correct and are left alone.
The merge iterates through `map_mod.forEachEntry`, the shared walk that
`map.mergeInto` uses a few lines above this file's import of it, rather than
re-deriving a per-representation walk for array_map vs hash_map. GC-rooted: the
transient `assoc` allocates, so a collect mid-merge would otherwise sweep the
source map and the accumulator, neither of which the GC can see in a Zig local.
Not changed, and deliberately still divergent: a non-nil, non-map, non-[k v]
entry (e.g. '(:a 1)) raises IllegalArgumentException where clj raises
ClassCastException. That is the pre-existing D-459 choice; it was a DIFF before
this commit and remains one, recorded in the corpus header.
Evidence:
- clojure.core-test.conj-bang: 5 errors -> 0 failures, 0 errors
- corpus test/diff/clj_corpus/transient_conj_merge.txt, 12 golden pairs appended
via --corpus per clj_diff_sweep.md Discipline 1, so the claim is mechanically
re-checkable. It also pins the deliberate asymmetry that
`(conj! (transient []) nil)` is [nil] while the map treats nil as nothing.
- smoke green: 20 passed / 0 failed / 554s, including corpus_regression (which
now runs the new corpus), both zig backends, e2e phase8_transient_consumed_read,
phase14_deftype_transient_family, phase16_gc_torture, phase14_realworld_program
Both edits landed through carto write-form / insert-form on the Zig tier.
Refs D-459, D-199.
Smell-audited: 1: reused map.forEachEntry rather than writing a second
per-representation map walk next to the one mergeInto already uses
…nsient nil) throw
D-199 declared a live transient a first-class READ target and wired `get` for a
transient map and vector. The rest of the surface was missed, so a correct
declaration was only half true:
((transient [1 2 3]) 1) clj 2 cljw "Cannot call value of type"
((transient {:x 1}) :x) clj 1 cljw "Cannot call value of type"
((transient #{42}) 42) clj 42 cljw "Cannot call value of type"
(:x (transient {:x 1})) clj 1 cljw nil
(:x (transient #{:x})) clj :x cljw nil
(get (transient #{42}) 42) clj 42 cljw nil
THE ARITIES ARE NOT UNIFORM, and every one was checked against the oracle
rather than inferred from the persistent sibling, because inferring would have
been wrong three times:
- a transient MAP and a transient SET both accept an optional not-found
(clj's ATransientSet implements the 2-arity invoke), where a PERSISTENT set
is 1-arity only
- a transient VECTOR is 1-arity and THROWS on a bad index rather than
answering nil: ((transient [1 2 3]) 99) is IndexOutOfBounds, not nil
- ((transient v) 1 :dflt) is an ArityException, not a defaulted read
Routed through the existing shared paths rather than new ones:
- the three transient tags join the `lookup_mod.invoke` arm in treeWalkCall,
documented as the single both-backend value-producing call choke point, so
the VM gets this via vt.callFn and the ADR-0036 parity contract stays
unengaged (the same leverage D-186 used for with-meta)
- `vectorIndex` gained a .transient_vector branch, so the not-an-integer and
out-of-range errors cannot drift from the persistent vector's
- getFn's new .transient_set arm mirrors its .hash_set sibling exactly
Also: `(transient nil)` answered an empty transient vector where clj throws.
That arm was the only one in transientFn with no D-number justifying it, and
returning a value there turns a typo into a silently empty accumulator. nil now
falls to the mismatch arm. The 0-arity `(conj!)` identity builds its empty
transient by calling transient_vector.fromVector directly, so it is unaffected.
Evidence:
- read-surface sweep 12/20 -> 20/20 against the clj oracle
- clojure.core-test.transient: 3 failures + 3 errors -> 0 failures, 0 errors
(51 assertions)
- corpus test/diff/clj_corpus/transient_read_surface.txt, 25 golden pairs, with
a header recording WHY the three transients disagree on arity so a later
reader does not "simplify" them into agreement
- smoke green: 21 passed / 0 failed / 528s, including corpus_regression, both
zig backends, e2e phase8_transient_consumed_read,
phase14_deftype_transient_family, phase16_gc_torture, phase14_realworld_program,
phase14_arity_parity
NOT fixed, filed instead as [CLJW-VECFN-BADINDEX] and confirmed PRE-EXISTING by
testing the persistent path: a non-integer index raises ClassCastException where
clj raises IllegalArgumentException, uniformly across vector, sub_vector and now
transient_vector, because all three share vectorIndex. One helper, one future fix.
lookup.zig, collection.zig and transient.zig were edited through carto
write-form / insert-form; tree_walk.zig out of band (it is absent from the
polyglot index, so no qn in it resolves).
Refs D-199, ADR-0036, D-186.
Smell-audited: 2: extended the shared vectorIndex instead of writing a second
index/bounds path for transients, and routed the call through the existing
both-backend choke point rather than adding a transient-specific one
… arity first
clj makes `assoc!` deliberately MORE lenient than `assoc`: a trailing key with
no value is accepted and its value is nil. The upstream suite states it in a
comment ("on the contrary to assoc, assoc! accepts an odd number (> 1) of args
and assumes missing value is nil") and the oracle confirms it:
(persistent! (apply assoc! (transient []) [0 1 1])) => [1 nil]
(persistent! (apply assoc! (transient [1]) [0 1 1 2 2])) => [1 2 nil]
(persistent! (apply assoc! (transient {:a 1}) [:b 2 :c])) => {:a 1 :b 2 :c nil}
(persistent! (apply assoc! (transient {:a 1}) [:b 2 :c 3 :d])) => {:a 1 :b 2 :c 3 :d nil}
cljw rejected every one with map_literal_arity_odd, which is `assoc`'s rule
applied to `assoc!`.
A SECOND fault in the same guard, found by sweeping the boundary cases rather
than only the headline one: the even-count rejection ran BEFORE the arity
check, so `(assoc! tm :b)` reported a key-without-value error where clj reports
an ArityException. Too few args is an arity fault. Arity is checked first now,
and the even-count rejection is gone entirely.
The nil default goes through one `pairedValue` helper shared by all three
loops (transient map, transient vector, user ITransientAssociative), so the
leniency cannot be applied to one representation and missed on another.
Evidence:
- oracle sweep 6/12 -> 10/12; both survivors are ArityException vs
clojure.lang.ArityException, the same exception type differing only in clj's
package prefix (AD-007), so functionally 12/12
- clojure.core-test.assoc-bang: 5 errors -> 0 failures, 0 errors (38 assertions)
- BATCH B1 of private/notes/compliance/drain-plan-2026-08-22.md is now complete:
transient 51/0/0, conj-bang 51/0/0, assoc-bang 38/0/0
- corpus test/diff/clj_corpus/transient_assoc_bang_arity.txt, 9 golden pairs
- smoke green: 20 passed / 0 failed / 509s, including corpus_regression, both
zig backends, e2e phase14_arity_parity, phase8_transient_consumed_read,
phase14_deftype_transient_family, phase14_realworld_program
Edited out of band: assocBangFn sits below an earlier edit in the same file, and
carto cannot address a Zig form below a prior edit (the index does not retag
shifted rows).
Smell-audited: 1: routed the nil default through one shared helper instead of
repeating the bounds test in each of the three loops
…ass parent cljw's derive accepted any value in either position, so (derive :user/tag 42) silently installed a number as a parent, and (derive ::a :b) accepted an un-namespaced parent into the GLOBAL hierarchy where one library's :shape can collide with another's. clj's 2-arity is deliberately stricter than its 3-arity on exactly that point: a hierarchy value you pass in is your own, the global one is shared by every namespace. Both arities are now checked the way clj checks them, verified against the oracle rather than inferred. clojure.core-test.derive goes from 11 failures to 1. The residual one is deliberate and now carries a ledger row: cljw permits a CLASS parent where clj raises ClassCastException, because ADR-0109 makes a host class a first-class hierarchy participant. (derive ::x Object) then (isa? ::x Object) is a tracked behaviour of test/e2e/phase14_opaque_host_class.sh, and that isa? is FALSE without the derive, so the edge is real rather than implied by Object being the universal supertype. Refusing a class parent would make an ADR-0109 hierarchy reachable only through make-hierarchy, which the global defmulti path does not read. A first pass at this rejected the class parent and broke that e2e (37 assertions red). The suite assertion is about class-as-PARENT; the probe that had looked green was class-as-TAG. The two are not the same edge. Refs ADR-0109, AD-068. Smoke: e2e_phase14_opaque_host_class, e2e_phase7_multimethod, e2e_phase7_catch_hierarchy, e2e_phase15_campaign_keywords - 20 passed, 0 failed. Smell-audited: 2
…tier ledger The two File-returning resolution methods landed on the java.io.File surface without being added to that class's `methods:` row, so check_compat_members reported one drift and the full gate went red on both platforms in PR #21. The methods themselves are correct and tested; only the ledger was behind. The check is the reason the ledger is trustworthy: an implemented member that no row lists is exactly what it exists to catch, so the fix is the listing, not a waiver. Gate: check_compat_members ok, 42 surface classes, member lists match the code. Smell-audited: 1
(nth m 2) on a re-matcher raised "No implementation of method '-nth' on protocol 'Indexed' for type 'java.util.regex.Matcher'". clj's RT.nth has a Matcher arm that answers group n, and clojure.core-test.nth exercises it in both arities, which is where the 7 errors in that namespace came from (its only failures; it is green now). The Matcher descriptor already had a method table, but every entry registered under the empty protocol name, so nothing could reach it through a protocol. MethodSpec gains a `proto` field (the java.util.ArrayList shape, which already registers -seq and -count that way) and `-nth` registers under Indexed. nth's generic arm then dispatches to it with no change to the primitive. The semantics are clj's, checked against the oracle rather than assumed: index 0 is the whole match, a group that did not participate is nil, an index past groupCount raises or answers the not-found value, and a NEGATIVE index raises in the 2-arity but answers not-found in the 3-arity, because clj's not-found arm returns early for a negative index. That last asymmetry showed up as the one DIFF in the first sweep and is now pinned. Pinned in test/diff/clj_corpus/matcher_indexed.txt (10 pairs, 10/10 matched). Smoke: e2e_phase14_re_matcher, e2e_regex_parity_gaps, e2e_phase14_instaparse_substrate, corpus_regression - 16 passed, 0 failed. Smell-audited: 1
…ncluded
Three defects behind 11 failed assertions in clojure.core-test.{int,byte,short},
none of them guessable from the failure text:
1. `int` and `long` shared one body, so `int` never range-checked at all:
(int 2147483648) answered 2147483648 where clj raises. They are separate now,
intCoerce narrowing and longCoerce not.
2. clj range-checks a DOUBLE before narrowing it, so (byte 127.9) raises rather
than truncating to 127. clj selects that overload by STATIC type: a literal
reaches byteCast(double), a boxed Double reaches byteCast(Object) and
truncates first. cljw has one unboxed f64 whatever produced it (F-005), so one
rule has to cover both, and the primitive rule is the one that carries the
intent of a narrowing cast. (byte (identity 127.000001)) therefore raises here
and is 127 on clj: AD-070.
3. NaN is asymmetric IN CLJ, verified against the oracle rather than reasoned
from RT.java: 0 for int/long, IllegalArgumentException for byte/short. A
NanPolicy parameter carries that instead of a comment.
Also: an out-of-range cast is clj's IllegalArgumentException, not a
ClassCastException, so those raises moved from .type_arg_invalid to
.arg_value_invalid. A user catch clause on IllegalArgumentException now behaves
as it does on clj.
int 5 failures -> 1, byte 3 -> 1, short 3 -> 1. The 3 residuals are the
`instance? Integer/Byte/Short` assertions: cljw has no narrow integer types
(F-005 / ADR-0059), which is AD-069. Both new rows are pinned.
Pinned in test/diff/clj_corpus/narrowing_casts.txt (44 pairs, 44/44 matched) and
suites.numeric-tower-test/narrowing-cast-range.
Smoke: test_clj_suites, corpus_regression, e2e_phase15_unchecked_math,
e2e_phase14_math_exact, golden - 17 passed, 0 failed.
Refs AD-069, AD-070.
Smell-audited: 2
(eval (fn [x] x)) raised "macro return value of tag 'fn_val' cannot be
re-analysed as a form", and (eval +) the same for 'builtin_fn'. eval fed its
argument to the analyzer unconditionally, so any value with no source spelling
(a closure, a builtin, a deftype instance) failed instead of evaluating.
clj analyses only a COLLECTION or a SYMBOL and returns everything else
unchanged: the final else of Compiler.eval. An already-evaluated value has no
form to analyse, which is the whole reason (eval (fn [x] x)) is that fn. eval now
follows that rule, which also makes it cheaper for the self-evaluating tags it
used to walk through the analyzer.
The seq VIEWS stay on the analysed side, which the sweep caught: (eval (seq
[1 2])) is the form (1 2) in clj and fails to call 1, so .array_seq and
.string_seq belong with the other seq tags, not with the self-evaluating ones. A
first pass omitted them and answered the seq itself.
clojure.core-test.eval goes from 2 errors to green (34 assertions).
Also records keys / vals on a non-map as AD-071 rather than changing them. clj
answers nil for (keys #{}) and (keys "") but throws on realization for
(keys #{1}), because its KeySeq wraps seq(coll) and casts each element only when
walked. Answering nil for an empty argument and throwing for a larger one of the
same type is not a contract worth adopting; cljw checks the argument once and
names the type that failed. The same row covers the other direction, where cljw
is more lenient: (keys (list [:a 1])) works here because a MapEntry IS a 2-vector
(ADR-0078), and clj's per-element IMapEntry cast rejects it.
One residual, left as a separate parity question: a 0-arg keyword call reports
ClassCastException where clj reports ArityException.
Pinned in test/diff/clj_corpus/eval_values.txt (28 pairs, 28/28 matched) and
suites.map-helpers-test/keys-vals-demand-a-map.
Smoke: e2e_phase14_eval, e2e_phase14_fn_macro, e2e_phase14_defmacro_user,
test_clj_suites, corpus_regression - 16 passed, 0 failed.
Refs AD-071.
Smell-audited: 1
…round-trip
(rationalize 1.1M) raised "expected number". The switch handled integer,
big_int, ratio and float, and a BigDecimal fell into the not-a-number else, so
the one numeric type that already IS an exact decimal was the only one
rationalize refused.
It now works from the value itself: the unscaled significand over 10^scale,
gcd-reduced, for a positive scale, and the digits with -scale trailing zeros for
a non-positive one. No float is involved, so nothing is lost that the
BigDecimal was chosen to keep: (rationalize 12345678901234567890.5M) is
24691357802469135781/2 exactly, where a double round-trip drops the low digits.
The integral arm needed the quotient computed here. ratio.allocFromManagedPair
answers null when the reduction collapses to den == 1, leaving the caller to
emit the integer, and it takes its pair as const - so the numerator it was
handed is still UNREDUCED. Wrapping that answered 10 for 1.0M, 100 for 1.00M
and 100 for 10.0M. A divTrunc by the denominator fixes it. The float path above
shares the same tail and cannot reach that arm: a normalised float render
carries no trailing zero, so its numerator is never divisible by 10^scale,
which is why the bug is new to the BigDecimal path rather than latent in both.
Records AD-072 for what remains: an integral result is cljw's machine integer
where clj has a BigInt (1 vs 1N), because cljw's exact-integer tier is chosen by
MAGNITUDE per F-005 and not by the producing operation. The values are =, which
the pin asserts. AD-016 already accepts the mirror case.
Found on the way: cljw's reader REJECTS a ratio literal whose numerator exceeds
i64 ("Invalid float literal '24691357802469135781/2'"), so cljw constructs a big
ratio it cannot read back. Filed separately; the pin spells that case through
numerator / denominator with a comment, rather than working around it silently.
CHANGELOG also picks up the eval fix from 706a562, which landed without an
entry.
Pinned in test/diff/clj_corpus/rationalize_bigdec.txt (29 pairs) and
suites.numeric-tower-test/rationalize-bigdec-is-exact (18 assertions).
Smoke: test_clj_suites, corpus_regression, e2e_phase14_math_exact - 17 passed,
0 failed, 112 suites loaded.
Refs AD-072, F-005.
Smell-audited: 1: Silent-default-shift avoided - the null arm of allocFromManagedPair was treated as 'already the answer' when it is 'caller emits the quotient'; the contract is now read from its docstring rather than assumed.
…a sleep
bounded-pool-runs-saturated-work-on-caller failed on the macOS gate of the
v1.14.5 release PR:
expected: (= ["main" :caller] (.get caller-job))
actual: (not (= ["main" :caller] ["Thread-4" :caller]))
The third job ran on the worker instead of on the caller, so the pool was not
saturated when it was submitted. Saturation was held by (Thread/sleep 120) in
the first job: a 120 ms race window, not a synchronisation point. On a loaded
runner the first job finished, the worker took the queued job, the one-slot
queue emptied, and CallerRunsPolicy was correctly NOT triggered. The assertion
failed for a reason it was not written to detect, which is the exact failure
mode .claude/rules/test_taxonomy.md § "Wall-clock assertions" describes.
Both saturation tests now pin the worker on a Semaphore the test itself
releases, so the pool is provably busy for as long as the assertions need it,
at any load. The handoff is an AtomicBoolean the worker sets, waited on with a
bounded loop: a deadline separates a genuine hang from a busy machine (the safe
half of a wall-clock assertion), where the old sleep bounded neither.
No cljw behaviour changes; this is the test's own synchronisation.
Smoke: test_clj_suites, corpus_regression, e2e_phase14_math_exact - 17 passed,
0 failed, 112 suites loaded, 5 tests / 24 assertions in this suite.
Smell-audited: 1: Progress-pressure resisted - the quick fix was a longer sleep, which only moves the race window; the sleep was replaced by a real gate.
The Linux leg of the v1.14.5 release PR failed with
error: failed to spawn build runner .zig-cache/o/<hash>/build: FileNotFound
and the warm-build step then printed "build failed for a reason unrelated to
the cache" and failed the job. It is the cache: a restored .zig-cache that
references a file it does not contain, which is precisely the condition the
step exists to heal.
The recovery was gated on grep "failed to check cache" - the wording of the
first poisoning ever observed. A restored cache has at least two spellings for
one condition (a missing compile output, and a missing build runner binary),
so the predicate reported a confident false negative and wedged the release.
The gate is gone. Any failed warm build now clears .zig-cache and builds once
more. Wiping a content-addressed cache costs a rebuild and nothing else, and
the retry is a stronger discriminator than any log match: a failure with no
cache present is unambiguously about the source, and it is that second build
whose output the job reports. A healthy run is unaffected, since it never
reaches the retry.
Smell-audited: 2: Smallest-diff bias rejected - adding the second wording to the grep would have left the third to find us; the predicate itself was removed.
… exact BigDecimal rationalize Compliance drain plus the two release-gate fixes. Runtime fixes: - a re-matcher is Indexed, so nth reaches its capture groups (core-test.nth 7 errors to green) - int, byte and short enforce their own range, and a double is range-checked before it narrows - a value that is not a form evaluates to itself (core-test.eval 2 errors to green) - rationalize accepts a BigDecimal and works from its exact decimal, no float round-trip - derive validates its tag and parent, and keeps the class parent - transient fixes: assoc! trailing key, readable and callable live transient, conj! on a map - the analyzer carries a collection's metadata across macroexpansion - java.io.File publishes getCanonicalFile and getAbsoluteFile Ledger: 4 new accepted divergences (AD-069 narrowing yields a Long, AD-070 a double is range-checked before narrowing, AD-071 keys/vals demand a map eagerly, AD-072 an integral rationalize result is a machine integer), each with a derives_from and a pin. 66 rows, gate green. Four new clj-oracle corpora. Gate fixes, neither in the runtime, both of which had reddened this PR: - the executor saturation tests hold the sole worker on a semaphore instead of a 120 ms sleep, which a loaded macOS runner outran - the CI cache self-heal no longer matches one wording of the poison, so it stops failing the build for the condition it exists to heal CI green both platforms at 70bf18c.
…colon Release sync moves to v1.14.5 (tag 4421dcf, both artifacts), origin/main fast-forwarded into staging, [Unreleased] empty again. First commit on resume is now [CLJW-BOX-CTORS], whose card carries a 37-form oracle sweep and a settled design, with the two things NOT to do recorded as Forbidden. Corrects a factual error that cost a blocked push tonight: the entry said the push hook "wants Smell-audited: <0-4>: with a BARE digit". The hook's regex is ^Smell-audited:[[:space:]]*[0-4][[:space:]]*: so it needs the digit, a COLON and a summary; a bare `Smell-audited: 1` is refused. I wrote the bare form on this advice, the push was blocked, and three commits had to be rewritten. Also records that a self-heal and a sleep-held precondition BOTH read as runtime failures before they were diagnosed, since that is what happens next time CI is red, and trims back to the 100-line cap (the ADR-0198 landing and the perf-stop line were duplicated by git log and the unfinished section). Smell-audited: 0: clean, a docs-only edit with no source diff
…f memory
Measured before deleting anything. The glob auto-loader re-sent, per edit:
one .zig edit 12 rules ~18,000 tokens
one .md edit 6 rules ~7,900
one .clj test edit 4 rules ~2,700
every session 5 rules ~3,300 (** glob)
whole corpus 32 rules ~46,700
A session that touches Zig a few times spends six figures of tokens on prose it
already has in memory. The 2026-09-09 audit recorded 6 rules / 456 lines for a
session; the corpus grew past that without anyone measuring again.
Three preconditions, each verified rather than assumed:
1. The mirror is complete. All 32 exist in memory tagged rule-<name>, and their
ids are now the table in .claude/CLAUDE.md.
2. No script reads a rule file. Every .claude/rules/ mention in scripts/ was a
citation printed in an error message, including the one real variable
(check_gate_cadence.sh's RULE=, interpolated only into output). The gates read
the YAML SSOTs and the source.
3. Enforcement already lived in memory, not in the files: 16 of the 32 entries
carry a guard-rule block loaded by hive-knowledge.guard.memory-source. Those
fire whether or not anything reads them, which this session demonstrated
repeatedly, and once more AFTER the deletion when
:guard/orphan-background-no-timeout caught an unwrapped background loop in a
tree with no rules directory at all.
So the tier was a cache with worse economics than its source: all-or-nothing per
glob, re-sent per edit, and no enforcement role.
The 18 scripts and the settings.json hook have their citations repointed to
memory ids, because CLAUDE.md now asserts that they are, and an assertion has to
be made true rather than left as prose. Two remain in the handover scripts, which
the next commit removes.
Residual risk, stated in the decision entry rather than glossed: glob injection is
deterministic and semantic recall is not. The split that absorbs it is the user's
own axiom, rationale in memory and enforcement in guards, so the 16 rows with no
G are advice by definition. When one proves mechanizable, write its guard-rule
block into the entry; never restore a file, because the duplicate is what drifts.
Kept deliberately: the 4 YAML SSOTs (gate inputs, machine-read) and docs/ +
README + CHANGELOG + CONTRIBUTING (product documentation).
Decision entry: 20260910235746-74389f7f. It replaces the ADR that would have
recorded this, since writing that ADR as a file is the practice being retired.
Follow-up: 20260911000859-212f6199 (guard reason texts still cite dead paths).
Smoke: 17 passed, 0 failed, 112 suites loaded.
Smell-audited: 2: Reservation-as-bias rejected - "the files stay on disk" was a reservation from the mirroring cycle, and nothing had re-measured whether its two stated reasons still held; both had lapsed.
The file cost more to maintain than it carried. Its own rule policed a 100-line
cap that took a dozen turns to satisfy earlier today, and that rule
(handover_framing.md) was itself ~8k tokens injected on every .md edit, so the
policing cost more per edit than the thing policed. Four artifacts maintained one
piece of session state: the file, the rule, check_handover_framing.sh, and a
SessionStart hook that cat'd it.
Everything durable in it is now in memory or was already there. Five facts lived
ONLY in the file and were rescued first, because "it is an md file" is not a
reason to delete something unmirrored:
20260911001201-09a51b5b the release path: a push to main whose .version has
shipped cuts the next patch, what the preflight bumps,
stamps and tags, and never hand-cut a release first
20260911001202-2ee19e2f the smell-audit trailer regex (digit COLON summary),
forward commits, CI is ONE configuration, and never
kill a co-tenant JVM
Everything else in the file either already cited a memory id or was volatile
state that git log, the CHANGELOG and the kanban board hold better. The resume
path is `project workflow catchup` plus the board; the "first commit MUST be"
line is replaced by raising [CLJW-BOX-CTORS] to high priority.
Two traps found while removing it, both of which would have shipped broken:
1. The SessionStart hook was an && chain gated on `test -f .dev/handover.md`, so
deleting the file alone would have silently stopped printing `git log -3` too.
The hook is rewritten rather than trimmed.
2. test/units.list registered check_handover_framing as a GATE step, so deleting
the script without the row would have reddened the full gate on a missing
file.
Smoke: 17 passed, 0 failed.
Smell-audited: 1: Silent-default-shift watched for - the resume ORDERING was load-bearing in the deleted file, so it moved to a card priority rather than being dropped as incidental.
Per user direction: knowledge lives in memory + KG, not md files across the repo. I argued against this one on the grounds that ADRs cost no per-turn tokens (they are not auto-loaded) and that deleting them breaks citation targets. The user chose full removal; recording the disagreement rather than pretending it did not happen, since the reversal is one `git revert` away if the reader disagrees. MIRROR AUDIT FIRST, and it is the reason this is safe: 199 of 199 ADRs have a memory entry tagged adr-NNNN. The only file without one is 0000_template.md, which is a template and which both source-bearing classifiers already excluded by name. The first audit pass reported 67 unmirrored; that was WRONG, an artifact of a query truncated at its 250-entry limit, and re-running at 600 returned 417 entries and closed the gap to zero. A pagination default reads as absence, and acting on the first number would have been a fabricated 67-file knowledge gap. Every EXECUTABLE consumer is handled here, not left to rot: - check_adr_affected_files.sh and check_adr_history.sh are DELETED with their units.list rows. The first one mattered: it globs the ADR dir with `[ -e "$f" ] || continue`, so with no ADRs it would have gone green on any input forever. A vacuous gate is worse than a deleted one because it still reports success. - The ADR arms are removed from check_smell_audit.sh and check_learning_doc.sh, which classified an ADR edit as source-bearing. Both still parse (bash -n). - Four printed citations now name a `memory query :tags ["adr" "adr-NNNN"]` recipe instead of a path. A recipe cannot rot the way 199 ids would. - check_debt_id_refs.sh already EXCLUDED .dev/decisions/, so it is untouched. CLAUDE.md's priority chain no longer names files for layer 3: implementation decisions are entries (adr-NNNN and cljw-rule), amendable by editing the entry and never by re-creating a file. Known residue, filed rather than hidden: prose in .dev/ and docs/ still links ADRs by path, so those links 404 (20260911001938-70d8404d). Deliberately not swept here; a mass prose rewrite would have buried the consumer changes above, which are the ones that decide whether the gate stays honest. Decision entry: 20260910235746-74389f7f. Smoke: 17 passed, 0 failed. Smell-audited: 3: Reservation-as-bias and vacuous-gate both caught - "ADRs are cited by commit messages" was a reservation nobody had re-tested (nothing validates those citations), and check_adr_affected_files would have survived as a gate that passes on an empty corpus.
Layer 4 of the priority chain moves to memory. No script reads the file; all four references were citations printed in error messages, now repointed at the corpus query or the specific entry. The mirror was INCOMPLETE and that is the part worth recording. An ingestion pass on 2026-09-01 had already written 18 of the smells one entry each, so the file looked mirrored. Six were missing, and one of them is Reservation-as-bias, which I had cited in a commit message earlier in this same session. Also missing: three of the four premises, the three finished-form questions, the meta-layer rule, and the whole structural-imagination section with its F-003 scope boundary and F-015 narrowing. Written now: 20260911004141-07f46c49 Reservation-as-bias smell 20260911004142-5ea8db18 Stale-phase-ref smell 20260911004143-5c7cb201 Defer-to-amnesia smell 20260911004143-529fb7da Ad-hoc-pass smell (F-013, the allowlist tell) 20260911004144-129398c8 Framework-incomplete smell 20260911004145-390e301f Dual-backend drift smell 20260911004145-29ffd52a the premises + the three questions + the meta layer 20260911004146-720ff81e structural imagination, F-003 boundary, F-015 narrowing "Already ingested" is not the same as "completely mirrored", and checking the difference is cheap next to losing a sensor the loop cites in its own audits. The CLAIM I MADE EARLIER AND HAVE NOW DISPROVED, recorded so it does not get repeated: I said moving project_facts.md to memory would lose nothing mechanical because a file cannot enforce its own immutability either. It can, and it does. scripts/check_facts_immutable.sh diffs the STAGED project_facts.md against `git show HEAD:.dev/project_facts.md`, fails a commit that removes an F-NNN, and fails an amendment with no revision-history entry. Moving that file to memory makes the gate vacuous (never staged, so exit 0 forever) and leaves nothing between the loop and project law. ROADMAP.md is machine-read too: it is an input to check_capability_claims.sh (the claim gate) and to binary_size_report.sh's size-claim check. Both are SSOTs with gates, not agent prose, so both stay as files pending the user's call. Smoke: 17 passed, 0 failed. Smell-audited: 1: Framework-incomplete avoided - the discovery recipe (compare the catalogue against tag bad-smell) was run BEFORE deleting, which is what surfaced the six missing smells.
convergence_campaign, library_incorporation_playbook, cw_v0_parity_and_gap_plan and tech_debt_consolidation are migrated and deleted. 22 entries, 23 KG edges. What survived is the durable part: the 12 library-blocker classes with a fix site each (20260911004716-29dac88c), the M1-M5 debt-trigger leak modes (20260911004940-38e9586e), redesign-first v0 incorporation (20260911004837-4f399769), which v0 gaps are still unincorporated (20260911004838-1e222586), the undrained D-175 C/D/E items (20260911004942-2c0c9674), and stage exits as mechanical predicates (20260911004835-63674963). Spent history was NOT mirrored: finished checklists, the 2026-05-29 parity snapshot, landed work lists. git already has those and a mirrored checklist is just a slower way to read git. No gate reads any of the four. check_debt_id_refs.sh named tech_debt_consolidation.md in an rg EXCLUDE regex, which only stops matching, and the same regex still carried .dev/decisions/ from the ADR deletion; both are dropped now. Every inbound reference is repointed rather than left dangling, including five in ROADMAP.md that I had broken in the three preceding commits: A22 cited the deleted ADR template, A23 cited principle.md plus a deleted rule, A24 and A25 cited principle.md, and A26 instructed consulting the v0 parity plan. ROADMAP is priority 2 in the chain, above decisions, so leaving its invariant table pointing at deleted files was the worst of the residue. STILL BLOCKED, and deliberately not forced: .dev/structure_plan.md stays. F-008 in project_facts.md cites its `src/runtime/wasm/` subtree BY NAME, and F-NNN is user-declared law the loop never amends on its own. The content is mirrored ahead of the decision (20260911004615-45865d5d holds that subtree, promoted to permanent; 20260911004613-4f993c1a holds the three-family placement rule; 20260911004614-2649e859 records where the anticipated tree diverged from what actually landed, which is most of it). debt.yaml's D-175 citation is left alone: it sits inside a wide multi-colon prose field where a line edit corrupts the file, and rewriting it with yq for a cosmetic link is not worth that risk. Filed instead, along with a real defect found on the way: D-175 is in the discharged: list while its discharged_at field says `quality-loop floor: debt-hygiene`, so the ledger contradicts itself about whether the row is open (20260911005500-2e1bc578). Smoke: 17 passed, 0 failed. Smell-audited: 2: Defer-to-amnesia avoided twice - the structure_plan block and the D-175 contradiction both got cards with testable next steps instead of a mention in prose.
Re-requiring a Wasm component swapped the handle but left every Var the PREVIOUS build had interned. An export the new build dropped stayed callable and reached into the old instance, which is the stale-Var half of the hot-swap problem: the swap looked clean because the names still resolved. The export table is now the source of truth for which names a component owns. require-component* tags each Var it interns with :cljw.wasm/component-export and :cljw.wasm/component-path, and on a re-require retires the tagged Vars the new table no longer has. In the :as namespace that is every tagged Var; under :refer only those interned from the SAME path, since several components may be referred into one namespace. A Var the user defined, or re-defined over an export name, is never touched: def replaces the metadata and with it the tag. Retiring rebinds the root to a fn that throws "component <path> no longer exports `name`" BEFORE unmapping, so a caller that captured the Var gets a catchable error naming the export rather than a call into the previous instance. That needed ns-unmap, which cljw did not have. Removing a mapping is not enough on its own: the Namespace owns its mappings keys AND its Var bodies, so freeing on unmap dangles any captured #'ns/x, and simply dropping the reference both leaks the Var and un-roots it from the GC. The Var therefore MOVES to a per-namespace `retired` list: the ns_vars root walk visits it after `mappings`, and namespace teardown frees it. The Var stays alive and usable through a captured reference, exactly as on the JVM, where unmapping is a namespace operation and not a Var one. ns-unmap is an upstream clojure.core name, so it does not enter data/core_surface_extras.txt: that ledger records only what cljw publishes BEYOND upstream, and this narrows the gap rather than widening it. The gate passes unchanged at 107 extras with no regen. data/placement.yaml and core_meta.clj regenerated by their own generators. placement picks up the three new vars; core_meta picks up ns-unmap's upstream docstring and, incidentally, four rows of pre-existing drift (file-seq, load-file, munge, proxy) that the same pipeline had been missing. Verified in the worktree: new e2e phase16_wasm_require_component_reswap 20/20; full gate 359 passed, placement_drift and doc_coverage green. The gate's one remaining blocking failure, adr_affected_files, is pre-existing and unrelated: run_all.sh:367 still calls a script commit 1e8a058 deleted (tracked separately).
First migration under the direction that cljw tests are cljw SUITES, not bash e2e. Nothing this test asserts is about the process boundary: it evaluates expressions and checks values, which is exactly what the suite tier is for, so the script and its fixture are deleted rather than kept alongside. What the bash tier was doing for it: spawn a process, run a fixture that printed 20 "PASS <name>" markers, and grep stdout for each one. A failure reported a missing line of output. The suite reports the VALUE that was wrong, which is how the two real defects below were found at all. Two things the script could do that a suite cannot, both now handled explicitly rather than by accident: 1. The script ran top to bottom, so `#'swp/get` was READ after the component had created the `swp` namespace. A suite COMPILES the whole file first, when `swp` does not exist, so any `#'swp/get` or `(swp/counter 5)` in the file fails it to LOAD. Every component Var is resolved at run time through `ns-resolve`. 2. `run-tests` does not promise definition order, and the generations are a sequence over state on disk and in captured Vars. The sequence is one deftest with `testing` sections carrying the old marker names, so the migration stays auditable. Also: the user-def setup uses `binding [*ns* ...]`, not `in-ns`. Under a test runner *ns* is already thread-bound, so `in-ns` mutates the ROOT while `eval` keeps reading the thread-local value and the def lands in the calling namespace. The script could use `in-ns` because at top level the root binding is the one in effect. Caught by the suite failing with the component tag still present on `increment`. run_all.sh loses a step; test/e2e loses 317 -> was 318. check_e2e_reach, check_e2e_dup and check_test_reach all green. Suite tier: 113 suites, 602 tests, 2971 assertions, 0 failures.
`(hash (slurp "some.wasm"))` core-dumped cljw and took the REPL with it.
A cljw String is raw BYTES (AD-009), so it can hold anything a file or a
Wasm guest buffer contains, but javaStringHashCode and hashUnencodedChars
both walked it with std.unicode.Utf8View.initUnchecked. That call's
contract is "the caller already validated"; its iterator reads past the
end of a truncated sequence and trips `unreachable`, which is a process
ABORT in every safe build, not a catchable Clojure error.
The asymmetry is the interesting part. `count` had already been taught
that the representation can be invalid and raised a clean type error, so
the codebase LOOKED like it handled this, and nothing pointed at the
other readers of the same bytes. A validated entry point is evidence
about that entry point and nothing else.
`hash` cannot take the raising option that `count` legitimately takes. A
value that cannot be hashed cannot be a map key or a set member, so
raising would make a String conditionally un-mappable for a reason the
caller cannot see, and the failure would surface far away inside `assoc`.
Both now decode through nextCodepointLossy: U+FFFD per invalid byte,
advancing one byte. Valid UTF-8 decodes byte-identically to what the
checked iterator produced, so no JVM parity value moves. Distinct invalid
strings may collide, which a hash is allowed to do; `=` still compares
bytes.
The helper uses the fixed-width utf8Decode2/3/4, which take an ARRAY.
That is what makes them the non-deprecated spelling: the length is
comptime per branch, so a caller cannot hand them a slice whose length
disagrees with its lead byte, which is the hazard `utf8Decode`'s slice
API is deprecated for. zlinter flagged the first version for exactly
this. Its unreachable-shaped `else` is spelled as a return instead: this
function exists BECAUSE an unreachable on malformed input aborted the
process, and re-introducing one here would be the same bet.
Tested at both tiers. src/runtime/hash.zig covers the byte patterns
(Wasm header, truncated 2/3/4-byte sequences, stray continuations, an
invalid byte between valid text). suites/hash-invalid-utf8-test covers
the guarantee a Clojure caller depends on: every String is hashable,
therefore usable as a map key.
Neither can FAIL on regression, they abort the runner, so the decisive
evidence is differential:
pre-fix binary: panic: attempt to unwrap error: Utf8ExpectedContinuation
-> dumped core
fixed binary: 10 assertions, 0 failures
zig fmt and zlinter clean; smoke gate green.
# Conflicts: # CHANGELOG.md
The previous fix made hashing total over arbitrary bytes but replaced
per BYTE. The JDK replaces per MAXIMAL SUBPART, so the two disagreed on
every truncated sequence: for {0x41, 0xE2, 0x82} the JVM's
new String(bytes, UTF_8) holds ['A', U+FFFD] and cljw produced
['A', U+FFFD, U+FFFD], a different hashCode. The docstrings claimed JVM
parity that the code did not deliver.
utf8_total.Iterator is a drop-in for std.unicode.Utf8Iterator that is
total over arbitrary bytes and reproduces the JDK's substitution exactly,
including the two places it departs from the plain Unicode reading: ED
accepts 80..BF as its second byte, and a complete 3-byte sequence
encoding a surrogate is ONE ill-formed span. Oracle values were measured
on JDK 25 and 26.
compare.zig carried the SAME abort and was missed by the earlier fix:
javaStringCompareTo still walked both strings with
Utf8View.initUnchecked, so (compare a b) on a String holding
non-UTF-8 bytes -- (slurp "x.wasm") produces one -- trips unreachable
and kills the process. It now decodes totally, and falls back to byte
order when the unit sequences tie, so 0 is returned exactly when the
strings are =.
Verified: zig build test; 21 measured JDK oracle cases; exhaustive
totality over every 1-, 2- and 3-byte input; a 20k-iteration property
run; suites 604 tests / 2981 assertions / 0 failures, 114 loaded /
0 failed to load; zig build lint --max-warnings 0 clean.
Makes String hashing and comparison total over arbitrary bytes AND byte-identical to the JVM, and fixes the second abort site in compare.zig that the earlier hash-only fix missed.
…pare fix The entry still described the first, per-byte fix through nextCodepointLossy. d9806b0 replaced that with utf8_total.Iterator (maximal-subpart substitution, JDK oracle values) and closed the same abort in javaStringCompareTo, neither of which the entry mentioned. The release preflight stamps [Unreleased] verbatim, so it has to be right before the PR, not after.
The Pike VM carried a fixed [16]i32 capture array per thread (MAX_SLOTS_INLINE: 8 slot pairs with pair 0 unused), so the parser rejected the 8th capturing group. It reported this as NotImplemented, which surfaced as "this regex feature (backreferences / an unsupported construct) is not supported". That blocked emmy.complex, whose number pattern has 8 groups. Capture slots are now sized per program ((groups + 1) * 2). Each ThreadList keeps one contiguous i32 slab with a row per thread, `save` writes and then restores a scratch row instead of copying a fixed array, and a MatchResult owns its slot slice (MatchResult.deinit / .group). Every caller (re-find, re-matches, re-seq, re-groups, replace, split, String/Pattern .matches, Matcher) frees it; Matcher copies the slots into gc.infra and frees them on the next record, on reset and in the finaliser. Lookaround sub-VMs carry the parent's slot width, so lookahead captures past group 8 still thread through. The group count is bounded by compile.MAX_CAPTURE_GROUPS (1024) as a resource guard. CompileError gains TooManyGroups, which raises the catchable regex_too_many_groups error naming the limit, and Backreference, which still raises the uncatchable unsupported-feature error (AD-060) but now names the backreference. NotImplemented now covers only the remaining unsupported constructs.
Covers the Emmy complex-number pattern through re-matches, re-find, re-groups and the Matcher API (1+2i, -3.5e2 - 4I, 7), 12 groups through re-seq/nth, nested and optional groups (unmatched -> nil), $9 in replace, 1000 groups, the catchable too-many-groups error and the named, uncatchable backreference error.
…ns regex_parity_gaps.sh Per the suite-first rule (decision 20260911144411-22d9cd94): the value cases become suites.regex-many-groups-test; the uncatchable backreference raise is a process-boundary check, so its 'regex backreference' naming is pinned in the existing regex_parity_gaps.sh instead of a new e2e script.
9cbc147 turned every token after a script path into a script argument, which broke the documented ADR-0135 shape 'cljw app.clj -cp lib': phase16_wasm_require_component ran 'ns_classpath_require.clj -cp test/e2e/fixtures/wasm', the classpath silently fell back to cwd, and the bare-name component :require failed to find greet_component.wasm. A '-cp' / '--classpath <dirs>' placed directly after the script path is the classpath option again; the first other token starts the script args and everything from there on (a later -cp included) is an argument. The cli-args e2e covers both shapes, reads the first output line with 'sed -n 1p' instead of 'head -1' (check_epipe_head) and carries the e2e-process-boundary marker, since its subject is the binary's argv.
…e ledgers The Map$Entry remap test moves from a new bash e2e (which pushed the e2e script count over its baseline, check_e2e_count) to test/clj/suites/map_entry_interface_test.clj with the same cases, and its run_all step is dropped. data/placement.yaml is regenerated for the new IMapEntry/-key/-val vars, and data/doc_coverage.txt records them as undocumented like the other core protocols (IFn, ILookup, ...).
… e2e to suites Convert test/e2e/phase14_deftype_destructured_param_shadow.sh and test/e2e/phase14_qualified_core_special.sh into clojure.test suites under test/clj/suites/, keeping every assertion, and drop their run_all.sh steps. Restores the e2e script count to the gate baseline.
Merge swarm/defrecord-shadow (its e2e scripts are now suites) and convert test/e2e/phase14_macro_class_form.sh into test/clj/suites/macro_class_form_test.clj, keeping every assertion. The two cross-namespace cases run through load-string. Drop the run_all.sh step, restoring the e2e script count to the gate baseline.
…cripts to a suite
Strings are indexed by Unicode code point, not UTF-16 code unit, on both the language surface (count, subs, nth, seq) and the interop surface (.length, .charAt, .indexOf, .substring). Record it as an accepted divergence with rationale, add a porting section to docs/clojure_vs_clojurewasm.md, and pin the answers in test/clj/suites/string_code_point_test.clj. No runtime change. Closes #1
Adds the user-visible changes since v1.14.12 that had no entry: the java.lang.Number instance methods, wide ratio literals, RT/nextID, IRecord / Fn / Map$Entry interop, script args to *command-line-args*, large collection literals (D-346), the 1024-group regex cap, the doubleValue conversions, temporal hash parity, interface-driven predicates, CharSequence acceptance, macro class forms, and the smaller fixes from PRs #36-#40, #44, #48 and #52-#56.
New backward-compatible callable surface since v1.14.12 (java.lang.Number instance methods, ratio literals wider than a Long, clojure.lang.RT/nextID, clojure.lang.IRecord / clojure.lang.Fn as interfaces, deftype/reify over java.util.Map$Entry, script args to *command-line-args*) makes this a MINOR. build.zig.zon .version 1.15.0 has not shipped, so preflight releases the staging->main merge verbatim and does not stamp CHANGELOG; stamp [Unreleased] as [1.15.0] here, as 10f0b61 did for v1.14.0. Known: check_changelog_reachability --gate flags this commit until the v1.15.0 tag exists (the first heading after [Unreleased] is not v1.14.12's).
`check_changelog_reachability.sh --gate` required the first heading after [Unreleased] to be the newest tag's. A hand-written MINOR bump stamps its own `## [X.Y.0] - date` heading before the tag exists (release.yml's verbatim path does not stamp), so the bump commit failed the gate step `changelog_reachability`, locally and, once CI fetches tags, in CI too. The check now lets one heading sit above the newest tag's when build.zig.zon names that version, the version is newer than the newest tag, and no tag for it exists. Then the newest tag's heading must be the second. Any other heading there is still a violation. Checked in a scratch clone with the release tags: bump commit passes; heading 1.16.0 against zon 1.15.0 fails; a stray [1.15.0] heading with zon at 1.14.12 fails; the pre-bump tree passes. Smell-audited: 1: one bounded exception keyed on build.zig.zon, the version SSOT
fix: destructured method params shadow record fields; clojure.core-qualified defmacro/binding/ns
feat(interop): java.util.Map$Entry on deftype via IMapEntry; fix(cli): script args reach *command-line-args*
fix(analyzer): a macro may return a class object
feat(interop): clojure.core.logic loads (RT/nextID, static . on a class, IRecord, Fn)
fix(regex): size capture slots per program, lifting the 7-group cap
t300/cljw-ci-tags
docs(strings): record code-point string indexing as AD-076 (#1)
chore(release): changelog for rounds 5-6 and target v1.15.0
… model .claude/settings.json pinned "model": "claude-opus-4-8", which overrode the account default for every session in this repo.
chore(claude): drop the project model pin
@chaploud handed the clojurewasm organization to @BuddhiLW. A News section records it, says the fork's v1.11 through v1.15.0 work merges back into clojurewasm/ClojureWasm, and that releases and the Homebrew tap keep publishing from BuddhiLW/ClojureWasm for now. The credit paragraph records the handover too.
docs(readme): announce the maintainer handover
BuddhiLW
added a commit
that referenced
this pull request
Oct 16, 2026
Smell-audited: 1: the checker learns one qualified spelling instead of an allowlist of foreign ids, so the next foreign citation needs no script edit; the two pipe fixes apply the herestring shape the repo already uses. Three full-gate failures, all non-code, all pre-dating this session's source change on staging. The first two would have failed PR #16's CI. debt_id_refs: the per-call wasm write-up cites zwasm's own debt rows D-584 and D-585. A bare `D-NNN` is a cljw row to check_debt_id_refs.sh, so D-584 read as undefined, and D-585 passed only because cljw happens to own a D-585 of its own (the JIT zero-result gap, ADR-0192). The two ledgers share the id shape and already collide. zwasm's ids are now always written `zwasm/D-NNN`; the checker strips that qualified form before extracting ids, so a bare id stays a cljw citation and a qualified one is never mistaken for one. Retrofit: every zwasm citation in .dev/wasm_percall_findings.md, .dev/bench/ffi_boundary/README.md, .dev/handover.md, .dev/zwasm_capabilities.md and build.zig.zon. cljw's D-585 (engine.zig, ADR-0192, mem_kernel.wat) is untouched. epipe_head: bench/compare_langs.sh:504 fed `sed … /proc/cpuinfo | head -1` under pipefail (one line per core, so head closes the pipe early). Replaced by a single awk that prints the first match and exits; no pipe. e2e_phase16_wasm_memory: failed once in the full gate with the expected line visibly present in the output it printed, and passed on two direct re-runs. Its `want` helper was `echo "$out" | grep -qF` under `set -o pipefail`: grep -q exits on the first match and the producer's EPIPE fails the pipeline. Same class check_debt_id_refs.sh documents; same fix, `grep -qF "$1" <<<"$out"`, applied to all three pipes in the script.
BuddhiLW
added a commit
that referenced
this pull request
Oct 16, 2026
build.zig.zon's .version was set to 1.14.0 in 86006ea and has not shipped, so preflight releases the staging->main merge VERBATIM and does not stamp CHANGELOG (the stamp is part of the auto patch-bump commit only). Stamp [Unreleased] as [1.14.0] - 2026-09-05 here, the way v1.13.4 should have been: its notes stayed under [Unreleased] and are folded into this heading. Release PR: #16.
BuddhiLW
added a commit
that referenced
this pull request
Oct 16, 2026
…6 tracks zwasm/D-584 Smell-audited: 3: a new ADR with the mandatory fresh-context Devil's-advocate fork reflected verbatim (Alt B taken per F-002; Alt A = Smallest-diff bias, Alt C = Workaround smell). Decision: one module (runtime/concurrency/runtime_thread.zig) owns RUNTIME_STACK_BYTES, the SpawnConfig and STACK_BUDGET_BYTES for the entry hop AND the workers, with a comptime margin assertion replacing ADR-0157's prose '8 MiB main / 16 MiB worker'; main.zig runs cli.dispatch on a spawned runtime thread. zwasm/D-584 (16.5 us per JIT wasm/call on the initial thread on Linux) is the trigger, tracked as D-586 with an upstream barrier, not the reason. The engine default is kept out (CLJW-WASM-ENGINE-DEFAULT, cljw's own D-585 ranked above the residual 2.9x). Implementation follows on this branch behind a smoke; the branch merges into staging after v1.14.0 (PR #16) is cut.
BuddhiLW
added a commit
that referenced
this pull request
Oct 16, 2026
v1.14.0: string/walk/slurp parity fixes, bench tier rebuilt, wasm FFI measured, cljw-native suites
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fast-forward after the handover from @chaploud.