# pmmanifest

Generate **PlayMolecule v2 app manifests** — standard JSON Schema (`inputSchema`)
plus an RJSF `uiSchema` — directly from a typed Python function signature, and
preview the resulting GUI locally. The successor to `func2argparse`'s
manifest-generation role (the argparse path is dropped; apps run via
`inputs.json`).

## Why

One typed signature is the single source of truth: it drives the **manifest**
(what the GUI renders), **runtime validation/coercion**, and the **Python API**.
Standard JSON Schema means an off-the-shelf renderer (RJSF) handles nested
objects, arrays-of-objects, enums and `oneOf` — no bespoke widgets.

## Authoring (signature-first)

Apps stay plain annotated functions — no `BaseModel` per function. Plain
`pathlib.Path` is auto-substituted with the path-role type, so most apps need
no rewrite. Presentation rides along in `Annotated[T, UI(...)]`.

```python
from pathlib import Path
from typing import Annotated, Literal
from pmmanifest import UI, func_to_manifest, build_app_manifest

def systembuilder(
    outdir: Path,
    structure: Annotated[Path | None, UI(widget="file")] = None,
    forcefield: Annotated[Literal["GAFF2", "openff-2.3.0"], UI(group="Force field")] = "GAFF2",
):
    """SystemBuilder builds systems for MD simulation."""

entry = func_to_manifest(systembuilder, function="myapp.app.systembuilder")
manifest = build_app_manifest("systembuilder", [entry])
```

## Preview the GUI

```
pmmanifest serve  myapp.app:systembuilder      # live RJSF form at http://127.0.0.1:8000
pmmanifest emit   myapp.app:systembuilder      # print the v2 manifest JSON
```

## Docstring fallback (existing functions work unchanged)

Annotations are *optional*. If a parameter has no `Field(description=...)` / `UI(...)`,
`pmmanifest` reads the function's numpydoc `Parameters` section (func2argparse style) for the
**description**, `choices=` (→ enum/dropdown), and `gui_options=` (→ groups, labels, hidden,
legacy `columns`). The annotation always wins when present. Labels follow the PlayMolecule
rule: the description's first sentence if ≤4 words, else a prettified field name.

```python
def systembuilder(outdir: Path, forcefield: str = "GAFF2"):
    """SystemBuilder builds systems for MD simulation.

    Parameters
    ----------
    outdir : Path
        Output directory to store results
    forcefield : str, choices=("GAFF2", "openff-2.3.0"), gui_options={"group": "Force field"}
        Force field. Used for non-canonical residue parameterization.
    """
    ...
# -> outdir labeled "Output directory" (+tooltip), forcefield a "Force field"-group dropdown.
```

## Flexible path types

A path argument is always a string on the wire and may be a local path **or** a
remote URI (`slpm://`, `app://`, `bck://`). `PMPath` is a permissive tagged
string (`{"type":"string","format":"path"}`), never `pathlib.Path` (which would
corrupt `slpm://x` to `slpm:/x`). File-staging keys off `format:"path"`.

## Shared dialect contract

`detect_dialect`, `iter_path_values`, `set_in` are the v1/v2 coexistence helpers
intended for the execution backends to import (so the contract can't drift).
`detect_dialect` works on a full envelope *or* a bare function dict (autodiscovers
from keys: `inputSchema` ⇒ v2, `params` ⇒ v1).
