diff --git a/.codex/AGENTS.md b/.codex/AGENTS.md index d8611694..c4ab1a4e 100644 --- a/.codex/AGENTS.md +++ b/.codex/AGENTS.md @@ -1,63 +1,178 @@ # AGENTS.md -## Project overview - -Myna is a Python package for configuring and running additive-manufacturing simulation workflows. The codebase centers on workflow orchestration, application wrappers, database readers, and the CLI used to configure and execute those workflows. - -## Repository map - -- `src/myna/core`: workflow logic, metadata, file types, and shared utilities -- `src/myna/application`: wrappers and templates for external simulation applications -- `src/myna/cli`: CLI entrypoints and Peregrine launcher assets -- `tests`: unit and integration tests -- `examples`: runnable workflow inputs and example datasets -- `docs`: MkDocs documentation sources - -## Environment setup - -- Python 3.10 or newer is required. -- Use `uv` to manage the development environment and dependencies. -- From the repository root, install the development environment with `uv sync --frozen --extra dev`. -- Use `uv run ...` for project commands. -- If dependencies change, rerun `uv lock` and commit the updated `uv.lock`. - -## Development expectations - -- Keep changes focused on the relevant subsystem and update tests or docs when behavior changes. -- For Python changes, run: `uv run pytest`. -- For documentation changes or API-surface changes that can affect docs, also run `uv run mkdocs build --strict`. -- Before handing off PR-ready work, run `uv run pre-commit run --all-files`. - -## Testing notes - -- The default `pytest` configuration excludes tests marked `apps`. -- Example and external-application tests require optional dependencies and executables such as 3DThesis, AdditiveFOAM, or ExaCA. -- If an `apps`-marked test needs a missing executable and `spack` is available, prefer using `spack` to install or load the required app before concluding the test cannot run. -- Do not assume `apps`-marked tests are runnable in a normal local environment. -- If a required tool or dependency is unavailable, run the relevant subset of checks you can and clearly report what was not run. - -## Git and PR conventions - -- If creating a new branch, create it from `main` and use the branch naming convention from `CONTRIBUTING.md`: `/`. -- Use the Conventional Commit format from `CONTRIBUTING.md` for commit messages. -- Use the same Conventional Commit format for pull request titles. -- When opening a PR, complete the template in `.github/pull_request_template.md`. - -## GitHub comment attribution - -- For all agent-authored GitHub comments, use the GitHub connector app tools so GitHub can display the `with ChatGPT Codex Connector` badge. -- This applies to issue comments, pull request conversation comments, pull request reviews, and inline review replies. -- Do not use `gh issue comment`, `gh pr comment`, `gh api` write calls, browser/manual comment flows, or any PAT/OAuth token path for agent-authored GitHub comments when badge attribution is required. -- If connector-based comment writing is unavailable or cannot preserve the badge, stop and report the blocker instead of falling back to another write path. -- Read-only GitHub inspection through `gh` is allowed when the connector does not expose the needed metadata. -- When comment attribution matters, verify the resulting comment in the GitHub UI or through issue or pull request event metadata before considering the task complete. - -### Why - -- GitHub shows the connector badge when a GitHub App acts on behalf of the user. -- Comments posted through `gh` in this environment normally use a regular GitHub CLI OAuth token and will be attributed to the user without the connector badge. -- A GitHub App installation token would attribute the action to the app itself rather than jointly to the user and the app. - -## Formatting - -- Always put a blank line after a heading in markdown files. +## Project Overview + +Myna is a Python package for additive-manufacturing simulation workflows. It connects +build database readers, workflow components, application wrappers, metadata, file +contracts, examples, and CLI stages for configuring, running, and syncing simulations. + +Agents usually work on workflow orchestration, database adapters, application wrappers, +example cases, tests, and documentation. Do not change product behavior for docs-only +tasks. + +## Start Here + +- Read [ARCHITECTURE.md](../ARCHITECTURE.md) for system structure, control flow, and + dependency boundaries. +- Read [CONTRIBUTING.md](../CONTRIBUTING.md) for branch, commit, PR title, and PR template + conventions. +- Read [docs/developer_guide.md](../docs/developer_guide.md) before adding components, + metadata, file types, or application wrappers. +- Read [docs/testing.md](../docs/testing.md) for markers, external-app tests, and CI + expectations. +- Read [docs/documentation.md](../docs/documentation.md) before changing docs or the agent + harness. +- Inspect nearby code and tests before changing a subsystem. + +## Repository Map + +| Path | Purpose | +| --- | --- | +| `src/myna/core/` | Generic workflow orchestration, components, metadata, files, app helpers, and utilities | +| `src/myna/core/workflow/` | `myna config`, `myna run`, `myna sync`, `myna status`, input loading, and Peregrine launch flow | +| `src/myna/core/components/` | Component classes and user-facing component lookup keys | +| `src/myna/core/files/` | Output file contracts and validation/sync value extraction | +| `src/myna/core/metadata/` | Metadata requirement classes and lookup keys | +| `src/myna/database/` | Database readers/adapters for Peregrine, HDF5, MynaJSON, Pelican, AMBench, and no-database mode | +| `src/myna/application/` | External application wrappers, stage scripts, and templates | +| `src/myna/cli/peregrine_launcher/` | Peregrine-oriented launch templates and default workspace | +| `tests/` | Pytest suite; default config excludes `apps` tests | +| `examples/cases/` | Runnable workflow cases and per-case dependency matrix | +| `examples/databases/` | Sample database fixtures for examples/tests | +| `examples/workspaces/` | Example workspace YAML settings | +| `examples/utils/` | Standalone Python API examples | +| `docs/` | MkDocs source pages | +| `scripts/` | Maintenance scripts, including docs generation and harness validation | +| `.github/workflows/` | CI, pre-commit, and docs deployment workflows | + +See [ARCHITECTURE.md](../ARCHITECTURE.md) for the full repository map. + +## Environment Setup + +Start with the development tool preflight: + +```bash +python3 scripts/check_dev_tools.py +``` + +On a fresh clone, install the development environment with `uv` from the repository +root: + +```bash +uv sync --frozen --extra dev +python3 scripts/check_dev_tools.py +``` + +If an agent shell has read-only home caches, use writable cache directories before +running `uv` or `pre-commit`: + +```bash +export UV_CACHE_DIR=/tmp/uv-cache +export PRE_COMMIT_HOME=/tmp/pre-commit-cache +``` + +Install optional Python extras only when needed: + +```bash +uv sync --frozen --extra exaca +uv sync --frozen --extra bnpy +uv sync --frozen --extra cubit +uv sync --frozen --extra deer +``` + +If dependency declarations change, run `uv lock` and commit the updated `uv.lock`. + +## Common Commands + +| Task | Command | +| --- | --- | +| Check dev tools | `python3 scripts/check_dev_tools.py` | +| Format | `uv run ruff format` | +| Lint | `uv run ruff check` | +| Run default tests | `uv run pytest` | +| Run focused test | `uv run pytest tests/test_database.py` | +| Check external executables | `uv run pytest -m apps tests/test_executables.py` | +| Generate API docs | `uv run scripts/group_docs.py` | +| Build docs | `uv run mkdocs build --strict` | +| Check docs harness | `uv run python scripts/check_docs_harness.py` | +| Run pre-commit | `uv run pre-commit run --all-files` | + +## Testing Guidance + +Before handoff, run the most relevant focused tests plus formatting/linting for code +changes. For docs-only changes, run the docs harness and docs build when dependencies +are available. + +Default pytest excludes tests marked `apps`. Tests marked `apps`, `examples`, or +`parallel` may require external tools such as 3DThesis, AdditiveFOAM/OpenFOAM, ExaCA, +Adamantine, Cubit, or DEER. Do not claim those checks passed unless the local tools +were available and the commands actually ran. + +When a command cannot run because of missing optional dependencies, missing external +executables, container restrictions, or network access, report the exact command and +the reason. + +## Architecture Rules + +- Keep generic orchestration in `src/myna/core/`, especially under + `src/myna/core/workflow/`. +- Keep external-tool behavior, templates, executable handling, and postprocessing in + `src/myna/application/`. +- Keep database-specific parsing and sync behavior in `src/myna/database/`. +- Keep components declarative: define requirements, output contracts, and hierarchy + without embedding one application's execution details. +- Preserve user-facing lookup keys in component, metadata, and database lookup modules + unless a breaking change is intentional and documented. +- Parse and validate external files at subsystem boundaries. +- Do not import test helpers from runtime modules. +- Keep generated outputs and machine-local paths out of source control. + +## Documentation Rules + +- Update `ARCHITECTURE.md` when changing subsystem boundaries, control flow, public + extension points, or major dependencies. +- Update user/developer docs when changing commands, examples, installation, optional + dependencies, or public behavior. +- Add a decision record under `docs/decisions/` for decisions that are not obvious from + code review alone. +- Keep this file short; link to deeper docs instead of expanding it. +- Run `uv run python scripts/check_docs_harness.py` after changing `.codex/AGENTS.md`, + `ARCHITECTURE.md`, or linked docs. + +## PR and Commit Guidance + +Contributions target `main` and must pass CI with at least one Myna developer approval. +Use Conventional Commits for commits and PR titles: + +```text +(): +``` + +Branch names use: + +```text +/ +``` + +Examples: `docs/agent-harness`, `fix/database-paths`, `feat(cli): add dry-run flag`. +Complete `.github/pull_request_template.md` and state behavior impact, risk, and +testing strategy. + +## Security and Data Handling + +- Do not commit secrets, credentials, private data, or machine-specific executable + paths. +- Do not commit large generated simulation outputs, local logs, `site/`, + `docs/api-docs/`, or case output folders unless they are intentional fixtures. +- Avoid destructive commands against user data and example fixtures. +- Treat `examples/databases/` as fixture data; keep generated registered/simulation + outputs ignored unless maintainers explicitly request otherwise. + +## Handoff Checklist + +- Relevant tests, lint, docs, or harness checks run. +- Commands not run are listed with concrete reasons. +- Files changed are summarized. +- Public behavior changes, compatibility risks, or known gaps are called out. +- Docs and examples are updated when commands, inputs, outputs, or extension points + change. diff --git a/.codex/config.toml b/.codex/config.toml index 36f2d184..1ee29789 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -1,10 +1,4 @@ #:schema https://developers.openai.com/codex/config-schema.json -model = "gpt-5.4" +model = "gpt-5.5" personality = "pragmatic" -model_reasoning_effort = "xhigh" -plan_mode_reasoning_effort = "xhigh" -approval_policy = "on-request" - -[plugins."github@openai-curated"] -enabled = true diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a2f21dec..94c14a50 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -38,6 +38,8 @@ jobs: uv sync --locked --extra dev - name: Run pytest (without examples) run: uv run --frozen --no-sync pytest + - name: Check documentation harness + run: uv run --frozen --no-sync python scripts/check_docs_harness.py - name: Analyzing code with pylint run: uv run --frozen --no-sync pylint $(git ls-files '*.py') --fail-under=7.25 - name: Building API docs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99719b68..dd4c088b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,3 +37,11 @@ repos: name: licenseheaders (tests) pass_filenames: false args: ["-t", "scripts/bsd-3.tmpl", "--dir", "tests", "--ext", ".py"] + + - repo: local + hooks: + - id: docs-harness + name: docs harness + entry: python3 scripts/check_docs_harness.py + language: system + pass_filenames: false diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..50f2f1ec --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,306 @@ +# Architecture + +## Status + +- Last verified: 2026-06-01 during the documentation-harness update +- Audience: maintainers, contributors, and coding agents +- Scope: describes current architecture; does not replace API docs or user docs + +## System Purpose + +Myna is a Python package for connecting additive-manufacturing build data with +multi-stage modeling and simulation workflows. It provides a structured interface +between build databases, workflow components, application wrappers, and output files so +users can configure, run, and sync simulation pipelines from Myna input files. + +The main user workflows are: + +- install Myna from a local checkout with `uv` or `pip`; +- write or copy a workflow input file from `examples/cases/`; +- run `myna config` to extract required metadata and create case directories; +- run `myna run` to execute configured application stages; +- run `myna sync` to push supported outputs back to a database; +- use `myna launch_peregrine` for Peregrine-oriented workflow launch inputs; +- import `myna` from Python for utility workflows, as shown under `examples/utils/`. + +## Architectural Goals + +- Keep workflow orchestration generic across build database formats. +- Represent workflow steps as components with explicit metadata, input, output, and + hierarchy requirements. +- Isolate external simulation-tool behavior in application wrappers. +- Keep database-specific parsing and sync behavior behind database adapter classes. +- Preserve reproducible local environments through `pyproject.toml` and `uv.lock`. +- Make examples runnable while clearly marking optional Python packages and external + applications. +- Keep generated case files, generated API docs, and local simulation outputs out of + source control unless they are intentional fixtures. + +## Repository Map + +| Path | Purpose | Notes for agents | +| --- | --- | --- | +| `pyproject.toml` | Package metadata, dependencies, optional extras, CLI entrypoint, pytest markers | Source of truth for runtime Python version and dependency groups | +| `uv.lock` | Locked `uv` dependency graph | Commit updates when dependency declarations change | +| `src/myna/` | Python package source | Runtime code lives here | +| `src/myna/core/` | Workflow orchestration, components, file abstractions, metadata, app base helpers, utilities | Prefer generic workflow behavior here | +| `src/myna/core/workflow/` | `config`, `run`, `sync`, `status`, and input loading stages | CLI entrypoint delegates here | +| `src/myna/core/components/` | Workflow component classes and lookup table | Component string keys are user-facing compatibility surface | +| `src/myna/core/files/` | Output file classes and validation/sync value extraction | Add new output formats here | +| `src/myna/core/metadata/` | Build, part, layer, and file metadata requirements | Add new database-extracted requirements here | +| `src/myna/core/app/` | Shared application wrapper behavior | Use `MynaApp` for reusable app argument, template, process, MPI, and Docker behavior | +| `src/myna/database/` | Database readers/adapters and datatype lookup | Keep database-specific parsing and sync here | +| `src/myna/application/` | Wrappers/templates for external applications | Keep tool-specific configure/execute/postprocess logic here | +| `src/myna/cli/peregrine_launcher/` | Peregrine launch templates and default workspace | Used by `myna launch_peregrine` | +| `src/myna/mist_material_data/` | Packaged material JSON data | Package data used by application workflows | +| `tests/` | Pytest suite | Default test command excludes `apps` marker | +| `examples/cases/` | Runnable workflow cases | See `examples/cases/README.md` for dependency matrix | +| `examples/databases/` | Sample database fixtures | Some generated outputs under these trees are ignored | +| `examples/workspaces/` | Example workspace YAML files | Workspace files share app settings such as executables | +| `examples/utils/` | Standalone Python API examples | Not the same as runnable workflow cases | +| `docs/` | MkDocs source pages | Navigation controlled by `docs/.pages` | +| `docs/api-docs/` | Generated API docs from `scripts/group_docs.py` | Ignored by git; regenerate before docs builds | +| `docs/decisions/` | Lightweight architecture decision records | Add records for non-obvious architectural choices | +| `scripts/group_docs.py` | Generates and groups LazyDocs API documentation | CI runs this before `mkdocs build --strict` | +| `scripts/check_dev_tools.py` | Checks local development tool availability and writable caches | Run first in new agent or container shells | +| `scripts/check_docs_harness.py` | Validates required agent-harness docs and links | Runs in pre-commit and CI | +| `.pre-commit-config.yaml` | Local quality hooks | Includes Ruff, codespell, license headers, and docs harness check | +| `.github/workflows/CI.yml` | Package build, default tests, pylint, API docs, MkDocs, and external-app example CI | External-app job runs in a container | +| `.github/workflows/pre-commit.yml` | Pre-commit CI | Runs hooks on pull requests | +| `.github/workflows/doc-deployment.yml` | GitHub Pages deployment | Regenerates API docs and deploys MkDocs site on `main` | +| `CONTRIBUTING.md` | Contribution workflow, commit, PR title, and branch conventions | Follow for PR handoff | +| `README.md` | User-facing project overview and installation | Keep installation and high-level examples current | +| `.codex/AGENTS.md` | Agent entrypoint | Keep short; link to deeper docs | +| `.codex/config.toml` | Local Codex configuration for this repository | Codex-specific runtime configuration | + +## Runtime and Packaging Model + +- Runtime language: Python `>=3.10`, from `pyproject.toml`. +- CI runtime: GitHub Actions package/test job currently uses Python `3.10`. +- Build backend: `setuptools.build_meta` with packages discovered under `src/`. +- Preferred dependency workflow: `uv sync --frozen` from the checked-in `uv.lock`. +- Development dependencies: `uv sync --frozen --extra dev`. +- Optional Python dependency groups: + - `exaca`: `pyebsd`; + - `bnpy`: `bnpy`, `opencv-python`, `POT`; + - `cubit`: `netCDF4`; + - `deer`: `netCDF4`. +- Runtime dependencies include Git-hosted packages, including `mistlib` from + `ORNL-MDF/mist`. +- Console script: `myna = "myna.core.workflow.all:main"`. +- Generated files that should not be hand-edited as source: + - `docs/api-docs/`, generated by `scripts/group_docs.py`; + - local MkDocs `site/`; + - workflow output folders such as `myna_resources/`, `myna_output/`, logs, and + registered/simulation output folders listed in `.gitignore`. + +On import, `src/myna/core/__init__.py` sets: + +- `MYNA_INSTALL_PATH` to the installed `myna` package root; +- `MYNA_APP_PATH` to the installed `myna/application` directory. + +Workflow commands set additional runtime environment variables, including `MYNA_INPUT`, +`MYNA_CONFIG_INPUT`, `MYNA_RUN_INPUT`, `MYNA_SYNC_INPUT`, `MYNA_STEP_NAME`, +`MYNA_STEP_CLASS`, and `MYNA_STEP_INDEX`. The `*_INPUT` variables are marked in code as +future deprecation targets; prefer `MYNA_INPUT` for new shared behavior. + +## Main Concepts and Domain Model + +- **Input file**: YAML or JSON workflow settings loaded by + `myna.core.workflow.load_input`. Accepted suffixes are `.yaml`, `.json`, + `.myna-workspace`, and `.myna-workspace-json`. +- **Workflow**: Ordered `steps` in an input file. The CLI can configure, run, and sync + all steps or selected steps. +- **Step**: A named workflow entry with a component `class`, an `application`, optional + app-stage arguments, optional executable, and optional input/output templates. +- **Component**: A subclass of `myna.core.components.Component` that declares + `data_requirements`, `input_requirement`, `output_requirement`, and hierarchical + `types` such as `build`, `build_region`, `part`, `region`, and `layer`. +- **Application wrapper**: Tool-specific code under `src/myna/application///` + that may provide `configure.py`, `execute.py`, and `postprocess.py` stages. Wrappers + commonly use `myna.core.app.MynaApp`. +- **Database adapter**: A subclass of `myna.core.db.Database` under + `src/myna/database/` that loads metadata and syncs outputs for a supported data + source. +- **Metadata**: Build, part, layer, or file requirements declared by components and + fulfilled by database adapters through classes in `src/myna/core/metadata/`. +- **File abstraction**: Output file classes in `src/myna/core/files/` that validate + files and optionally expose values for database sync. +- **Workspace**: Optional `.yaml` or `.myna-workspace` settings file referenced by + `myna.workspace` in an input file. Workspaces share app settings such as executable + paths across inputs. +- **Case directory**: Directory generated by `myna config` for a build/part/region/layer + and step. Each case gets a `myna_data.yaml` subset of the configured input data. +- **Configure/run/sync stages**: The primary CLI flow. `config` extracts and writes + required inputs, `run` delegates to app stages, and `sync` sends valid outputs through + database adapter sync logic. + +## Control Flow + +```mermaid +flowchart TD + UserInput[Input YAML/JSON and optional workspace] --> CLI[myna CLI] + CLI --> Config[myna config] + Config --> LoadInput[load_input validation] + Config --> DB[database adapter] + DB --> Metadata[metadata and input files] + Config --> Component[component instance] + Component --> Cases[case directories and myna_data.yaml] + CLI --> Run[myna run] + Run --> ComponentRun[Component.run_component] + ComponentRun --> AppStages[application configure/execute/postprocess scripts] + AppStages --> Outputs[component output files] + CLI --> Sync[myna sync] + Sync --> FileValidation[file validation] + FileValidation --> DBSync[database adapter sync] +``` + +The command dispatch starts in `myna.core.workflow.all.main`. For `config`, Myna loads +the input file, resolves the database adapter with `return_datatype_class`, extracts +component metadata requirements, creates case directories, writes per-case +`myna_data.yaml`, records expected output paths, and writes the configured input. For +`run`, Myna reloads the input before each step, applies step settings to a component, +sets step environment variables, and runs the available app-stage scripts. For `sync`, +Myna validates component outputs and delegates supported sync behavior to the selected +database adapter. + +## Dependency Boundaries + +Current intended boundaries: + +- `src/myna/core/` owns generic abstractions and workflow orchestration. +- `src/myna/core/components/` should declare requirements and hierarchy, not embed + external application execution details. +- `src/myna/core/files/` should validate Myna file formats and expose sync values, not + parse database layouts. +- `src/myna/core/metadata/` should define metadata requirements and base loading + behavior, not hard-code one database schema. +- `src/myna/database/` owns database-specific metadata loading, existence checks, + segmentation type, and output sync. +- `src/myna/application/` owns external-tool wrappers, templates, executable handling, + and postprocessing needed to satisfy component file contracts. +- `src/myna/cli/` contains launch templates and CLI-specific support; general workflow + behavior belongs under `src/myna/core/workflow/`. +- `tests/` contains test-only helpers. Do not import from `tests/` in runtime modules. + +Current mechanical enforcement is limited. There is no import-layer checker. Boundaries +are enforced by code organization, pytest coverage, Ruff, pylint in CI, docs, and +review. When changing subsystem dependencies, update this document and consider whether +tests or a lightweight check should enforce the new rule. + +## Extension Points + +| Extension | Put code here | Inspect first | Add or update | Validation | +| --- | --- | --- | --- | --- | +| New workflow component | `src/myna/core/components/` | `component.py`, a similar `component_*.py`, `component_class_lookup.py` | Component subclass, lookup key, tests, docs/example input if user-facing | `uv run pytest tests/test_component_output_paths.py` plus focused tests | +| New output file type | `src/myna/core/files/` | `file.py`, `file_vtk.py`, `file_temperature.py` | File subclass, `__init__.py`, component output requirement, sync value handling if needed | Focused file validation tests | +| New metadata requirement | `src/myna/core/metadata/` | `data.py`, `file.py`, existing `data_*` and `file_*` modules, `data_class_lookup.py` | Metadata class, lookup entry, database adapter load support, tests | Focused configure/database tests | +| New database reader | `src/myna/database/` | `database_types.py`, `myna_json.py`, `peregrine_hdf5.py`, `pelican.py` | Adapter subclass, lookup entry, example fixture/input if useful, database tests | `uv run pytest tests/test_database.py` and configure tests | +| New application wrapper | `src/myna/application///` | `src/myna/core/app/base.py`, a similar app wrapper, `docs/developer_guide.md` | `app.py` or stage scripts, templates, optional dependency docs, example case, tests | Unit tests by default; `apps`/`examples` tests only with tools available | +| New CLI command or mode | `src/myna/core/workflow/` or `src/myna/cli/` | `all.py`, `launch_from_peregrine.py`, `docs/cli.md` | Parser dispatch, tests, docs, launch template if applicable | Focused CLI tests and docs build | +| New example case | `examples/cases/` | `examples/cases/README.md`, similar case input | `input.yaml`, optional readme/template, dependency matrix row, tests if runnable in CI | `uv run pytest -m examples` only when dependencies exist | +| New docs page | `docs/` | `docs/.pages`, `docs/documentation.md`, `mkdocs.yml` | Page, navigation, supporting links | `uv run mkdocs build --strict` after generating API docs | +| New architecture decision | `docs/decisions/` | `docs/decisions/0000-template.md` | Numbered decision record and nav update if needed | Docs harness and docs build | + +Preserve lookup keys in `component_class_lookup.py`, `data_class_lookup.py`, and +`database_types.py` unless a breaking change is intentional and documented. + +## Testing and Validation Architecture + +Pytest configuration lives in `pyproject.toml`. The default addopts are: + +```bash +--import-mode=importlib -m "not apps" +``` + +Markers: + +- `apps`: requires external application dependencies; +- `examples`: runs cases in `examples/`; +- `parallel`: example uses multiple cores. + +Common validation commands: + +| When | Command | Notes | +| --- | --- | --- | +| New shell preflight | `python3 scripts/check_dev_tools.py` | Verifies `uv`, writable caches, lock state, and dev tool entrypoints | +| Install dev tools | `uv sync --frozen --extra dev` | Uses `uv.lock`; add extras only as needed | +| Format | `uv run ruff format` | Same formatter as pre-commit | +| Lint | `uv run ruff check` | Pylint also runs in CI | +| Default tests | `uv run pytest` | Excludes `apps` tests | +| Focused tests | `uv run pytest tests/test_database.py` | Prefer targeted tests while iterating | +| External-app availability | `uv run pytest -m apps tests/test_executables.py` | Requires tools on `PATH` | +| External examples | `uv run pytest -m "examples and not parallel"` | Requires external tools and example fixtures | +| Generate API docs | `uv run scripts/group_docs.py` | Writes ignored `docs/api-docs/` | +| Build docs | `uv run mkdocs build --strict` | Run after API docs generation for parity with CI | +| Docs harness | `uv run python scripts/check_docs_harness.py` | Verifies required agent docs and links | +| Pre-commit | `uv run pre-commit run --all-files` | May require network the first time hooks install | + +CI behavior: + +- `.github/workflows/CI.yml` installs with `uv sync --locked --extra dev`, runs default + pytest, checks the docs harness, runs pylint with `--fail-under=7.25`, generates API + docs, and builds MkDocs strictly. +- The `test-examples` CI job runs in `ghcr.io/ornl-mdf/containers/ubuntu:dev`, + installs all extras, checks external executables, and runs example tests split by + serial/parallel markers. +- `.github/workflows/pre-commit.yml` runs the pre-commit hooks on pull requests. + +## Documentation Architecture + +Documentation has three audiences: + +- user-facing overview and install guidance in `README.md`, `docs/index.md`, and + `docs/getting_started.md`; +- developer and extension guidance in `docs/developer_guide.md`, `docs/testing.md`, + `docs/documentation.md`, and `docs/decisions/`; +- agent-facing orientation in `.codex/AGENTS.md` and this `ARCHITECTURE.md`. + +The docs site uses MkDocs Material with `awesome-pages`, `awesome-nav`, +`gh-admonitions`, and `pymdownx.superfences`. Top-level docs navigation is in +`docs/.pages`. API docs are generated by `scripts/group_docs.py` using LazyDocs and are +ignored from git as `docs/api-docs/`. + +Update docs when behavior changes: + +- update `README.md` or `docs/getting_started.md` for installation, usage, or public CLI + workflow changes; +- update `docs/developer_guide.md` for new extension patterns; +- update `docs/testing.md` for markers, CI changes, or new validation commands; +- update `docs/documentation.md` for docs tooling or harness changes; +- update `ARCHITECTURE.md` for subsystem boundaries, control flow, extension points, or + significant dependency changes; +- add a decision record under `docs/decisions/` when a design choice will not be obvious + from code review alone; +- keep `.codex/AGENTS.md` compact and link outward instead of copying long explanations. + +## External Systems and Data + +Required for the Python package: + +- Python `>=3.10`; +- Python dependencies from `pyproject.toml` and `uv.lock`. + +Optional or workflow-specific: + +- 3DThesis, commit `646d461` or later, for Thesis application examples; +- AdditiveFOAM version `1.0` or later and OpenFOAM 10 for AdditiveFOAM/OpenFOAM + application workflows; +- ExaCA version `1.3` or later for ExaCA workflows; +- Adamantine, Cubit, and DEER for their corresponding application wrappers/examples; +- Docker daemon when using `MynaApp` Docker execution options or the project container; +- sample databases under `examples/databases/` for local examples. + +Never commit credentials, local executable paths, private data, or large generated +simulation outputs unless they are intentional fixtures. Prefer workspace files for +local executable configuration, and keep machine-specific values out of shared examples. + +## Known Gaps and Follow-Up Work + +| Gap | Why it matters | Suggested follow-up | +| --- | --- | --- | +| Import-layer boundaries are documented but not mechanically enforced | Cross-layer imports could creep into generic workflow code | Consider a small import-linter only if boundary drift becomes a recurring issue | +| External application version coverage is mostly documented in prose and CI container behavior | Local users may not know which exact tool version failed | Add per-application troubleshooting notes as failures are reported | +| API docs are generated but not committed | A fresh checkout needs generation before strict MkDocs parity with CI | Keep `scripts/group_docs.py` documented and run it before docs-build checks | +| LazyDocs API generation is verified in CI on Python 3.10, not all newer local Python versions | Local docs builds can fail before checking authored docs content | Use a Python 3.10 environment for docs-generation parity with CI | +| Deprecated environment variables remain in workflow code | New code could depend on names marked for future removal | Prefer `MYNA_INPUT` in new code and track deprecation in release notes when removal is planned | diff --git a/README.md b/README.md index d24675e4..164f9bc1 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,11 @@ available Peregrine v2023-10 dataset. If you use this data, please cite ## Development -See the [guidelines](CONTRIBUTING.md) on how to contribute. +See the [guidelines](CONTRIBUTING.md) on how to contribute. Repository architecture is +summarized in [ARCHITECTURE.md](ARCHITECTURE.md), agent-oriented instructions are in +[.codex/AGENTS.md](.codex/AGENTS.md), and focused development/testing guidance is in +[docs/developer_guide.md](docs/developer_guide.md) and +[docs/testing.md](docs/testing.md). ## License diff --git a/docs/.pages b/docs/.pages index 65826c72..d0129145 100644 --- a/docs/.pages +++ b/docs/.pages @@ -4,4 +4,8 @@ nav: - docker.md - cli.md - developer_guide.md + - testing.md + - documentation.md + - updating_changelog.md + - decisions - api-docs diff --git a/docs/decisions/.pages b/docs/decisions/.pages new file mode 100644 index 00000000..912f3c17 --- /dev/null +++ b/docs/decisions/.pages @@ -0,0 +1,4 @@ +title: Architecture Decisions +nav: + - index.md + - 0000-template.md diff --git a/docs/decisions/0000-template.md b/docs/decisions/0000-template.md new file mode 100644 index 00000000..14c3ee29 --- /dev/null +++ b/docs/decisions/0000-template.md @@ -0,0 +1,25 @@ +--- +title: Decision Template +--- + +# 0000: Decision Title + +## Status + +Proposed | Accepted | Superseded + +## Context + +Describe the problem, constraints, and relevant repository evidence. + +## Decision + +State the decision clearly. + +## Consequences + +Describe expected benefits, tradeoffs, compatibility impact, and maintenance impact. + +## Validation + +List tests, docs checks, examples, or follow-up work that should validate the decision. diff --git a/docs/decisions/index.md b/docs/decisions/index.md new file mode 100644 index 00000000..a2b440c2 --- /dev/null +++ b/docs/decisions/index.md @@ -0,0 +1,14 @@ +--- +title: Architecture Decisions +--- + +# Architecture Decisions + +This directory contains lightweight architecture decision records for Myna. + +Add a decision record when a design choice affects future contributors, subsystem +boundaries, public workflows, dependency policy, or maintenance expectations and the +reasoning will not be obvious from code review alone. + +Use `0000-template.md` as the starting point for new records. Number records +monotonically with four digits, for example `0001-document-agent-harness.md`. diff --git a/docs/developer_guide.md b/docs/developer_guide.md index 5c014a7d..9575f569 100644 --- a/docs/developer_guide.md +++ b/docs/developer_guide.md @@ -2,8 +2,53 @@ title: Myna Developer Guide --- -To install developer-related Python packages, follow the instructions in -the [main project README](https://github.com/ORNL-MDF/Myna/blob/main/README.md). +This guide focuses on extending Myna. For local validation details, see +[Testing](testing.md). For documentation maintenance, see +[Documentation](documentation.md). + +## Local development workflow + +Check that the current shell can find `uv` and has writable caches expected by the +repository: + +```bash +python3 scripts/check_dev_tools.py +``` + +Install development dependencies from the repository root with: + +```bash +uv sync --frozen --extra dev +``` + +Then rerun the preflight to verify that `pytest`, `ruff`, `mkdocs`, and `pre-commit` +are available through `uv run`: + +```bash +python3 scripts/check_dev_tools.py +``` + +Some coding-agent and container shells do not inherit the same `PATH` or writable home +cache directories as an interactive terminal. If the preflight reports cache errors, +set writable cache locations before running `uv` or `pre-commit`: + +```bash +export UV_CACHE_DIR=/tmp/uv-cache +export PRE_COMMIT_HOME=/tmp/pre-commit-cache +``` + +Run the default validation loop with: + +```bash +uv run ruff format +uv run ruff check +uv run pytest +``` + +If you change dependencies, run `uv lock` and commit the updated `uv.lock`. If you +change documentation, run `uv run python scripts/check_docs_harness.py`; for MkDocs +pages, generate API docs with `uv run scripts/group_docs.py` and then run +`uv run mkdocs build --strict`. ## Developing new workflow components diff --git a/docs/documentation.md b/docs/documentation.md new file mode 100644 index 00000000..6ccde26d --- /dev/null +++ b/docs/documentation.md @@ -0,0 +1,100 @@ +--- +title: Documentation +--- + +# Documentation + +Myna documentation is split between root project files, MkDocs site pages, generated +API docs, examples, and agent-facing orientation files. + +## Documentation Surfaces + +| Surface | Purpose | +| --- | --- | +| `README.md` | User-facing project overview, installation, citation, and high-level usage | +| `ARCHITECTURE.md` | Repository architecture, boundaries, control flow, and extension points | +| `.codex/AGENTS.md` | Short entrypoint and checklist for coding agents | +| `CONTRIBUTING.md` | Contributor workflow, branch names, commit convention, and PR title format | +| `docs/` | MkDocs source pages for user/developer documentation | +| `docs/api-docs/` | Generated API documentation, ignored by git | +| `docs/decisions/` | Lightweight architecture decision records | +| `examples/README.md` and nested readmes | Example organization and dependency notes | +| `CHANGELOG.md` | User-visible release and unreleased change history | + +## Docs Toolchain + +The docs site uses MkDocs Material. Configuration is in `mkdocs.yml`; navigation is +managed by `docs/.pages` through the Awesome Pages plugins. + +API docs are generated by: + +```bash +uv run scripts/group_docs.py +``` + +That script regenerates `docs/api-docs/` with LazyDocs and writes nested `.pages` files. +The generated API docs are ignored by git. Run the script before strict docs builds +when you need parity with CI. + +CI generates docs with Python 3.10. If LazyDocs fails in a newer local Python +environment while generating `docs/api-docs/`, use a Python 3.10 development +environment before treating strict MkDocs failures as docs-content failures. + +Build the docs with: + +```bash +uv run mkdocs build --strict +``` + +## What To Update + +| Change type | Update | +| --- | --- | +| Install, CLI usage, or public workflow behavior | `README.md`, `docs/getting_started.md`, and relevant example docs | +| Component, file, metadata, database, or app extension pattern | `docs/developer_guide.md` and possibly `ARCHITECTURE.md` | +| Test markers, CI commands, external-app checks | `docs/testing.md` | +| Docs tooling, navigation, generated API docs, or harness checks | This page and `.codex/AGENTS.md` if agents need to know | +| Subsystem boundary, control flow, dependency model, or extension point | `ARCHITECTURE.md` | +| Non-obvious design choice | New record under `docs/decisions/` | +| User-visible change | `CHANGELOG.md` under `## Unreleased` | + +Use root files for repository-wide orientation and MkDocs pages for user/developer +guides. Avoid duplicating long sections between `README.md`, `.codex/AGENTS.md`, and +`ARCHITECTURE.md`. + +## Agent Harness + +The documentation harness is intentionally small: + +- `.codex/AGENTS.md` is the compact entrypoint. +- `ARCHITECTURE.md` is the higher-detail system map. +- `docs/testing.md` and this page cover validation and maintenance details. +- `scripts/check_dev_tools.py` verifies that the current shell can run the repository's + development toolchain. +- `scripts/check_docs_harness.py` verifies required headings and relative links from + `.codex/AGENTS.md`. + +Run the harness check with: + +```bash +uv run python scripts/check_docs_harness.py +``` + +The check runs in pre-commit and main CI. If it fails, follow the error message: it +should identify the missing file, heading, or link target. + +## Decision Records + +Use `docs/decisions/` for decisions that affect future architecture or maintenance and +are not obvious from code alone. Start from `docs/decisions/0000-template.md`. + +Keep records short and concrete: + +- status; +- context; +- decision; +- consequences; +- validation or follow-up. + +Do not create decision records for routine implementation details that are already clear +from a focused code change. diff --git a/docs/getting_started.md b/docs/getting_started.md index 05ebeed1..9756d3fa 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -40,6 +40,7 @@ For developers, install the optional `dev` extra so that `pytest`, `ruff`, ```bash uv sync --frozen --extra dev +python3 scripts/check_dev_tools.py ``` You can add other optional application dependencies only when you need them: @@ -62,8 +63,8 @@ using `pytest` are given below. # Default tests for aspects of the Myna Python package installation uv run pytest -# Include optional tests that check application functionality -uv run pytest --apps +# Include optional tests that check external application functionality +uv run pytest -m apps ``` #### Installation of External Dependencies diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..4f95b4fa --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,108 @@ +--- +title: Testing +--- + +# Testing + +Myna uses `pytest` for unit, integration, executable, and example-case tests. Pytest +configuration lives in `pyproject.toml`. + +## Default Test Suite + +The default command is: + +```bash +uv run pytest +``` + +The configured pytest addopts are: + +```bash +--import-mode=importlib -m "not apps" +``` + +That means the default suite excludes tests marked `apps`. Use it for changes to core +workflow logic, database adapters, app wrapper helper code, file contracts, metadata, +and docs-harness Python code that does not require external simulation tools. + +Use focused tests while iterating: + +```bash +uv run pytest tests/test_database.py +uv run pytest tests/test_configure.py +uv run pytest tests/test_component_output_paths.py +uv run pytest tests/test_app_argument_parsing.py +``` + +## Markers + +Markers are declared in `pyproject.toml`: + +| Marker | Meaning | +| --- | --- | +| `apps` | Test requires an external application dependency | +| `examples` | Test runs a workflow case under `examples/cases/` | +| `parallel` | Test needs multiple cores to run | + +External-app tests should only be reported as passing when the needed tools are +installed and available in the current environment. + +## External Application Checks + +Executable availability checks live in `tests/test_executables.py`: + +```bash +uv run pytest -m apps tests/test_executables.py +``` + +These tests currently check for tools such as ExaCA, OpenFOAM/AdditiveFOAM, and +3DThesis on the current `PATH` or active environment. + +Example-case tests live in `tests/test_examples.py`. They copy runnable examples to +temporary case directories under `examples/cases/`, run `myna config`, run `myna run`, +and clean up after execution. + +Useful example commands: + +```bash +uv run pytest -m "examples and not parallel" +uv run pytest -m "examples and parallel" +uv run pytest -m "apps and not examples" +``` + +The example dependency matrix is maintained in the repository file +`examples/cases/README.md`. + +## CI Expectations + +The main CI workflow installs with: + +```bash +uv sync --locked --extra dev +``` + +It then runs: + +```bash +uv run --frozen --no-sync pytest +uv run --frozen --no-sync python scripts/check_docs_harness.py +uv run --frozen --no-sync pylint $(git ls-files '*.py') --fail-under=7.25 +uv run --frozen --no-sync scripts/group_docs.py +uv run --frozen --no-sync mkdocs build --strict +``` + +The external examples CI job runs in `ghcr.io/ornl-mdf/containers/ubuntu:dev`, +installs all extras, checks external executables, and runs example tests in serial and +parallel marker groups. + +## Reporting Skipped Or Unavailable Checks + +When handing off work, include: + +- the exact command run; +- whether it passed or failed; +- the reason if it could not run, such as missing external executable, unavailable + optional extra, container dependency, or network restriction. + +Do not replace a missing external-app test with a default pytest run and call the +external behavior covered. Say what was covered and what remains unverified. diff --git a/scripts/check_dev_tools.py b/scripts/check_dev_tools.py new file mode 100644 index 00000000..3818dd87 --- /dev/null +++ b/scripts/check_dev_tools.py @@ -0,0 +1,246 @@ +"""Check whether the local shell is ready for Myna development. + +The check is designed for humans and coding agents. It uses only the Python standard +library so it can run before the project environment has been synced. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +MIN_PYTHON = (3, 10) +COMMAND_TIMEOUT_SECONDS = 120 +DEV_TOOL_COMMANDS = [ + ["pytest", "--version"], + ["ruff", "--version"], + ["mkdocs", "--version"], + ["pre-commit", "--version"], +] + + +def print_status(kind: str, message: str) -> None: + print(f"[{kind}] {message}") + + +def find_common_uv_candidates() -> list[Path]: + home = Path.home() + candidates = [ + home / ".local" / "bin" / "uv", + home / ".cargo" / "bin" / "uv", + home / "snap" / "code" / "current" / ".local" / "bin" / "uv", + Path("/opt/homebrew/bin/uv"), + Path("/usr/local/bin/uv"), + ] + uv_env = os.environ.get("UV") + if uv_env: + candidates.insert(0, Path(uv_env)) + return [path for path in candidates if path.exists()] + + +def check_current_python(errors: list[str]) -> None: + version = sys.version_info + if version < MIN_PYTHON: + errors.append( + "Current Python is " + f"{version.major}.{version.minor}.{version.micro}; " + f"Myna development requires Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+." + ) + return + print_status( + "ok", + f"current Python is {version.major}.{version.minor}.{version.micro}", + ) + + +def check_uv_on_path(errors: list[str], warnings: list[str]) -> str | None: + uv = shutil.which("uv") + if uv: + print_status("ok", f"uv found on PATH: {uv}") + return uv + + candidates = find_common_uv_candidates() + if candidates: + paths = "\n".join(f" - {path}" for path in candidates) + path_dirs = ":".join(str(path.parent) for path in candidates) + errors.append( + "uv was found in a common install location, but it is not on PATH:\n" + f"{paths}\n" + "Add the containing directory to PATH, for example:\n" + f' export PATH="{path_dirs}:$PATH"' + ) + else: + errors.append( + "uv is not installed or is not discoverable. Install uv, then make sure " + "the uv executable is on PATH. See " + "https://docs.astral.sh/uv/getting-started/installation/." + ) + + warnings.append( + "Non-interactive agent shells may not load the same startup files as an " + "interactive terminal. Verify PATH in the shell that will run validations." + ) + return None + + +def directory_is_writable(path: Path) -> bool: + try: + path.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(dir=path): + pass + except OSError: + return False + return True + + +def check_cache_dir( + env_var: str, + default_path: Path, + fallback_path: str, + errors: list[str], +) -> None: + path = Path(os.environ.get(env_var, default_path)).expanduser() + if directory_is_writable(path): + print_status("ok", f"{env_var or default_path} cache is writable: {path}") + return + + errors.append( + f"{path} is not writable. Set a writable cache directory before running " + f"development commands, for example:\n" + f" export {env_var}={fallback_path}" + ) + + +def run_command( + command: list[str], + errors: list[str], + *, + label: str, + timeout: int = COMMAND_TIMEOUT_SECONDS, +) -> str: + try: + result = subprocess.run( + command, + cwd=REPO_ROOT, + check=False, + capture_output=True, + text=True, + timeout=timeout, + ) + except FileNotFoundError as exc: + errors.append(f"{label} failed because the command was not found: {exc}") + return "" + except subprocess.TimeoutExpired: + errors.append(f"{label} timed out after {timeout} seconds.") + return "" + + output = "\n".join( + part.strip() for part in [result.stdout, result.stderr] if part.strip() + ) + if result.returncode != 0: + errors.append( + f"{label} failed with exit code {result.returncode}:\n{output or '(no output)'}" + ) + return output + + print_status("ok", f"{label}: {output.splitlines()[0] if output else 'passed'}") + return output + + +def command_failed_because_tool_missing(output: str) -> bool: + missing_tool_markers = [ + "No such file or directory", + "Failed to spawn", + "command not found", + "ModuleNotFoundError", + ] + return any(marker in output for marker in missing_tool_markers) + + +def check_project_commands(uv: str, errors: list[str], warnings: list[str]) -> None: + run_command([uv, "--version"], errors, label="uv version") + run_command([uv, "lock", "--check"], errors, label="uv lock --check") + python_output = run_command( + [ + uv, + "run", + "--frozen", + "--no-sync", + "python", + "-c", + "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')", + ], + errors, + label="project Python", + ) + if python_output and not python_output.startswith("3.10."): + warnings.append( + "CI builds docs with Python 3.10. If API-doc generation fails locally, " + "sync a Python 3.10 environment with " + "`uv sync --frozen --extra dev --python 3.10`." + ) + + missing_dev_tools = [] + for tool_command in DEV_TOOL_COMMANDS: + before_error_count = len(errors) + output = run_command( + [uv, "run", "--frozen", "--no-sync", *tool_command], + errors, + label=f"uv run {' '.join(tool_command)}", + ) + if len(errors) > before_error_count and command_failed_because_tool_missing( + output + ): + missing_dev_tools.append(tool_command[0]) + + if missing_dev_tools: + errors.append( + "Development tools are not installed in the project environment: " + + ", ".join(sorted(set(missing_dev_tools))) + + ". From a fresh clone, run:\n" + " uv sync --frozen --extra dev\n" + "If you need CI docs parity, use:\n" + " uv sync --frozen --extra dev --python 3.10" + ) + + +def main() -> int: + errors: list[str] = [] + warnings: list[str] = [] + + check_current_python(errors) + uv = check_uv_on_path(errors, warnings) + check_cache_dir( + "UV_CACHE_DIR", Path.home() / ".cache" / "uv", "/tmp/uv-cache", errors + ) + check_cache_dir( + "PRE_COMMIT_HOME", + Path.home() / ".cache" / "pre-commit", + "/tmp/pre-commit-cache", + errors, + ) + + if uv is not None: + check_project_commands(uv, errors, warnings) + + for warning in warnings: + print_status("warn", warning) + + if errors: + print("\nDevelopment tool check failed. Fix the following:") + for index, error in enumerate(errors, start=1): + print(f"\n{index}. {error}") + return 1 + + print("\nDevelopment tool check passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_docs_harness.py b/scripts/check_docs_harness.py new file mode 100644 index 00000000..084dfc20 --- /dev/null +++ b/scripts/check_docs_harness.py @@ -0,0 +1,114 @@ +"""Validate the repository documentation harness. + +This check intentionally avoids external dependencies so it can run in pre-commit, +CI, and minimal local development environments. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] + +REQUIRED_HEADINGS = { + "ARCHITECTURE.md": [ + "# Architecture", + "## Status", + "## System Purpose", + "## Architectural Goals", + "## Repository Map", + "## Runtime and Packaging Model", + "## Main Concepts and Domain Model", + "## Control Flow", + "## Dependency Boundaries", + "## Extension Points", + "## Testing and Validation Architecture", + "## Documentation Architecture", + "## External Systems and Data", + "## Known Gaps and Follow-Up Work", + ], + ".codex/AGENTS.md": [ + "# AGENTS.md", + "## Project Overview", + "## Start Here", + "## Repository Map", + "## Environment Setup", + "## Common Commands", + "## Testing Guidance", + "## Architecture Rules", + "## Documentation Rules", + "## PR and Commit Guidance", + "## Security and Data Handling", + "## Handoff Checklist", + ], +} + + +def fail(message: str) -> None: + print(f"docs harness check failed: {message}", file=sys.stderr) + raise SystemExit(1) + + +def read_required_file(relative_path: str) -> str: + path = REPO_ROOT / relative_path + if not path.exists(): + fail(f"missing required file: {relative_path}") + return path.read_text(encoding="utf-8") + + +def has_heading(text: str, heading: str) -> bool: + pattern = rf"^{re.escape(heading)}\s*$" + return re.search(pattern, text, flags=re.MULTILINE) is not None + + +def check_required_headings() -> None: + for relative_path, headings in REQUIRED_HEADINGS.items(): + text = read_required_file(relative_path) + for heading in headings: + if not has_heading(text, heading): + fail(f"{relative_path} is missing required heading: {heading}") + + +def iter_markdown_links(text: str) -> list[str]: + return re.findall(r"\[[^\]]+\]\(([^)]+)\)", text) + + +def is_external_link(target: str) -> bool: + return target.startswith(("http://", "https://", "mailto:", "#")) + + +def check_agents_links() -> None: + agents_path = REPO_ROOT / ".codex" / "AGENTS.md" + agents_text = read_required_file(".codex/AGENTS.md") + if "(../ARCHITECTURE.md)" not in agents_text: + fail(".codex/AGENTS.md must link to ../ARCHITECTURE.md") + + for raw_target in iter_markdown_links(agents_text): + target = raw_target.strip() + if is_external_link(target): + continue + target = target.split("#", 1)[0].strip() + if not target: + continue + if target.startswith("<") and target.endswith(">"): + target = target[1:-1] + path = (agents_path.parent / target).resolve(strict=False) + try: + path.relative_to(REPO_ROOT) + except ValueError: + fail(f".codex/AGENTS.md link escapes repository: {raw_target}") + if not path.exists(): + fail(f".codex/AGENTS.md links to missing path: {raw_target}") + + +def main() -> None: + check_required_headings() + check_agents_links() + print("docs harness check passed") + + +if __name__ == "__main__": + main()