diff --git a/.claude/skills/docs-writing/SKILL.md b/.claude/skills/docs-writing/SKILL.md index 1e62e0b4..577d5dce 100644 --- a/.claude/skills/docs-writing/SKILL.md +++ b/.claude/skills/docs-writing/SKILL.md @@ -101,10 +101,10 @@ section, saying why a reader would open it. **The Python API is rendered from the docstrings.** `docs/reference/api.md` holds one `:::` entry per name in `math_spec.__all__`, -and mkdocstrings renders each from its docstring. `docs/static/hooks.py` -renders one page per module under `src/math_spec/`, and puts them in the -Contributing group of the Development section as `Modules`. The prose of both is the docstring rules in -`AGENTS.md`. +for a model writer. `docs/reference/program.md` renders `math_spec.program`, +for whoever builds on the program. mkdocstrings renders both from the +docstrings, so their prose is the docstring rules in `AGENTS.md`. No other +module gets a page: an internal module is read in the source. Mixing kinds is the most common failure. Rationale inside a reference section makes the rules unskimmable, and rules inside an explanation page make the @@ -117,7 +117,7 @@ prints from it. What a consumer does with a spec — the data it attaches, how i solves, what it reads back — is that consumer's page, not this tree's ([what counts as language](../../../docs/about/what-counts-as-language.md)). A rule about a consumer says only what the file guarantees it -([reading a loaded model](../../../docs/reference/reading.md)). +([reading a spec and its program](../../../docs/reference/reading.md)). Answer the two questions before starting. If a page needs two kinds, it is two sections with two headings, or two pages. diff --git a/README.md b/README.md index 30249386..d258e653 100644 --- a/README.md +++ b/README.md @@ -272,8 +272,8 @@ ms.to_typst(spec) # compiles without a TeX toolchain A [symbol table](docs/reference/typeset.md#symbol-tables) gives the names their conventional spelling, as in the first folded block. [Print a model as math](docs/howto/print.md) does the same from a shell. -`to_spec` returns a `Spec`, and `spec.program` the model it builds -([reading a loaded model](docs/reference/reading.md#spec-and-program)). +`to_spec` returns a `Spec`, and `spec.program` its `Program` +([reading a spec and its program](docs/reference/reading.md#spec-and-program)). ## Documentation diff --git a/docs/about/file-and-program.md b/docs/about/file-and-program.md deleted file mode 100644 index 86b6d377..00000000 --- a/docs/about/file-and-program.md +++ /dev/null @@ -1,48 +0,0 @@ - - -# The file and the program - -This page explains why a loaded model is two objects, and which one each tool -reads. You need none of it to write a model. -[Reading a loaded model](../reference/reading.md) is the reference for both. - -```text -file ── to_spec ──▶ Spec ── .program ──▶ Program - │ - └── .expand() ──▶ Spec of the rows ── .program ──▶ Program of the rows -``` - -## Two states - -**A `Spec` is the file as written**, checked against every rule that needs no -data. **A `Program` is what the file means**: every name typed, every operator -resolved to a node, every macro expanded, and each `piecewise:` or `sos:` block -kept as one declaration. - -## Which tool reads which - -| Tool | Reads | Because | -| -------------------------- | ------------------------------------------- | ------------------------------------------------- | -| The typesetter | `spec.program`, or a `Program` handed to it | it prints each curve as the curve the file states | -| `advice` | `spec.program` | its notes are about the model the author wrote | -| An engine that builds rows | the program of `spec.expand()` | a solver takes rows | -| A tool that rewrites files | the `Spec` | only the spec holds the text and the macros | - -## Why the split falls here - -- **A reader after load needs one typed object.** Printing a model needs the - typed trees, the descriptions and the curves together. The program carries - all three, so no reader parses text again or reads two objects. -- **The program keeps the model the author wrote.** A curve is one declaration - to print and one to explain. Its rows are one formulation of it, so the rows - are a second model, which a caller asks for with - [`spec.expand()`](../reference/api.md#math_spec.Spec.expand). -- **The spec keeps the text.** A tool that rewrites a model needs the file as - written: `to_yaml()` writes it back, and `expand()` rewrites it. A tree does - not give the text back. -- **The program does not hold its spec.** Nothing reads the file from a - program, and two objects that own each other form a cycle. A tool handed a - bare `Program` has the model, not the file. diff --git a/docs/index.md b/docs/index.md index 5edb449f..074ae7f3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -183,7 +183,7 @@ call. - [Print a model as math](howto/print.md): LaTeX, Typst or Markdown, from the file alone. - [Check a model without data](howto/check.md): on your machine and in CI. -- [Reading a loaded model](reference/reading.md): for whoever writes an engine +- [Reading a spec and its program](reference/reading.md): for whoever writes an engine or a renderer. ## Install it diff --git a/docs/reference/api.md b/docs/reference/api.md index c76f5ee2..299179a8 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -12,8 +12,8 @@ task. ## Loading -The module `math_spec.program` holds the node and declaration classes of a -loaded model. [Reading a loaded model](reading.md) documents them. +The module `math_spec.program` holds the classes a `Program` is made of. The +[Program API](program.md) documents them. ::: math_spec.to_spec options: diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index ad451ac4..3986e41a 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -11,9 +11,14 @@ its [language page](language/index.md). ## The file and what reads it +**Model** +: The optimisation problem a file states: its dimensions, the data it expects, +its decisions and its rules. A model holds no data. The Python objects that +hold it are named for what they are, a `Spec` or a `Program`. + **Spec** : The file as written, checked: what `to_spec` returns -([reading a loaded model](reading.md#spec-and-program)). +([reading a spec and its program](reading.md#spec-and-program)). **Program** : What the file means, `spec.program`: every name typed, every macro expanded, diff --git a/docs/reference/language/assumptions.md b/docs/reference/language/assumptions.md index e3952034..9898ccef 100644 --- a/docs/reference/language/assumptions.md +++ b/docs/reference/language/assumptions.md @@ -121,5 +121,5 @@ the same _Assumptions_ heading as the written ones. | `curve_breakpoints` | `lp` | each curve has at least two breakpoints | | `curve_contiguous` | a block `points:` | the marked breakpoints are one consecutive run of at least one | -[Reading a loaded model](../reading.md#what-the-data-has-to-satisfy) says how +[Reading a spec and its program](../reading.md#what-the-data-has-to-satisfy) says how a consumer runs them. diff --git a/docs/reference/program.md b/docs/reference/program.md new file mode 100644 index 00000000..c7c09537 --- /dev/null +++ b/docs/reference/program.md @@ -0,0 +1,21 @@ + + +# Program API + +This page documents every name that `math_spec.program` exports: the +declarations, the expression and predicate nodes, and the reports a program +answers. [Reading a spec and its program](reading.md) says how they fit together. + + + +::: math_spec.program + options: + show_root_heading: false + show_root_toc_entry: false + heading_level: 2 + members_order: alphabetical + + diff --git a/docs/reference/reading.md b/docs/reference/reading.md index 42cc7395..58b10b98 100644 --- a/docs/reference/reading.md +++ b/docs/reference/reading.md @@ -3,7 +3,7 @@ SPDX-FileCopyrightText: math-spec contributors SPDX-License-Identifier: CC-BY-4.0 --> -# Reading a loaded model +# Reading a spec and its program This page is for whoever writes an engine that builds models, a renderer, or a checker. A tool reads the model through two objects, `Spec` and `Program`. @@ -14,9 +14,21 @@ A `Spec` holds the file as written: its `macros:`, its descriptions, and a `piecewise:` block as one block. A `Program` holds the model the file builds: every macro expanded, every name typed, every operator resolved to a node, and every dimension and degree rule already checked. A curve stays one curve there -until [`spec.expand()`](#formulations-written-out) writes it out. -[The file and the program](../about/file-and-program.md) says why the two are -split, and which tool reads which. +until [`spec.expand()`](#formulations-written-out) writes it out. The +[Program API](program.md) documents every class a program holds. + +Each tool reads the object that holds what it needs: + +| Tool | Reads | +| -------------------------- | ------------------------------------------- | +| The typesetter | `spec.program`, or a `Program` handed to it | +| `advice` | `spec.program` | +| An engine that builds rows | the program of an expansion | +| A tool that rewrites files | the `Spec`, which alone holds the text | + +The program keeps each curve as the one declaration the file states, so the +typesetter and `advice` read the model the author wrote. A program does not +hold its spec: a tool handed a bare `Program` has the model, not the file. The curve below [expands](language/piecewise.md) into a weight per breakpoint, a convexity row and one row per link: diff --git a/docs/static/hooks.py b/docs/static/hooks.py index d99cd015..f1e6edc8 100644 --- a/docs/static/hooks.py +++ b/docs/static/hooks.py @@ -5,21 +5,15 @@ """Hooks to run when building documentation.""" import re -import tempfile from pathlib import Path import mkdocs.plugins from mkdocs.structure.files import File -TEMPDIR = tempfile.TemporaryDirectory() - -# Add to this list if you want to ignore any source files from the documentation API reference -API_FILES_TO_IGNORE: list[str] = [] - @mkdocs.plugins.event_priority(50) def on_files(files: list, config: dict, **kwargs) -> list: - """Link (1) top-level files to mkdocs files and (2) generate the python API documentation. + """Link the top-level files the site shows, such as the changelog, into the docs. Args: files (list): mkdocs file list. @@ -32,10 +26,6 @@ def on_files(files: list, config: dict, **kwargs) -> list: for file in Path('./resources').glob('**/*.*'): files.append(_new_file(file, config)) files.append(_new_file(Path('./CHANGELOG.md'), config)) - - api_nav = _api_gen(files, config) - _update_nav(api_nav, config) - return files @@ -61,97 +51,6 @@ def _new_file(path: Path, config: dict, src_dir: str = '.') -> File: ) -def _api_gen(files: list, config: dict) -> dict: - """Project Python API generator. - - Args: - files (list): mkdocs file list. - config (dict): mkdocs config dictionary. - - Returns: - dict: Python API navigation tree, to add into the mkdocs `nav` tree. - """ - source_dir = Path(config['watch'][0]) - source_file = source_dir.parts[-1] - api_nav: dict = {'top_level': []} - for filepath in sorted(source_dir.rglob('[!_]*.py')): - rel_filepath = filepath.relative_to(source_dir) - if rel_filepath.as_posix() in API_FILES_TO_IGNORE: - continue - file = _py_to_md(source_file / rel_filepath, api_nav, config) - files.append(file) - return api_nav - - -def _py_to_md(filepath: Path, api_nav: dict, config: dict) -> File: - """Create a markdown file for the API documentation for a given python file in the package source directory. - - Markdown files are stored in a temporary directory, which will be cleaned after mkdocs has finished building the docs. - - Args: - filepath (Path): Path to python file relative to the package source code directory. - api_nav (dict): Nested dictionary to fill with mkdocs navigation entries. - config (Config): mkdocs config dictionary. - - Returns: - File: mkdocs object that links the temp file to the docs directory, ready to be added to the mkdocs file list. - """ - module_parts = filepath.with_suffix('').parts - - module_name = '.'.join(module_parts) - - api_file = 'reference' / filepath.with_suffix('.md') - api_full_filepath = Path(TEMPDIR.name) / api_file - api_full_filepath.parent.mkdir(exist_ok=True, parents=True) - api_full_filepath.write_text(f'::: {module_name}') - - nav_component = {module_name: api_file.as_posix()} - if len(module_parts) > 2: # i.e., in a nested directory - parent_module = '.'.join(module_parts[:2]) - if parent_module not in api_nav: - api_nav[parent_module] = [nav_component] - else: - api_nav[parent_module].append(nav_component) - else: - api_nav['top_level'].append(nav_component) - return _new_file(api_file, config, TEMPDIR.name) - - -def _update_nav(api_nav: dict, config: dict) -> None: - """Append the per-module API pages to the Contributing group of Development, as `Modules`. - - Mkdocs navigation is composed of lists of dictionaries. - Lists nesting defines navigation nesting, dictionary keys are the page names, and values are the pointers to markdown files. - - Args: - api_nav (dict): Python API navigation tree. - config (dict): mkdocs config dictionary (in which `nav` can be found). - """ - modules_nav = {'Modules': [*api_nav.pop('top_level'), *[{k: v} for k, v in api_nav.items()]]} - _get_nav_list(_get_nav_list(config['nav'], 'Development'), 'Contributing').append(modules_nav) - - -def _get_nav_list(nav: list[dict | str], ref: str) -> list: - """Get navigation entry sub-page list. - - Navigation list entries can be dictionaries or strings. - Sub-list entries can then also be dictionaries or strings. E.g., - - ```python - [{"Page Title": ["sub-page-1", {"Sub-Page-2 Title": "sub-page-2"}, ...], ...}] - ``` - - Args: - nav (list[dict | str]): MKdocs `nav` config entry. - ref (str): Page title reference to return the sub-list of. - - Returns: - list: Nav sub-list linked to `ref`. - """ - nav_ref = [idx for idx in nav if isinstance(idx, dict) and set(idx.keys()) == {ref}][0] - return nav_ref[ref] - - #: A fenced block, indented or not — its contents are nobody's to rewrite, and #: a ```math one is the superfences entry's in `mkdocs.yml`. FENCED_BLOCK = re.compile(r'^[ \t]*```.*?^[ \t]*```$', re.DOTALL | re.MULTILINE) @@ -188,13 +87,3 @@ def on_page_markdown(markdown: str, **kwargs) -> str: kept = FENCED_BLOCK.sub(lambda m: spans.append(m[0]) or f'\x00{len(spans) - 1}\x00', markdown) rewritten = GITHUB_INLINE_MATH.sub(lambda m: f'${m["math"]}$', kept) return re.sub(r'\x00(\d+)\x00', lambda m: spans[int(m[1])], rewritten) - - -@mkdocs.plugins.event_priority(-100) -def on_post_build(**kwargs): - """After mkdocs has finished building the docs, remove the temporary directory of markdown files. - - Args: - **kwargs: Automatic MKDocs hook inputs. - """ - TEMPDIR.cleanup() diff --git a/mkdocs.yml b/mkdocs.yml index 7d7b25de..a22413b4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,12 +72,11 @@ nav: # renderer, a checker), whoever changes math-spec itself, and the proofs of # concept: the notation page, which renders the typesetting test model, and # the PyPSA pages. The PyPSA pages stay in `docs/examples/`, where - # `tools/gallery.py` writes them. `docs/static/hooks.py` appends one page per module under - # `src/math_spec/` to Contributing, as `Modules`. + # `tools/gallery.py` writes them. - Development: - Building on math-spec: - - Reading a loaded model: reference/reading.md - - The file and the program: about/file-and-program.md + - Reading a spec and its program: reference/reading.md + - Program API: reference/program.md - What counts as language: about/what-counts-as-language.md - Contributing: - contributing.md diff --git a/src/math_spec/typesetting/__init__.py b/src/math_spec/typesetting/__init__.py index ef5af3aa..bb1e9b28 100644 --- a/src/math_spec/typesetting/__init__.py +++ b/src/math_spec/typesetting/__init__.py @@ -114,7 +114,7 @@ def typeset( Args: model: Anything :func:`math_spec.to_spec` accepts, or a - :class:`~math_spec.program.Program`. A loaded model or a program + :class:`~math_spec.program.Program`. A ``Spec`` or a ``Program`` is rendered as it stands, so printing one model in several formats reads and checks the file once rather than once per format, and a curve prints as the curve it states. Pass ``spec.expand()`` for the rows a solver holds