I picked ljharb/qs from the official Port Mortem 2026 Track H pool because its size was deceptive. Roughly a thousand lines of query-string code looked manageable in 72 hours. Its observable behavior was not.
A query such as a[0]=x&a[2]=y is not merely text becoming a map. It can encode nested objects, arrays, sparse holes, duplicate keys, prototype-sensitive names, configurable depth, and JavaScript's property-ordering rules. Moving that behavior to Go meant translating a runtime model, not syntax.
The result is qs-go: a standard-library Go module, standalone CLI, and no-install Go/WebAssembly demo. The release path contains no Node.js, subprocess fallback, cgo, or second parser hidden in the demo.
Try it first: live Go/WASM demo · 4:40 evidence video · submission release
Here is the result in one deliberately qualified paragraph. The frozen upstream JavaScript baseline passes 1,045 / 1,045 Tape assertions. Those assertions did not all run directly against Go. The Go equivalence claim is supported separately by focused Go tests, a hash-verified JavaScript oracle, and a final deterministic differential record of 672,321 comparisons in 60 seconds, with zero observed mismatches and zero execution errors inside a declared dense, JSON-compatible scope.
That distinction is the thesis of this project: a large number is useful only when its provenance and boundary are equally visible.
The evidence contract
Before discussing implementation, this is what each headline claim does and does not prove.
| Claim | Published evidence | Boundary |
|---|---|---|
| The original snapshot is healthy | Frozen JavaScript run: 1,045 / 1,045 assertions | Baseline only; not 1,045 assertions executed against Go |
| The port builds and runs independently | One-command build, clean-archive rebuild, Linux/Windows CI, native CLI, Go/WASM demo | The browser demo exposes parse and normalize, not every library option |
| Dense values matched the oracle | 672,321 deterministic parse/stringify comparisons; zero observed differences or errors | JSON-compatible dense trees and supported serializable options, not callbacks, cycles, or sparse holes |
| Sparse behavior exists in the Go model | Explicit Element.Present representation and Go regression tests |
Ordinary JSON cannot transport a JavaScript array hole faithfully |
| Performance was measured comparatively | Shared workloads, 40 retained raw samples, p99, startup, and externally sampled Working Set | One sequential Windows session; not a universal performance claim |
This table prevented several tempting but false sentences from entering the README.
A query string is not map[string]any
The shortest Go API would have been map[string]any. It also would have erased the distinctions I most needed to preserve: object versus array, a sparse hole versus explicit undefined, deterministic property order, and type-specific stringify behavior.
Instead, the public API uses a closed value algebra. Its state is private, and callers construct values through typed constructors:
type Member struct {
Key string
Value Value
}
type Element struct {
Present bool // false means a sparse hole
Value Value
}
type Value struct {
kind Kind
objectValue []Member
objectIndex map[string]int
arrayValue []Element
// private scalar fields omitted
}
There is no any or interface{} escape hatch in the public Go value model. Ordered members provide stable enumeration while an internal index preserves efficient lookup. Sparse elements distinguish a missing slot from a present undefined or null value.
The parser also uses a balanced bracket scanner instead of one convenient regular expression. Nested bracket segments and unclosed suffixes have observable upstream behavior; a flat split is shorter, but it is not the same parser. Compatibility thresholds such as arrayLimit remain separate from resource budgets such as input bytes, nodes, and nesting depth.
This design costs more code than a map. It pays that cost once, at the language boundary, instead of rediscovering ambiguity throughout the parser and stringifier.
Freeze the oracle before asking it questions
Differential testing can create false confidence when the reference moves underneath it. My oracle refuses to start unless it verifies:
- upstream commit
3a890d4ecd3deb72a45d90be36f4f8c5970467c7; - test-tree identity
bef346f180a38793ec6d47e11f25f88a7eb579ca; - source-tree identity;
- four recorded original-test SHA-256 values.
The comparison path is intentionally boring:
deterministic case + supported options
|
+-------+-------+
| |
frozen JS oracle Go implementation
| |
+-------+-------+
|
structural result comparison
|
mismatch log + final report
Node.js exists only on the left side of that development pipeline. The library, native CLI, and WebAssembly build do not invoke it.
The first oracle wire uses versioned NDJSON and intentionally accepts only dense JSON-compatible trees. Plain JSON cannot preserve sparse holes, undefined, negative zero, invalid UTF-16 surrogates, callbacks, cycles, or reference identity. I could have invented encodings and then advertised a larger number, but that would have mixed transport design with implementation proof. I kept the scope smaller, versioned it, and tested sparse semantics directly in Go.
The scope statement is not fine print. It is part of the result.
The fuzzer found a JavaScript rule I had modeled incorrectly
The most valuable counterexample was only 23 characters:
a[3]=4zf&a[1]=ui_ir
with:
{"arrayLimit": 2}
Because index 3 crosses that representation threshold, qs materializes object properties instead of a dense array. My first Go implementation preserved arrival order: 3, then 1.
JavaScript exposes a different order. Under OrdinaryOwnPropertyKeys, array-index keys are returned numerically before ordinary string keys. The observable upstream result is therefore conceptually:
{"1":"ui_ir","3":"4zf"}
The correct fix was not "sort every key." That would destroy insertion order for ordinary properties. The Go object constructor now classifies canonical unsigned 32-bit property indices below 4294967295, sorts only that class numerically, and retains insertion order for everything else. Keys such as 01 and 4294967295 remain ordinary strings. A focused regression test locks the counterexample.
This was a bug in my developing port, not a latent bug in upstream qs, so I did not claim the separate Bug Catcher prize.
Then the validator failed its own test
The quieter discovery mattered even more.
The differential runner alternated parse and stringify using one global case number. Parse received global indices 0, 2, 4, ...; stringify received 1, 3, 5, .... The generators contained 32 parse templates and 24 stringify templates.
Modulo an even schedule length, repeatedly adding two can visit only half the residues:
gcd(2, 32) = 2 -> 16 of 32 parse templates reachable
gcd(2, 24) = 2 -> 12 of 24 stringify templates reachable
The comparison count kept rising. The output stayed green. Half the scheduled families were still unreachable.
I replaced the shared counter with operation-local indices, then added runner-level tests that enumerate the executed schedule and prove all 32 / 32 parse and 24 / 24 stringify templates are reachable and distinct. The earlier 564,651-case run was marked superseded and excluded from final evidence.
The replacement run used seed 0x5153474f for exactly 60,000 ms:
- 336,161 parse comparisons;
- 336,160 stringify comparisons;
- 672,321 total comparisons;
- zero observed mismatches;
- zero Go errors;
- zero oracle errors.
This is why I now treat a fuzz harness as production code. A green validator can be wrong about its own coverage while being perfectly correct about every case it actually executes.
The benchmark refused to become a victory slide
The comparative benchmark runs Node and Go sequentially on the same host and shared flat and nested workloads. It retains all 40 latency samples, all 40 cold-start samples, environment metadata, source identity, and externally sampled Working Set observations. With the recorded percentile rule and 40 samples, p99 is the maximum; the report says so.
The throughput story split cleanly by direction:
| Workload | Go median vs Node | Go p99 vs Node |
|---|---|---|
| Parse 100 flat pairs | 20.0% slower | 36.8% slower |
| Parse 20 nested values | 11.6% slower | 61.2% slower |
| Stringify 100 flat pairs | 40.3% faster | 36.6% faster |
| Stringify 20 nested values | 60.7% faster | 56.1% faster |
Process-level measurements were also mixed:
| Measurement | Node | Go | Interpretation |
|---|---|---|---|
| Median cold start | 73.2933 ms | 14.9117 ms | Go about 4.9x faster |
| p99 cold start | 79.6998 ms | 384.8232 ms | Go worse because one retained first-start sample is the maximum |
| Polled peak Working Set | 65.9102 MiB | 35.8477 MiB | Go 45.6% lower in this run |
The 384.8232 ms Go observation was inconvenient. It would have been easy to call it warm-up noise, delete the run, and rerun until p99 looked attractive. I kept it.
The defensible conclusion is narrower: this port has a strong median startup, memory, and stringify story on the recorded host; its parser requires optimization before anyone should claim general performance superiority. The complete benchmark evidence archive includes the raw samples, scripts, hashes, host snapshots, and correctness outputs.
The live demo reports per-run browser timing for feedback. Those numbers are deliberately excluded from the cross-runtime benchmark claim.
The decision I would take back
I would not let one global case index serve as both an operation selector and a generator selector. It looked deterministic and economical; mathematically, it coupled two independent schedules and hid half the corpus. Separate counters should have existed from the first version, together with reachability assertions.
I would also define the cross-language value model and transport boundary before touching parser code. Once I stopped treating JSON as a transparent carrier, decisions about sparse arrays, undefined, negative zero, and ordering became much easier to explain and test.
Finally, I would design benchmark retention before the first benchmark. The initial generation kept aggregates but not every observation. Those samples cannot be reconstructed honestly. The v2 runner therefore writes a new immutable evidence directory instead of overwriting history.
A five-minute verification path
The fastest inspection starts in the live demo. GitHub Actions compiles the existing cmd/qsgo entry point to WebAssembly; the page does not contain an alternate JavaScript parser. Run both parse and normalize, then reproduce the native checks from a fresh clone:
go test ./... -count=1
go vet ./...
go build -trimpath ./...
go test . -cover -count=1
The expected root-package statement coverage is 81.5%. Then inspect three receipts:
-
fuzz/report.jsonandfuzz/log.txtfor the final differential record and counterexample; -
testdata/oracle/oracle_manifest.jsonfor the frozen source and test identity; -
DECISIONS.mdand theEvaluation Guidefor architectural tradeoffs and claim boundaries.
One check remains unavailable in the recorded Windows environment: the portable Go toolchain had cgo disabled, so the race detector could not run. This library is not concurrency-heavy, but unavailable is not the same word as passed.
The most important artifact is not the zero in the mismatch column. It is the chain that makes the zero interpretable:
- freeze the source identity;
- establish the original baseline;
- define the equivalence boundary;
- test the port;
- test the validator;
- publish the counterexample and regression;
- retain the benchmark result that hurts the story;
- state what remains unproved.
AI has made cross-language ports cheap to generate. Evidence is still expensive. That is exactly why it is the part worth engineering.
Built by Parity Forge for Port Mortem 2026, organized by Hackathon Raptors, Track H — Open Pair: JavaScript to Go. The official pool lists ljharb/qs under both the JavaScript-runtime track and the Open Pair pool; this entry uses Track H and documents the migration rationale in its README.
Repository: https://github.com/agentic-build-lab/qs-go
Release: https://github.com/agentic-build-lab/qs-go/releases/tag/port-mortem-2026-submission
Live demo: https://agentic-build-lab.github.io/qs-go/
Evidence video: https://drive.google.com/file/d/1dL5DhhTMIiO67chlSwGq0GieninJYw9c/view?usp=sharing


Top comments (0)