Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .config/mise/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ node = { version = "22.21.1", postinstall = "corepack enable" }

# Helper to install tools
cargo-binstall = "1.19.1"
uv = "0.11.18"
uv = "0.11.28"

# Tools to compile the project
'cargo:wasm-opt' = "0.116.1"
Expand Down
26 changes: 13 additions & 13 deletions .config/mise/mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .config/mise/tasks/fix/python.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
#MISE description="Fix Python: sync lockfile, apply ruff autofixes and formatting"
set -euo pipefail

mise exec uv -- uv sync
mise exec uv -- uv run --frozen ruff check --fix .
mise exec uv -- uv run --frozen ruff format .
26 changes: 26 additions & 0 deletions .config/mise/tasks/lint/python.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
#MISE description="Lint Python: lock stability, workspace sync, ruff format + lint, ty type checks, tach architecture checks"
set -uo pipefail

FAILED=0

run() {
echo "$ $*"
mise exec uv -- "$@" || FAILED=1
}

# `--locked` doubles as the uv.lock stability check: it fails if the lockfile
# is out of sync with the workspace manifests.
run uv sync --locked
# Boundaries: explicit workspace membership, uniform requires-python, shared
# tool pins, and generated package.json turbo wiring.
run uv run --frozen repo-chores sync --check
# Dependency rules: fully bounded version ranges, registry-only sources.
run uv run --frozen repo-chores lint
run uv run --frozen ruff format --check .
run uv run --frozen ruff check .
run uv run --frozen ty check
run uv run --frozen tach check
run uv run --frozen tach check-external

exit "$FAILED"
11 changes: 11 additions & 0 deletions .config/mise/tasks/sync/python.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
#MISE description="Synchronize Python workspace membership, version bounds, and generated package.json wiring"
set -euo pipefail

# Unfrozen on purpose: this is a fix task, so a stale uv.lock is expected
# input. `uv run` refreshes the lock and environment before running the tool.
mise exec uv -- uv run repo-chores sync
# Membership or dependency fixes change the workspace graph; refresh the lock
# and environment again so the result is immediately usable.
mise exec uv -- uv lock
mise exec uv -- uv sync
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ blocks/**/.env
**/pip-delete-this-directory.txt
**/.mypy_cache
**/.pytest_cache
**/.ruff_cache
**/.hypothesis
**/wheels

Expand Down
7 changes: 5 additions & 2 deletions .github/actions/prune-repository/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ runs:
shell: bash
env:
SCOPE: ${{ inputs.scope }}
run: python3 ${{ github.action_path }}/prune.py "$SCOPE"
# Executed directly: pruning happens before any dependencies are
# installed, so the script relies on the stdlib alone. `repo-chores
# prune` wraps the same logic for use inside the workspace environment.
run: python3 libs/@local/repo-chores/python/repo_chores/prune.py "$SCOPE"

- name: Copy required files
shell: bash
run: |
cp -R Cargo.toml Cargo.lock rust-toolchain.toml oxfmt.config.ts infra/ out/
cp -R Cargo.toml Cargo.lock rust-toolchain.toml oxfmt.config.ts pyproject.toml uv.lock infra/ out/

# Globs are fun, especially in Bash. Covers all dot-files except `.`, `..`, and `.git`.
shopt -s extglob
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ jobs:
| jq '[.tasks[] | select(.task == "lint:clippy" and .command != "<NONEXISTENT>")] != []' || echo 'false')
echo "clippy=$CLIPPY" | tee -a $GITHUB_OUTPUT

RUFF=$(turbo run lint:ruff --filter '${{ matrix.name }}' --dry-run=json \
| jq '[.tasks[] | select(.task == "lint:ruff" and .command != "<NONEXISTENT>")] != []' || echo 'false')
echo "ruff=$RUFF" | tee -a $GITHUB_OUTPUT

HAS_RUST=$([[ -f "${{ matrix.path }}/Cargo.toml" || ${{ matrix.path }} = "apps/hash-graph" ]] && echo 'true' || echo 'false')
echo "has-rust=$HAS_RUST" | tee -a $GITHUB_OUTPUT

Expand Down Expand Up @@ -129,6 +133,10 @@ jobs:
if: always() && steps.lints.outputs.tsc == 'true'
run: turbo run lint:tsc --filter "${{ matrix.name }}"

- name: Run ruff
if: always() && steps.lints.outputs.ruff == 'true'
run: turbo run lint:ruff --filter "${{ matrix.name }}"

- name: Run rustfmt
if: always() && steps.lints.outputs.has-rust == 'true'
working-directory: ${{ matrix.path }}
Expand Down Expand Up @@ -282,6 +290,20 @@ jobs:
exit 1
fi

- name: Run yarn lint:python
if: ${{ success() || failure() }}
run: |
if ! yarn lint:python; then
echo ''
echo ''
echo 'ℹ️ ℹ️ ℹ️'
echo 'Python checks failed (uv.lock stability, ruff, ty, or tach).'
echo 'Try running `yarn fix:python` locally to apply autofixes;'
echo 'ty and tach findings need to be fixed by hand.'
echo 'ℹ️ ℹ️ ℹ️'
exit 1
fi

- name: Validate Claude Code skills
if: ${{ success() || failure() }}
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ blocks/**/.env
**/pip-delete-this-directory.txt
**/.mypy_cache
**/.pytest_cache
**/.ruff_cache
**/.hypothesis
**/wheels

Expand Down
5 changes: 5 additions & 0 deletions .lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ pre-commit:
glob: "*.rs"
run: rustfmt {staged_files} || true
stage_fixed: true
python:
tags: [backend, style]
glob: "*.py"
run: uv run --frozen ruff check --fix {staged_files} && uv run --frozen ruff format {staged_files} || true
stage_fixed: true
toml:
tags: [backend, style]
glob: "*.toml"
Expand Down
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ The HASH repository is organized into several key directories:
- TypeScript type check: `yarn lint:tsc`
- ESLint: `yarn lint:eslint`
- Formatting check: `yarn lint:format`
- Python (ruff, ty, tach, dependency bounds, uv.lock stability, workspace sync): `mise run lint:python`

- Fix ESLint issues: `yarn fix:eslint`
- Fix formatting: `yarn fix:format`
- Fix Python (ruff autofixes + formatting, lock refresh): `mise run fix:python`

### For Specific Packages

Expand All @@ -77,6 +79,10 @@ turbo run <command> --filter '<package-name>'
cargo nextest run --package <package-name>
cargo test --package <package-name> --doc # For doc tests
cargo clippy --all-features --package <package-name>

# For Python packages (from the package directory)
uv run --frozen pytest tests
uv run --frozen ruff check .
```

For Rust packages, you can add features as needed with `--all-features`, specific features like `--features=foo,bar`, or use `cargo-hack` with `--feature-powerset` for comprehensive feature testing.
Expand All @@ -91,6 +97,16 @@ mise run sync:turborepo # sync package.json identity + deps from Cargo.toml m

`sync:turborepo` only manages that generated wiring β€” the `scripts` section is hand-maintained and is used by CI and Turborepo (e.g. `test:unit`, `lint:clippy`, `doc:dependency-diagram`), so add or edit scripts by hand. The task wraps the `repo-chores` CLI; the equivalent direct invocation is `cargo run --package hash-repo-chores --bin repo-chores-cli -- sync-turborepo`. A related task, `mise run fix:package-json`, sorts `package.json` keys consistently.

### Monorepo wiring for Python packages

Python packages form a single uv workspace rooted at the repository's `pyproject.toml` (shared lockfile, virtual environment, and ruff/ty/tach configuration; Python version bound `>=3.14,<3.15`). After **adding, removing, or renaming a Python package**, or changing its workspace dependencies, re-sync the generated wiring:

```bash
mise run sync:python # membership, requires-python, dev pins, package.json wiring, uv.lock
```

The task wraps the Python `repo-chores` CLI (`libs/@local/repo-chores/python`; direct invocation: `uv run repo-chores sync`). Unlike the Rust wiring, the managed `package.json` includes baseline `scripts` (`lint:ruff`, `fix:ruff`, and `test:unit` when a `tests/` directory exists) β€” ruff and pytest are requirements, not opt-ins. Extra scripts and other keys are preserved. CI runs `repo-chores sync --check` and fails on any drift, including `pyproject.toml` files that are not workspace members. Import boundaries for new packages are declared in a `tach.domain.toml` next to the package's code; global tach settings live under `[tool.tach]` in the root `pyproject.toml`.

## Documentation Maintenance

### Petrinaut user-facing docs
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,39 @@ This repository's contents is divided across several primary sections:

</details>

<details>
<summary> &nbsp; Python tooling</summary>

### Python tooling

Python packages are managed as a single [uv](https://docs.astral.sh/uv/) workspace rooted at [`pyproject.toml`](/pyproject.toml), pinned to Python 3.14 (uv downloads the interpreter automatically; `mise install` provides uv itself). All packages share one lockfile (`uv.lock`), one virtual environment (`.venv`), and the workspace-level tool configuration:

- **[ruff](https://docs.astral.sh/ruff/)** β€” linting (`ALL` rules + preview, with curated exceptions) and formatting
- **[ty](https://docs.astral.sh/ty/)** β€” type checking
- **[tach](https://docs.gauge.sh/)** β€” import-boundary and layering checks, configured under `[tool.tach]` in the root `pyproject.toml`
- **pytest** β€” tests, run per-package through turbo (`test:unit`)

Common commands:

```sh
uv sync # create/refresh .venv from uv.lock
mise run lint:python # everything CI runs: lock stability, sync --check, dependency lint, ruff, ty, tach
mise run fix:python # apply ruff autofixes + formatting, refresh the lock
mise run sync:python # regenerate the boring parts (see below)
```

To add a new Python package, create a directory with a `pyproject.toml` (any location; `libs/` or `tools/` are conventional) and run `mise run sync:python`. The sync command β€” implemented by [`libs/@local/repo-chores/python`](/libs/@local/repo-chores/python), the Python sibling of the Rust `sync-turborepo` tooling β€” generates and enforces:

- explicit membership in the root `[tool.uv.workspace]` (no globs; CI fails on unlisted packages),
- a uniform `requires-python` bound across every member (the Python 3.14 boundary),
- shared ruff/pytest pins in each member's dev group,
- each member's `package.json` turbo wiring (`@python/<name>` with managed `lint:ruff`/`fix:ruff`/`test:unit` scripts and `workspace:*` dependencies mirroring `[tool.uv.sources]`),
- coverage by the root `package.json` yarn workspace globs, plus the managed ruff `src` and pytest `testpaths` arrays.

Module boundaries for new packages are declared in a `tach.domain.toml` next to the package's code (layer assignments; see [`libs/@local/repo-chores/python/repo_chores/tach.domain.toml`](/libs/@local/repo-chores/python/repo_chores/tach.domain.toml)). CI prunes per-package jobs with `turbo prune` (via `repo-chores prune`), which keeps the pruned tree a valid uv workspace by copying the manifests of pruned-out members.

</details>

<details>
<summary> &nbsp; Environment variables</summary>

Expand Down
12 changes: 3 additions & 9 deletions libs/@hashintel/petrinaut-cli/MODEL_EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ The SIR model has two parameters, three uncolored places, and one metric.
Uncolored initial states are supplied as token counts.

```python
with PetrinautClient(
model=Path("libs/@hashintel/petrinaut-cli/examples/sir-model.json")
) as client:
with PetrinautClient(model=Path("libs/@hashintel/petrinaut-cli/examples/sir-model.json")) as client:
result = client.run(
parameters={
"infection_rate": 1.5,
Expand All @@ -57,9 +55,7 @@ attributes defined by that color: `x`, `y`, `direction`, and `velocity`.

```python
with PetrinautClient(
model=Path(
"libs/@hashintel/petrinaut-cli/examples/satellites-launcher.json"
)
model=Path("libs/@hashintel/petrinaut-cli/examples/satellites-launcher.json")
) as client:
result = client.run(
parameters={
Expand Down Expand Up @@ -92,9 +88,7 @@ This model combines token counts with several colors. `InboundShipments`,

```python
with PetrinautClient(
model=Path(
"libs/@hashintel/petrinaut-cli/examples/supply-chain-with-disruption.json"
)
model=Path("libs/@hashintel/petrinaut-cli/examples/supply-chain-with-disruption.json")
) as client:
result = client.run(
parameters={
Expand Down
4 changes: 3 additions & 1 deletion libs/@hashintel/petrinaut-cli/PYTHON_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import optuna

client = PetrinautClient(CLI_PATH, MODEL_PATH)


def objective(trial):
result = client.run(
parameters={
Expand All @@ -122,6 +123,7 @@ def objective(trial):
)
return result["metrics"]["Infected Fraction"]


study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=100, n_jobs=1)
client.close()
Expand All @@ -143,7 +145,7 @@ For parallel trials, create one client/process per Optuna worker.
different color schemas:

```python
initialState={
initialState = {
"InboundShipments": [
{"eta": 1, "risk_score": 0.2, "source": 1, "cost": 10},
{"eta": 2, "risk_score": 0.4, "source": 2, "cost": 12},
Expand Down
2 changes: 2 additions & 0 deletions libs/@hashintel/petrinaut-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect("/tmp/petrinaut.sock")
stream = sock.makefile("rwb")


def request(payload):
stream.write((json.dumps(payload) + "\n").encode())
stream.flush()
Expand All @@ -64,6 +65,7 @@ def request(payload):
raise RuntimeError(response["error"]["message"])
return response["result"]


metadata = request({"id": 1, "method": "metadata"})

result = request({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const script = async () => {
"**/runner_venv/**",
"**/target/**",
"**/venv/**",
"**/.venv/**",
],
});

Expand Down
Loading
Loading