3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-03-24 21:38:36 +00:00
Commit graph

22033 commits

Author SHA1 Message Date
dependabot[bot]
c6dbe003ad
Bump actions/cache from 5.0.3 to 5.0.4 (#9112)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.3 to 5.0.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v5.0.3...v5.0.4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 16:33:01 -07:00
dependabot[bot]
ae90696e50
Bump actions/download-artifact from 8.0.0 to 8.0.1 (#9113)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v8...v8.0.1)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 16:32:49 -07:00
Nikolaj Bjorner
4258768d77 reduce number of benchmarks to 200
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-03-23 03:03:59 -07:00
Nikolaj Bjorner
19827a8599 remove stale aw files
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-03-22 20:04:18 -07:00
Copilot
a320a98489
ZIPT benchmark: scale to 500 files, fix seq timeout to 5s (#9099)
* Update ZIPT benchmark: 500 benchmarks, fix seq timeout description to 5s

Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/4df0700d-bb5b-4bd6-85a4-34ed56c8f40c

* Update qf-s-benchmark.md

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
Co-authored-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-03-22 19:58:23 -07:00
Copilot
40485e69be
Simplify extract_var_bound via operator normalization (#9062)
* Initial plan

* simplify extract_var_bound in qe_lite_tactic.cpp via operator normalization

Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>

* Add defensive check for integer type in lhs

Added a defensive check for integer type in lhs before proceeding with inequality checks.

* Update qe_lite_tactic.cpp

* Fix utility function call for integer check

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
Co-authored-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-03-22 16:01:12 -07:00
Copilot
acd5a4cd59
Update README.md status badges: remove disabled specbot/deeptest, add new active workflows (#9081)
Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/77922efd-7471-4264-bf14-5fe5e643618d

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-21 15:20:35 -07:00
Lev Nachmanson
c2a17dac0b Update .github/workflows/ci.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-21 06:16:46 -10:00
Lev Nachmanson
b8736991f3 Update .github/workflows/ci.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-21 06:16:46 -10:00
Lev Nachmanson
748fbd96f2 Use -j$(nproc) instead of -j3 in CI make builds
Replace hardcoded -j3 with -j$(nproc) in ci.yml, nightly.yml, and
release.yml to utilize all available cores on GitHub Actions runners.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-21 06:16:46 -10:00
Nikolaj Bjorner
ccdfdbb176 recompiled
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-03-20 16:32:44 -07:00
Nikolaj Bjorner
488c02711d updated workflows
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-03-20 16:32:25 -07:00
Copilot
8b7507c062
fix: forbid background ninja builds in ostrich-benchmark prompt to prevent OOM (exit 137) (#9069)
Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/919c0bc0-0f86-411e-aa7f-99ebf547eeb0

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-20 14:59:09 -07:00
Copilot
bb1c8ab230
Fix ostrich workflow OOM kill: use ninja -j1 to limit compilation memory (#9068)
Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/8c8cf73c-a94f-4bcf-b238-d35f8cdbb731

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-20 14:17:31 -07:00
Copilot
33dfce0507
Fix Ostrich Benchmark OOM kill: switch to Release build (#9066)
* Initial plan

* Fix OOM kill in Ostrich Benchmark workflow: use Release instead of Debug build

Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/3c146cc1-cdf8-4a25-90ad-31c366dbce40

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-20 13:02:41 -07:00
Copilot
4a8c9729bf
Add ostrich-benchmark agentic workflow for ZIPT/Z3 c3 branch benchmarking (#9064)
Agent-Logs-Url: https://github.com/Z3Prover/z3/sessions/bfaec259-86d9-4b56-ab04-182835e3563b

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-20 11:19:37 -07:00
Lev Nachmanson
fc94e3dcdf remove a not successful workflow
Signed-off-by: Lev Nachmanson <levnach@hotmail.com>
2026-03-20 07:39:20 -10:00
Mark DenHoed
43009600d4
Fix documentation for Z3_solver_to_dimacs_string (#9053)
Corrected the function name in the documentation comment.
2026-03-20 10:18:13 -07:00
Copilot
afe4bfcab2
chore: update RELEASE_NOTES.md for 4.17.0 per discussion #9023 (#9051)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-20 00:17:55 -07:00
Nikolaj Bjorner
1137d23725 fix bug reported in API coherence report
Signed-off-by: Nikolaj Bjorner <nbjorner@microsoft.com>
2026-03-19 23:20:55 -07:00
Lev Nachmanson
8cc75d444e fix box mode: reset bounds before each objective
update_lower_lex updates m_lower for subsequent objectives with saved
values from the current model. Reset m_lower[i] and m_upper[i] to
their initial values before optimizing each objective so earlier
objectives do not contaminate later ones.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 17:07:21 -10:00
Lev Nachmanson
fbbb582650 fix test: copy Z3_ast_to_string results before next call
Z3_ast_to_string returns a pointer to an internal buffer that is
overwritten on the next call. Store results in std::string immediately
to avoid reading a stale, garbled buffer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 17:07:21 -10:00
Lev Nachmanson
1c70b9e6ee fix box mode: isolate m_lower/m_upper between objectives
geometric_lex's update_lower_lex updates m_lower for all subsequent
objectives with saved values from the current model. In box mode this
contaminates later objectives' starting bounds, causing platform-dependent
results. Save and restore m_lower/m_upper across iterations so each
objective starts from a clean state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 17:07:21 -10:00
Lev Nachmanson
acd2e9475d fix #9030: box mode objectives are now optimized independently
In box mode (opt.priority=box), each objective should be optimized
independently. Previously, box() called geometric_opt() which optimizes
all objectives together using a shared disjunction of bounds. This caused
adding/removing an objective to change the optimal values of other
objectives.

Fix: Rewrite box() to optimize each objective in its own push/pop scope
using geometric_lex, ensuring complete isolation between objectives.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 17:07:21 -10:00
Lev Nachmanson
e351266ecb remove dead code in nlsat_explain
Signed-off-by: Lev Nachmanson <levnach@hotmail.com>
2026-03-19 12:02:21 -10:00
Copilot
cf6c8810ee
Update qf-s-benchmark to run twice daily at midnight and noon UTC (#9047)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-19 14:52:29 -07:00
Lev Nachmanson
996dc72300 Fix assertion violation in isolate_roots for nested calls (#6871)
resultant vanishes during a nested isolate_roots call. The mathematical
invariant that the resultant cannot vanish again after recovery does not
hold in all cases, e.g. with certain nonlinear real arithmetic formulas.

The algebraic_exception propagates cleanly through the nlsat solver and
tactic layers which already catch z3_exception.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 11:30:49 -10:00
Lev Nachmanson
47cbc746b5 fix #9036: expand bounded integer quantifiers in qe-light
After qe-light's equation solver (eq_der) eliminates variables from
linear equations, remaining bounded integer quantifiers may still have
non-unit coefficients that prevent Fourier-Motzkin elimination.

Add a bounded quantifier expansion step: when the remaining quantified
integer variables all have explicit finite bounds and the product of
domain sizes is <= 10000, expand the quantifier into a finite
disjunction. This turns e.g. exists y0 in [0,10), y1 in [0,15): P(x,y0,y1)
into P(x,0,0) | P(x,0,1) | ... | P(x,9,14), which is 150 disjuncts.

The SMT solver handles the resulting quantifier-free formula instantly,
whereas the previous QSAT/MBP approach timed out due to weak integer
projections from the (|a|-1)*(|b|-1) slack in Fourier-Motzkin resolution
with non-unit coefficients.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 08:29:35 -10:00
Lev Nachmanson
20bcf67155 Print full child output for all tests in parallel mode
Always print each test's captured output, not just for failures.
This preserves backward compatibility:
- PASS appears on its own line per test, as before
- ASAN/UBSAN reports from any test appear in captured logs
- timeit output is preserved for all tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 06:37:22 -10:00
Lev Nachmanson
04d2e66aab Make parallel execution the default for test-z3
Parallel mode (/j) is now the default. Use /seq to force serial execution.
Child processes are invoked with /seq to prevent recursive parallelism.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 06:37:22 -10:00
Lev Nachmanson
9fea91d0eb Add parallel test execution to test-z3 (/j flag)
Refactor src/test/main.cpp to support parallel test execution:
- Add /j[:N] flag to run tests in parallel using N jobs (default: number of cores)
- Use process-based parallelism: each test runs as a child process,
  avoiding thread-safety issues with global state like enable_debug/enable_trace
- Output is captured per-test and printed atomically, so different tests never mix
- Provide summary with pass/fail counts, wall time, and failed test names
- Refactor test list into X-macros for single source of truth
- Fix pre-existing bug where serial /a mode ran each test argc times

Platform support:
- Unix (Linux/macOS/FreeBSD): popen/pclose with WEXITSTATUS
- Windows: _popen/_pclose
- Emscripten: parallel disabled (no threading support)
- Works with both SINGLE_THREAD and multi-threaded builds

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 06:37:22 -10:00
Lev Nachmanson
b5bf4be87e fix: move m_fixed insertion after check_long_strings guard
m_fixed.insert(e) was placed before the check_long_strings guard,
causing check_fixed_length(false, false) to mark variables with
len > 20 as processed without actually decomposing them. The
subsequent check_fixed_length(false, true) then skipped them.

Move the insertion after the guard so variables are only marked
as fixed once they are actually decomposed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-17 20:19:12 -10:00
copilot-swe-agent[bot]
960ab8e67a perf: move check_fixed_length(false,true) before check_contains in final_check_eh
Co-authored-by: levnach <5377127+levnach@users.noreply.github.com>
2026-03-17 20:19:12 -10:00
Lev Nachmanson
09c13a75e3 fix #8023: don't skip axiom clauses with non-base-level satisfying literals
The add_axiom optimization that skips adding clauses when a literal is
already true was unsound: the satisfying literal could be retracted by
backtracking, leaving the axiom clause missing. This caused the solver
to miss propagations, e.g., not propagating indexof(a,s) = -1 when
contains(a,s) becomes false after backtracking.

Fix: only skip the clause if the satisfying literal is assigned at
base level (scope 0), where it can never be retracted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-17 15:05:41 -10:00
copilot-swe-agent[bot]
3745bdd43b fix: reduce verbose lock contention in theory_diff_logic (issue #8019)
In multi-threaded solving, IF_VERBOSE(0, ...) in found_non_diff_logic_expr
was always acquiring the global g_verbose_mux mutex (since verbosity >= 0 is
always true) while holding it for potentially expensive mk_pp() calls. This
caused catastrophic lock contention when multiple threads internalized atoms.

Change IF_VERBOSE(0, ...) to IF_VERBOSE(2, ...) in both theory_diff_logic_def.h
and theory_dense_diff_logic_def.h. The diagnostic message is still available at
verbosity level 2 (-v:2), but is no longer printed (or locked) at the default
verbosity level, eliminating the contention.

Co-authored-by: levnach <5377127+levnach@users.noreply.github.com>
2026-03-17 13:07:20 -10:00
Lev Nachmanson
0461e010bb assign every new issue to copilot by default
Signed-off-by: Lev Nachmanson <levnach@hotmail.com>
2026-03-17 12:09:01 -10:00
dependabot[bot]
a252121945
Bump actions/download-artifact from 8.0.0 to 8.0.1 (#9016)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v8...v8.0.1)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 16:11:55 -07:00
dependabot[bot]
103bf6dc35
Bump github/gh-aw from 0.53.4 to 0.59.0 (#9015)
Bumps [github/gh-aw](https://github.com/github/gh-aw) from 0.53.4 to 0.59.0.
- [Release notes](https://github.com/github/gh-aw/releases)
- [Commits](https://github.com/github/gh-aw/compare/v0.53.4...v0.59.0)

---
updated-dependencies:
- dependency-name: github/gh-aw
  dependency-version: 0.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 16:07:29 -07:00
dependabot[bot]
9d033f304a
Bump actions/setup-python from 5 to 6 (#9017)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 15:52:45 -07:00
dependabot[bot]
0564dfe32b
Bump actions/checkout from 5.0.1 to 6.0.2 (#9018)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.1 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v5.0.1...v6.0.2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 15:52:35 -07:00
Lev Nachmanson
f4adcde585 add regression test for #9012: box mode mod optimization
Test tst_box_mod_opt verifies that maximize (mod (- (* 232 a)) 256)
returns 248 when using box priority with multiple objectives.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 09:47:09 -10:00
Lev Nachmanson
f03cac6e51 fix #9012: incorrect optimization of mod in box mode
Push/pop isolation in maximize_objectives1 (added for #7677) can corrupt
LP column values between objectives. For non-linear objectives like mod,
the LP maximize call may return stale values after a preceding
objective's push/pop cycle.

Fix: save the baseline model before the push/pop loop and use it as a
floor for each objective's value. Extract two helpers:
- maximize_objective_isolated: push/pop + save/restore per objective
- update_from_baseline_model: adopt baseline model value when it is better

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 07:28:34 -10:00
Copilot
fe6efef808
Add monthly Academic Citation & Research Trend Tracker workflow (#9007)
* Initial plan

* Add academic-citation-tracker workflow and compiled lock file

Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-15 15:39:37 -07:00
Lev Nachmanson
99099255b6 Fix inconsistent optimization with scaled objectives (#8998)
When the LP optimizer returns the same blocker expression in successive
iterations of geometric_lex (e.g., due to nonlinear constraints like
mod/to_int preventing the LP from exploring the full feasible region),
the loop now falls back to using the model-based lower bound to push
harder instead of breaking immediately.

This fixes the case where minimize(3*a) incorrectly returned -162
while minimize(a) correctly returned -infinity with the same constraints.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-15 11:12:21 -10:00
Lev Nachmanson
5df80705aa Fix inconsistent optimization with scaled objectives (#8998)
When the LP optimizer returns the same blocker expression in successive
iterations of geometric_lex (e.g., due to nonlinear constraints like
mod/to_int preventing the LP from exploring the full feasible region),
the loop now falls back to using the model-based lower bound to push
harder instead of breaking immediately.

This fixes the case where minimize(3*a) incorrectly returned -162
while minimize(a) correctly returned -infinity with the same constraints.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-15 11:12:21 -10:00
Copilot
fbeb4b22eb
Add missing Go Goal/FuncEntry/Model APIs and TypeScript Seq higher-order operations (#9006)
* Initial plan

* fix: add missing API bindings from discussion #8992 for Go and TypeScript

- Go tactic.go: add Goal.Depth(), Goal.Precision(), Goal.Translate(), Goal.ConvertModel()
- Go solver.go: add FuncEntry struct, FuncInterp.GetEntry/SetElse/AddEntry, Model.HasInterp
- TypeScript types.ts + high-level.ts: add Seq.map/mapi/foldl/foldli

Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-15 12:57:44 -07:00
Copilot
6893674392
fix: correct misleading API comments in fp.go and JavaExample.java (#9003)
* Initial plan

* fix: correct misleading API comments in fp.go and JavaExample.java

Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-15 12:08:59 -07:00
Angelica Moreira
bebad7da50
Add numeral extraction helpers to Java API (#8978)
New methods:
- Expr.getNumeralDouble(): retrieve any numeral as a double
- IntNum.getUint(): extract numeral as unsigned 32-bit value
- IntNum.getUint64(): extract numeral as unsigned 64-bit value
- RatNum.getSmall(): numerator/denominator as int64 pair
- RatNum.getRationalInt64(): numerator/denominator (returns null on overflow)

Each is a thin wrapper around the existing Native binding.
Added examples to JavaExample.java covering all new methods.
2026-03-15 10:36:17 -07:00
Copilot
9256dd66e6
Switch memory-safety workflow from push to weekly Monday schedule (#9001)
* Initial plan

* Replace push trigger with weekly Monday schedule in memory-safety.yml

Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NikolajBjorner <3085284+NikolajBjorner@users.noreply.github.com>
2026-03-15 10:26:10 -07:00
Angelica Moreira
db46d52056
fix memory-safety-report to download artifacts via MCP tools (#8979)
gh CLI is not available inside AWF so the agent could not download
artifacts. Switch to GitHub MCP actions toolset for artifact URLs
and add helper scripts for download and parsing.
2026-03-15 10:12:49 -07:00