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 --all-packages
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 --all-packages
# 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 --all-packages
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
6 changes: 1 addition & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,11 @@ jobs:
ecs: [{ service: "integration-worker", cluster: "h-stage-euc1-worker", service_name: "h-stage-euc1-worker-integration" }]
},
{
# TODO(H-6664): switch to `package`-based detection once the
# monorepo tooling gives @apps/petrinaut-opt a `build:docker`
# task and workspace dependencies; until then turbo cannot see
# it and change detection runs on `paths` like kratos/hydra.
package: "@python/petrinaut-optimization",
service: "petrinaut-opt",
push: ["ecr", "ghcr"],
dockerfile: "apps/petrinaut-opt/docker/Dockerfile",
context: ".",
paths: ["apps/petrinaut-opt", "libs/@hashintel/petrinaut-cli", "libs/@hashintel/petrinaut-core"],
ecs: [{ service: "petrinaut-opt", cluster: "h-stage-euc1-app", service_name: "h-stage-euc1-app-petrinaut-opt" }]
},
{
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 --force-exclude --fix {staged_files} && uv run --frozen ruff format --force-exclude {staged_files} || true
stage_fixed: true
toml:
tags: [backend, style]
glob: "*.toml"
Expand Down
18 changes: 18 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,18 @@ 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
```

Dependency ranges are bounded on both sides and as tight as the resolution: the floor names the release series `uv.lock` resolves and the cap the next incompatible version, which is what `uv add` writes under `[tool.uv] add-bounds`. `repo-chores lint` fails on a floor older than the resolved series, so a dependency bump is a manifest edit rather than a silent widening.

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, and in `dependencies` the tool owns the `@python`-scoped entries only: an edge to a JavaScript workspace package (a built CLI an image ships, for instance) is declared by hand and survives every sync. Managed arrays in the root manifest are held sorted, which is the form `--check` compares against. 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
14 changes: 9 additions & 5 deletions apps/petrinaut-opt/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
# docker build -f apps/petrinaut-opt/docker/Dockerfile .

ARG NODE_VERSION=22
ARG PYTHON_VERSION=3.13
ARG PYTHON_VERSION=3.14
ARG TURBO_VERSION=2.6.3
ARG UV_VERSION=0.11.18
ARG UV_VERSION=0.11.28


FROM node:${NODE_VERSION}-bookworm-slim AS cli-pruner
Expand Down Expand Up @@ -79,10 +79,14 @@ WORKDIR /app
COPY --from=uv /uv /usr/local/bin/uv

# Install locked Python dependencies independently from the application source
# for better layer caching. The project itself is imported directly from /app.
COPY apps/petrinaut-opt/pyproject.toml apps/petrinaut-opt/uv.lock ./
# for better layer caching. One lockfile covers the whole Python workspace, so
# resolution reads the workspace root manifest next to this member's own, and
# `--package` narrows the install to this service's runtime dependencies. The
# service itself is imported directly from /app.
COPY pyproject.toml uv.lock ./
COPY apps/petrinaut-opt/pyproject.toml apps/petrinaut-opt/pyproject.toml
Comment on lines +86 to +87
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-install-project
uv sync --frozen --no-dev --no-install-workspace --package petrinaut-optimization


FROM python:${PYTHON_VERSION}-slim-bookworm AS runner
Expand Down
9 changes: 7 additions & 2 deletions apps/petrinaut-opt/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"title": "HTTPValidationError",
"type": "object"
},
"JsonValue": {},
"Phase": {
"enum": [
"idle",
Expand Down Expand Up @@ -143,7 +144,9 @@
"content": {
"application/json": {
"schema": {
"additionalProperties": true,
"additionalProperties": {
"$ref": "#/components/schemas/JsonValue"
},
"title": "Optimization Manifest",
"type": "object"
}
Expand Down Expand Up @@ -201,7 +204,9 @@
"content": {
"application/json": {
"schema": {
"additionalProperties": true,
"additionalProperties": {
"$ref": "#/components/schemas/JsonValue"
},
"title": "Optimization Manifest",
"type": "object"
}
Expand Down
10 changes: 8 additions & 2 deletions apps/petrinaut-opt/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
{
"name": "@apps/petrinaut-opt",
"name": "@python/petrinaut-optimization",
"version": "0.0.0-private",
"private": true,
"description": "Scenario-parameter optimization for Petrinaut using Optuna",
"license": "(MIT OR Apache-2.0)",
"scripts": {
"build:docker": "docker buildx build --tag petrinaut-opt --file docker/Dockerfile ../../ --load",
"codegen": "uv run python -m scripts.generate_openapi",
"fix:ruff": "uv run --frozen ruff check --fix . && uv run --frozen ruff format .",
"install:deps": "uv sync",
"lint:ruff": "uv run --frozen ruff check . && uv run --frozen ruff format --check .",
"start": "uv run uvicorn src.optimization_api:app",
"start:dev": "uv run uvicorn src.optimization_api:app --reload",
"test:unit": "uv run pytest && yarn codegen && git diff --exit-code -- openapi/openapi.json"
"test:unit": "uv run --frozen pytest tests"
},
"dependencies": {
"@hashintel/petrinaut-cli": "workspace:*"
}
}
Loading
Loading