Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
955b005
docs: record why the fit is done in the sensor's natural units
claude Aug 10, 2026
9d9db66
docs: the velocity peak sits at fc for the whole omega-squared family
claude Aug 10, 2026
de1a6b4
feat(fitting): warn when the fc guess is read off the wrong motion
claude Aug 10, 2026
a271b12
refactor(fitting): split the 745-line module into a package
claude Aug 10, 2026
30acf67
test(golden): measure the cwt residual, and tighten 5e-2 to 1e-3
claude Aug 10, 2026
97c001e
docs: a Sphinx site — skeleton, API reference, and the Pages workflow
claude Aug 10, 2026
87cd678
docs: build the window-position note into the site
claude Aug 10, 2026
47fe5e4
feat(release): release-please, and PyPI publishing in the same workflow
claude Aug 10, 2026
94ae4b5
docs: a release runbook, a roadmap, and say plainly that this is alpha
claude Aug 10, 2026
9df20c7
docs(plan): close out 2b, and correct what building it disproved
claude Aug 10, 2026
2b31802
Create docs.yml
sgjholt Aug 11, 2026
7993755
Update docs.yml
sgjholt Aug 11, 2026
1e45576
test(golden): the cwt residual is the machine, and 2e-2 bounds it
claude Aug 11, 2026
dd96808
test: stop two warnings leaking into pytest's summary
claude Aug 11, 2026
88370a0
build: quiet the floors job, and take docs.yml's venv fix into ci/
claude Aug 11, 2026
a047218
build: install both hook types, and write the rules agents keep breaking
claude Aug 13, 2026
aee252d
docs: a developer guide, and one for the documentation itself
claude Aug 13, 2026
839d1b4
docs: say what versioned docs would actually take
claude Aug 13, 2026
1548017
docs: publish through Read the Docs instead of GitHub Pages
claude Aug 13, 2026
b6cd31d
docs: derive the copyright year from the build
claude Aug 13, 2026
72fc3a9
feat(api): a stable import surface for downstream packages
claude Aug 17, 2026
c151425
fix(fitting): stop freezing configuration at import, and stop printing
claude Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: docs

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

# Pages needs these; the default token is read-only for contents.
permissions:
contents: read
pages: write
id-token: write

# A second push while a deploy is in flight should win, not queue behind it.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # hatch-vcs derives the version from tags
- uses: astral-sh/setup-uv@v5
- run: uv python install 3.11
- run: uv venv # create a venv to install into

# The package itself, not just the docs extra: autodoc imports every
# module it documents, so a docs build is also an import check.
- run: uv pip install -e ".[docs,io]"

- name: Build
# No -W. Intersphinx resolves seven inventories over the network and
# warns when one is briefly unreachable, which would turn a third
# party's downtime into a red build. A genuinely broken build exits
# non-zero on its own.
run: uv run sphinx-build -b html docs docs/_build/html

- uses: actions/upload-pages-artifact@v3
with:
path: docs/_build/html

deploy:
# Only main deploys. A pull request builds, which is what catches a broken
# reference before it lands, and stops there.
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# `pre-commit install` wires only the pre-commit stage by default, which left
# the commit-msg hook below inert in every clone that ran the documented setup
# — including the one that then committed three session links. Naming the
# stages here means one `pre-commit install` installs both.
default_install_hook_types: [pre-commit, commit-msg]

repos:
# CI runs `uvx ruff` unpinned, so it always gets the newest release. Keep
# this rev at that newest release or CI will fail on rules this does not have.
Expand Down
47 changes: 47 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Read the Docs build configuration.
#
# Read the Docs publishes the documentation; GitHub Actions only checks that it
# builds. The reason for the split is versions: this package is alpha and users
# are told to pin an exact version, so the documentation for a release has to
# stay readable after the trunk has moved on. Read the Docs keeps one build per
# tag, a `stable` pointing at the newest, and a `latest` from `main`, with a
# version switcher between them — and it builds pull requests to their own
# throwaway URL.
#
# Schema: https://docs.readthedocs.com/platform/stable/config-file/v2.html

version: 2

build:
os: ubuntu-24.04
tools:
python: "3.11" # the version CI's lint, typecheck and docs jobs use
jobs:
post_checkout:
# Read the Docs clones shallow and without tags, to save time. hatch-vcs
# derives the version from `git describe`, and `docs/conf.py` reads it
# back through importlib.metadata — so without these the sidebar reads
# the fallback 0.0.0 on every build, including tagged ones.
# `|| true` because a repository that is already complete makes
# --unshallow exit non-zero, which would fail the build.
- git fetch --unshallow || true
- git fetch --tags || true

sphinx:
configuration: docs/conf.py
# Deliberately off, for the same reason the CI job has no `-W`: intersphinx
# resolves seven inventories over the network and warns whenever one of them
# is briefly unreachable. That would make a third party's downtime a failed
# documentation build. A genuinely broken build still exits non-zero.
fail_on_warning: false

python:
install:
- method: pip
path: .
extra_requirements:
# `io` alongside `docs` because autodoc imports every module it
# documents, and `specmod.io` imports h5py and pyarrow. Without it the
# API reference loses those pages to import errors.
- docs
- io
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.1"
}
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Working on SpecMod with a coding agent

Rules for Claude Code, Codex, and any other agent committing to this
repository. `CLAUDE.md` points here so there is one copy.

The long version of everything below is
[`docs/development.md`](docs/development.md). This file is the part an agent
must not get wrong, and every entry is here because it has actually gone wrong.

## Before the first commit

```sh
uv venv && uv pip install -e ".[dev]"
pre-commit install # installs BOTH the pre-commit and commit-msg hooks
```

`pre-commit install` is not optional. In a fresh container it is easy to skip,
and the `commit-msg` hook is the only thing enforcing the rule below.

## Never publish session links

**No `Claude-Session:` trailers, session URLs, or agent-console links** in
commit messages, PR titles, PR bodies, code comments, or anything else that
lands in the repository. It is public; those links are private state.

`Co-Authored-By:` is fine. If your harness appends a session trailer by
default, strip it — the repository's rule wins over the harness default. The
`commit-msg` hook rejects it, which is why installing the hooks comes first.

## Commit messages are load-bearing

[Conventional Commits](https://www.conventionalcommits.org). They are not a
style preference: `release-please` reads them to compute the version bump and
to generate `CHANGELOG.md`. See
[`docs/releasing.md`](docs/releasing.md).

- `feat:` minor, `fix:` patch, `refactor:` / `docs:` / `build:` appear in the
changelog, `test:` / `ci:` / `chore:` are hidden.
- `!` or a `BREAKING CHANGE:` footer bumps the minor while the project is
`0.x`, not the major.
- Say *why*, with the measurement if there was one. The history is the record
of what was checked; a message that only restates the diff wastes it.

## You cannot push workflow files

A GitHub App token has no `workflows` permission, so any push touching
`.github/workflows/` is rejected outright. Write the intended file to
`ci/workflows/<name>.yml` instead and say in the PR that it needs copying
across. `tools/check_ci_mirror.py` runs in the `lint` job and fails until the
copy is made — that failure is the reminder, not a fault.
See [`ci/README.md`](ci/README.md).

## Verify before reporting

Run these, and report what they actually printed:

```sh
pytest -m "not dataset and not notebook" # the suite CI runs
pytest --without-optional-extras # what a default install sees
ruff check src/ tests/ tools/ && ruff format --check src/ tests/ tools/
mypy
python tools/check_ci_mirror.py
sphinx-build -b html docs docs/_build/html # if docs/ changed
```

`--without-optional-extras` matters: a development environment with
`specmod[multitaper]` installed passes tests that CI fails.

## Things that look like noise and are not

- **Golden references.** `tests/golden/*.json` is a record of numbers this code
used to produce. Do not regenerate it to make a test pass. If a change moves
a number, that is the finding — say which number, by how much, and why, and
regenerate deliberately with `python tools/make_golden.py`.
- **Measured tables in the docs.** Numbers in `docs/*.md` are generated between
markers by `python tools/measure_docs.py`. Edit the tool, not the table.
- **Tolerances.** Several carry a comment explaining what was measured to
choose them. Widening one to get to green, without measuring, is the specific
failure `docs/REFACTOR_PLAN.md` §6.6 exists to catch.

## Say what you did not check

The plan's §6.6 is an audit of claims in this repository that turned out to
describe mechanisms nobody had built. Do not add to it. If something is
untested, unreproducible, or assumed, write that down next to the claim — a
bound with a number behind it beats a confident sentence.
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CLAUDE.md

See [`AGENTS.md`](AGENTS.md) — one copy of the rules, for every agent.

The long version is [`docs/development.md`](docs/development.md).
90 changes: 90 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Contributing to SpecMod

The working guide is [`docs/development.md`](docs/development.md) — setup, the
daily loop, the tooling, the CI jobs, and how development relates to releases.
If you are an AI coding agent, read [`AGENTS.md`](AGENTS.md) first.

This file covers the one thing that is a promise rather than a practice.

## The stability promise, and its exact scope

SpecMod is `0.x` and says so loudly: names and signatures move between minor
releases without a deprecation cycle, because the API is still being worked
out and shipping shims for names that are about to move again costs more than
it protects.

**`specmod.api` is the exception.** It exists so that downstream packages have
something that does not move while the internals do, and it is the only part of
SpecMod that carries a compatibility guarantee.

| | Everything else | `specmod.api` |
|---|---|---|
| Rename or remove | any minor release, no notice | one minor release of `DeprecationWarning` first |
| Change a signature | any minor release, no notice | one minor release of `DeprecationWarning` first |
| Add | freely | freely, but see below |
| Behaviour change that moves a number | called out in the changelog | called out in the changelog **and** in the warning |

"One minor release" means: if a removal is decided during `0.4.x`, the warning
ships in `0.5.0` and the removal is `0.6.0` at the earliest. A deprecation that
has not been through a release has not been announced.

### Deprecating something in `specmod.api`

```python
warnings.warn(
"specmod.api.old_name is deprecated and will be removed in 0.6.0; "
"use specmod.api.new_name, which takes the same arguments.",
DeprecationWarning,
stacklevel=2,
)
```

Three things that make the difference between a warning people act on and one
they filter out:

- **Name the replacement**, or say plainly that there is none.
- **Name the release it goes in**, not "a future version".
- **`stacklevel=2`**, so the warning points at the caller's line rather than at
SpecMod's.

Keep the old name working for the whole cycle. A `DeprecationWarning` on
something that already raises is not a deprecation, it is a breakage with a
note attached.

### Adding to `specmod.api`

Every export is a compatibility obligation, so the surface is deliberately
small and does not grow opportunistically. To add one:

1. Have a caller that needs it. "Studio might want this" is not one.
2. Make it satisfy the five properties in the module docstring — path-free,
deterministic, non-mutating, quiet, typed errors. If the underlying
function does not, the wrapper is where that gets fixed, not the caller.
3. Add it to `EXPECTED_EXPORTS` in `tests/test_api_surface.py`. The list is
duplicated there on purpose, so an addition shows up as a diff in review
rather than as a passing test.
4. Give it a docstring with `Parameters`, `Returns` and `Raises`, and a type
annotation on everything. Both are tested.

**Do not reach around the surface.** If a downstream package needs something
`specmod.api` does not export, extend `specmod.api` in its own pull request
with the reason stated. An import of `specmod.core` or `specmod.fitting` from
a downstream package is a bug in the boundary, not a shortcut.

### What is *not* promised

- **Internals.** `specmod.core`, `specmod.fitting`, `specmod.transforms`,
`specmod.picks`, `specmod.config` and everything else may change in any
release. They are documented because SpecMod's own users read them; that is
not a stability claim.
- **Numerical output.** The promise is about names and signatures. A bug fix
that moves a number is still a bug fix, and it will move it — that is what
the golden references and the changelog are for. If you depend on exact
values, pin an exact version and keep the config hash.
- **The objects the surface returns**, beyond the attributes its docstrings
name. `SpectrumPair` gaining a field is not a breaking change.

## Everything else

Conventional Commits, `pre-commit install` before your first commit, and the
rest of the mechanics are in [`docs/development.md`](docs/development.md).
46 changes: 40 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ SpecMod estimates source parameters — long-period spectral level Ω, corner
frequency `f_c`, and the attenuation operator `t*` — by fitting a Brune-type
source model to direct-phase spectra.

> **Status: under active reconstruction.**
> The package is mid-refactor. The modern layers (`specmod.config`,
> `specmod.core`, `specmod.transforms`) are built and tested; the older
> pipeline modules still carry pre-refactor behaviour and are being replaced
> stage by stage. Expect breaking changes at every `0.x` release until the API
> settles at 1.0. See [`docs/REFACTOR_PLAN.md`](docs/REFACTOR_PLAN.md).
> **Status: alpha, and under active reconstruction.**
> The package is pre-1.0 and mid-refactor. The modern layers
> (`specmod.config`, `specmod.core`, `specmod.transforms`, `specmod.picks`,
> `specmod.fitting`) are built and tested; the older pipeline modules still
> carry pre-refactor behaviour and are being replaced stage by stage. Expect
> breaking changes at every `0.x` release until the API settles at 1.0 — they
> land in minor bumps by design, with no deprecation cycle. Pin an exact
> version for anything you intend to publish.
> [`docs/roadmap.md`](docs/roadmap.md) says which stages are done and what 1.0
> will mean; [`docs/REFACTOR_PLAN.md`](docs/REFACTOR_PLAN.md) is the working
> document behind it.

## Installation

Expand Down Expand Up @@ -141,17 +146,37 @@ Every output records the configuration that produced it, a hash of it, and the
SpecMod version, so a locally-overridden run is still reproducible from its
outputs.

## Documentation

The full documentation — the pipeline with its equations, the estimator
comparison, pick formats, and an API reference — builds with Sphinx:

```bash
uv pip install -e '.[docs]'
sphinx-build -b html docs docs/_build/html
```

`docs/REFACTOR_PLAN.md` is excluded from the built site on purpose: it is a
working document that records decisions and the measurements behind them, not
documentation for using the package.

## Development

```sh
uv venv && uv pip install -e ".[dev]"
pre-commit install # both hook types; not optional
pytest # test suite
pytest --without-optional-extras # as a default install and CI see it
ruff check src/ tests/ tools/ # lint
ruff format src/ tests/ tools/
mypy # strict on the rewritten modules
```

[`docs/development.md`](docs/development.md) is the full guide — the repository
mapped, every tool and CI check, the branch and commit conventions, and where
development stops and releasing begins. [`AGENTS.md`](AGENTS.md) is the short
version that binds AI coding sessions.

Run `--without-optional-extras` before pushing. A development environment
with `specmod[multitaper]` installed will pass tests that a default install
fails, and CI installs only `[dev]`.
Expand All @@ -178,6 +203,15 @@ published number fails the suite rather than quietly leaving the prose wrong.
Measurements that read `tutorial/data/events/` are slower and opt-in via `--field`;
refresh those by hand after changing an estimator.

### Releasing

Commit messages follow [Conventional Commits](https://www.conventionalcommits.org),
which is what makes the changelog and the version automatic: `release-please`
opens a standing release pull request, and merging it creates the tag, the
GitHub Release, the PyPI upload and the Zenodo DOI. Nothing is released until
that merge. See [`docs/releasing.md`](docs/releasing.md), which also lists the
repository settings that have to be turned on once.

## References

Edwards, B., Allmann, B., Fäh, D., Clinton, J. (2010). Automatic computation of
Expand Down
Loading
Loading