---
name: bmm-authoring
description: >
  Create or edit an openEHR component's BMM schema, the P_BMM JSON in `computable/BMM/` of a
  `specifications-XX` repo. This skill should be used when the user asks to write a BMM schema from
  scratch, add, rename or remove a class, attribute, function, invariant or enumeration in a
  component's model, set the schema header or includes, or check a hand-edited `.bmm.json`. Not for
  regenerating class tables (use class-generation), spec prose (use content-patterns), or the
  ITS-XML, ITS-BMM, ITS-JSON and ITS-REST repos, which hold no model of their own.
allowed-tools:
  - "Bash(python3 ${CLAUDE_SKILL_DIR}/scripts/check_bmm.py *)"
---

# openEHR BMM Schema Authoring (P_BMM JSON)

A component's BMM schema, `computable/BMM/openehr_<component>_<release>.bmm.json`, is the source of
truth for its classes. The class tables and diagrams in its documents are generated from it (see
`class-generation`), and
[`specifications-ITS-BMM`](https://github.com/openEHR/specifications-ITS-BMM) republishes it as JSON,
ODIN and YAML. Change the model here, and only here.

- **Format reference**: `references/p-bmm-json.md` (header, packages, every class, property, type and
  function kind, what the tables show, spec features the toolchain does not read).
- **Worked example**: `${CLAUDE_SKILL_DIR}/assets/openehr_demo_0.1.0.bmm.json`, one of each construct.
- **Checker**: `${CLAUDE_SKILL_DIR}/scripts/check_bmm.py` (Python 3.8+, standard library only, read-only).

## Where It Applies

Only in a component repo. Check `manifest.json` `id` (or the directory name `specifications-<id>`)
first. In the ITS-XML, ITS-BMM, ITS-JSON and ITS-REST repos, create and edit no BMM schema. Say that
the repo holds no model of its own, and make the change in the component's
`specifications-<COMPONENT>/computable/BMM/` instead. ITS-BMM copies its JSON from there and
generates its ODIN and YAML with its own `make generate`. Running the read-only checker on a file
there is fine.

## Related Skills

- **class-generation**: regenerate and place the class tables after a change
- **authoring**: the chapter `include::` lines for a new or removed class
- **amendment-record**: the entry for a change to published specification text
- **governance**: releases, and the lifecycle state a schema header names
- **content-patterns**: the prose around classes, which the tables do not carry

## Why the Checker Comes Before the Render

`bmm-publisher` exits 0 on most modelling mistakes and renders something else in their place:

- a misspelt or missing `_type` falls back to a single property or a simple type, often of type `Any`;
- an unknown key (`is_mandantory`) is ignored without a message;
- a class missing from `packages` gets no table;
- `{"lower": 2, "upper": 5}` loses its upper limit unless `"upper_unbounded": false` is written out;
- spec constructs it does not read (indexed containers, `ancestor_defs`, `type_ref` value sets) are
  dropped.

A clean run therefore proves little. `check_bmm.py` reports each of these, so run it first, then
render and read the tables.

## Workflow

### Changing a schema

1. Read the header and the classes next to the one you are changing, and follow their idiom. Run the
   checker once before editing (step 4) and save its output to a scratch file: the published schemas
   carry findings of their own, and comparing that output with the one after your change separates
   theirs from yours.
2. Make the change, using the format reference. Ground every class, attribute and function name in
   the specification text being written or in the user's instruction; never invent one.
3. Increase the build number in `schema_revision` (`1.3.0.2` → `1.3.0.3`). If the release is already
   published, whether the change belongs in it or in a new release (a new `rm_release`, file and
   schema id, which the `includes` of dependent schemas then name) is a decision for the user; see
   `governance`.
4. Run the checker, passing each included schema with `-d` (from its sibling clone):
   ```bash
   python3 ${CLAUDE_SKILL_DIR}/scripts/check_bmm.py computable/BMM/openehr_rm_1.2.0.bmm.json \
     -d ../specifications-BASE/computable/BMM/openehr_base_1.3.0.bmm.json
   ```
   Save the output again and `diff` it with the step 1 output; fix every ERROR and WARNING your
   change added. A non-zero "N name(s) not checked" in the summary (with an "incomplete" notice)
   means an included schema was not loaded: the exit status is 3 when there are no errors, 1 when
   there are. If the sibling clone is missing, say the check is incomplete; never report such a run
   as clean.
5. Regenerate the class tables (`class-generation`) and read the table of every class you touched.
   Expect the gaps listed under "What the class tables show" in the reference, such as a container's
   member count or a generic parameter's constraint.
6. A new class or package needs its `include::` line in a chapter, and a removed class loses its line
   (`authoring`). Each table is named after the first four parts of its class's package path and the
   class name in lower case: `LOCATABLE` in `org.openehr.rm.common` → `archetyped` gives
   `org.openehr.rm.common.locatable.adoc`, included as `{pkg}locatable.adoc`.
7. Add the amendment-record entry. When asked to commit, commit the BMM change on its own, as the
   repo's `AGENTS.md` says; leave regenerated tables and HTML unstaged unless the task is to refresh
   them.

### Writing a schema from scratch

1. If `computable/BMM/` already holds an empty schema for the component (the `scaffold` skill writes
   one), fill that in; replace its single root package `org.openehr.<schema_name>` as step 3 says,
   and confirm its `schema_author` and `schema_lifecycle_state` with the user. Otherwise copy
   `${CLAUDE_SKILL_DIR}/assets/openehr_demo_0.1.0.bmm.json` to `computable/BMM/<schema id>.bmm.json`
   and replace its content. Set the header from the reference's header table: `schema_name` is the
   lower-case component, `rm_release` the release, `schema_revision` `<rm_release>.1`. Ask the user
   for `schema_author` rather than guessing it.
2. Include BASE (`"includes": {"openehr_base_<version>": {"id": "openehr_base_<version>"}}`) and use
   its types (`Any`, `String`, `List`, `Hash`, `HIER_OBJECT_ID`, …). Do not redefine them;
   `primitive_types` belongs to BASE only.
3. Name top-level packages `org.openehr.<schema_name>.<package>`, one for each `:pkg:` the
   component's `master.adoc` files set (`org.openehr.<schema_name>.<package>.`): RM has
   `org.openehr.rm.common`, `org.openehr.rm.ehr` and others, and its `ehr` document sets two; TERM
   has the one package `org.openehr.term.terminology`. Group the classes in sub-packages as the
   chapters do, at most four package levels deep.
4. Add the classes, then continue from step 4 of "Changing a schema".

## Quick Reference

| To model | Write |
|----------|-------|
| `uid: HIER_OBJECT_ID`, mandatory | `P_BMM_SINGLE_PROPERTY`, `"type": "HIER_OBJECT_ID"`, `"is_mandatory": true` |
| `items: List<ITEM>`, never empty | `P_BMM_CONTAINER_PROPERTY`, `type_def` `{"container_type": "List", "type": "ITEM"}`, `cardinality` `{"lower": 1, "upper_unbounded": true}`, `"is_mandatory": true` |
| `ranks: List<Integer>`, 2 to 5 members | `cardinality` `{"lower": 2, "upper": 5, "upper_unbounded": false}` |
| `index: Hash<String, ITEM>` | `P_BMM_GENERIC_PROPERTY`, `type_def` `{"root_type": "Hash", "generic_parameters": ["String", "ITEM"]}` |
| `content: T` in `BOX<T: ITEM>` | class `generic_parameter_defs` `{"T": {"name": "T", "conforms_to_type": "ITEM"}}`; property `P_BMM_SINGLE_PROPERTY_OPEN`, `"type": "T"` |
| `List<BOX<GROUP>>` | container `type_def` holding a nested `type_def` with `"_type": "P_BMM_GENERIC_TYPE"` |
| `B` inherits `A` | `"ancestors": ["A"]` |
| `B<T>` inherits `A<T>` (open binding) | `"ancestors": ["A"]`, and redeclare `T` in `B`'s `generic_parameter_defs` |
| `B` inherits `A<Integer>` (closed binding) | `"ancestors": ["A"]` only, and state the binding in `documentation` |
| String enumeration | `"_type": "P_BMM_ENUMERATION_STRING"`, `"ancestors": ["String"]`, `item_names`, `item_documentations` |
| Function | `functions` entry: `parameters`, `result` (a type object with its `_type`), `pre_conditions`, `post_conditions` |
| Invariant | `"invariants": {"Name_valid": "not name.is_empty()"}` |

## The Checker

- Each finding is `ERROR`, `WARNING` or `INFO`, with a JSON path. ERROR means bmm-publisher fails,
  reads something other than what the file says (a misspelt `is_mandatory` included), or a generic
  argument does not conform to its parameter's `conforms_to_type`. WARNING means a broken convention
  or content the publisher ignores. INFO lists what could not be checked, typically type names from
  an included schema that was not passed with `-d`.
- Exit status: 0 complete and clean; 1 errors found (with `--strict`, warnings count too); 3 no
  errors, but names from an included schema not loaded with `-d` went unchecked; 2 a file is
  unreadable, not UTF-8 JSON, nested too deeply, or (for `-d`) not a BMM schema. The summary line
  always gives the unchecked count.
- The published schemas carry findings of their own. At the time of writing, AM 2.4.0 has ten errors
  (lists without an element type in `P_ARCHETYPE_SLOT`, and `FUNCTION` arguments in reverse order in
  `c_conforms_to`) and LANG's BMM3 overlay two. Report findings outside your change to the user; do
  not fix them as a side effect.
- If `python3` is missing, say so and report the schema as unchecked; review the change by hand
  against the reference's lists before rendering. In a tool that does not expand
  `${CLAUDE_SKILL_DIR}`, use the directory that holds this SKILL.md.

## Guardrails

- **Edit the JSON in place.** The files mix PHP-style escapes (`\/`, `\u2260`) with raw characters
  from later edits, so loading and re-serialising a file rewrites lines you did not touch. Keep the
  4-space indentation and key order, and check that `git diff` shows only your change.
- **Edit only `computable/BMM/` in the component repo.** Never the ITS-BMM copies, the generated
  `docs/UML/classes` tables, or the schemas bundled in `bmm-publisher`.
- **Keys equal names.** Every entry of a map of named things (packages, classes, properties,
  functions, parameters, constants, generic parameters) is keyed by its own `name`.
- **Use the forms bmm-publisher reads** (see the reference's table of unsupported specification
  features) and describe in `documentation` what they cannot express.
