From 21e68071335c7c41c36f2ad5908e725978d63be5 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 8 Oct 2026 14:52:35 +0200 Subject: [PATCH 1/4] fix(openehr-specs): generate class tables from the repo's BMM by path class-generation and regen-classes mounted no schema files and passed bundled schema ids, so bmm-publisher rendered its own, older copy of the BMM without any error; the suggested mount over /app/resources hid the bundled dependencies (exit 1). Both now mount the component repo's BMM and its sibling dependencies under /in/ and pass paths; regen-classes stops when a file is missing. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++ .../skills/class-generation/SKILL.md | 39 ++++++++++++------- .../references/bmm-publisher.md | 33 +++++++++++----- .../skills/regen-classes/SKILL.md | 30 +++++++++----- 4 files changed, 72 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b016a6..d7a5499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Notable changes to the plugins in this repository. The format follows [Keep a Ch ## Unreleased +### Changed + +- `class-generation` and `regen-classes` generate from the component repo's own BMM file and its sibling dependencies, mounted under `/in/` and passed by path. A bare schema id loads the copy bundled in the `bmm-publisher` image, which lags the repos and rendered an older model without any error; mounting a directory over `/app/resources` hid the bundled dependencies. `regen-classes` stops when a file is missing instead of falling back to an id. + ## openehr-specs 0.4.0 (2026-10-07) ### Added diff --git a/plugins/openehr-specs/skills/class-generation/SKILL.md b/plugins/openehr-specs/skills/class-generation/SKILL.md index 246edcf..f0cad58 100644 --- a/plugins/openehr-specs/skills/class-generation/SKILL.md +++ b/plugins/openehr-specs/skills/class-generation/SKILL.md @@ -57,9 +57,11 @@ To run from a cloned `bmm-publisher` checkout instead, see "Local development" i | `legacy-adoc` | Flat per-class definition tables for the legacy `docs/UML/classes` layout; `-o ` sets the target, e.g. `/app/output/UML/classes` | | `asciidoc` (`adoc`) | Class, effective (flattened), and definition tables plus rendered SVG class/package diagrams (current layout) | -Pass schema id(s) **without** the `.bmm.json` suffix (e.g. `openehr_base_1.3.0`), or `all`. Add a -repeatable `-d ` for each dependency schema, loaded for cross-references only (not exported), -and `-v`/`-vv` for progress or detailed logging. The other commands (`plantuml`, `embed-svg`, `yaml`, +Pass the **component repo's own BMM file** as a path, mounted into the container under `/in/`, and a +repeatable `-d` with the path of each dependency schema, loaded for cross-references only (not +exported). A bare schema id (`openehr_base_1.3.0`) or `all` loads the copy **bundled in the image**, +which lags the repos: a run from an id exits 0 but renders the older model, so use ids only to +reproduce published output. Add `-v` to log each file read (confirm it is `/in/…`) or `-vv` for detail. The other commands (`plantuml`, `embed-svg`, `yaml`, `split-json`, `odin`) and all options are in `references/bmm-publisher.md`. ## Output Layouts and How Specs Consume Them @@ -93,20 +95,27 @@ image::ROOT:uml/classes/COMPOSITION.svg[] ## Typical Workflow -1. **Identify the schema** as `openehr__`, e.g. `openehr_base_1.3.0`, - `openehr_rm_1.2.0`. Ask the user for the component and release when they are not stated; - `references/bmm-publisher.md` lists the bundled ids. +1. **Locate the schema files.** Work from the component repo's root. The schema is + `computable/BMM/openehr__.bmm.json`; each dependency comes from its sibling clone + (RM, AM, LANG and TERM classes refer to BASE types: + `../specifications-BASE/computable/BMM/openehr_base_.bmm.json`). When a file is missing, say + which and stop rather than falling back to a bundled id. 2. **Choose the command by layout** (see above): `legacy-adoc` for the legacy layout, `asciidoc` for the current one. -3. **Generate** into a working directory, mapping ownership to the host user: +3. **Generate** into a temporary directory, mounting the files read-only and mapping ownership to the + host user (RM shown, with its BASE dependency): ```bash + OUT=$(mktemp -d) docker run --rm --user $(id -u):$(id -g) \ - -v ./out:/app/output \ - ghcr.io/openehr/bmm-publisher asciidoc -v openehr_rm_1.2.0 -d openehr_base_1.3.0 + -v "$PWD/computable/BMM/openehr_rm_1.2.0.bmm.json":/in/openehr_rm_1.2.0.bmm.json:ro \ + -v "$PWD/../specifications-BASE/computable/BMM/openehr_base_1.3.0.bmm.json":/in/openehr_base_1.3.0.bmm.json:ro \ + -v "$OUT":/app/output \ + ghcr.io/openehr/bmm-publisher asciidoc -v /in/openehr_rm_1.2.0.bmm.json -d /in/openehr_base_1.3.0.bmm.json ``` - For the legacy layout, run `legacy-adoc -o /app/output/UML/classes -d ` - instead; the tables land in `./out/UML/classes`. -4. **Place** the output. Legacy layout: copy `./out/UML/classes/*.adoc` into the component's + For the legacy layout, run `legacy-adoc -v -o /app/output/UML/classes /in/.bmm.json -d + /in/.bmm.json` instead; the tables land in `$OUT/UML/classes`. Without the dependency, + links to its types come out as `link:/classes/`. +4. **Place** the output. Legacy layout: copy `$OUT/UML/classes/*.adoc` into the component's `docs/UML/classes/`. Current layout: ask the user how the component wires the `output/Adoc//` content. Ask before overwriting files in a `specifications-XX` repo, and do not commit. @@ -120,7 +129,9 @@ the user can run `/openehr-specs:publish `. ## Guardrails - **Generated output is never hand-edited.** Fix the BMM schema and regenerate. +- **Use the repo's BMM, not the bundled copy.** Mount the files under `/in/` and pass their paths. Do not + mount a directory over `/app/resources`: it hides the bundled schemas, so a dependency named by id is + then missing and the run exits 1. - **`resources/*.bmm.json` is upstream input** in a cloned `bmm-publisher` checkout: do not modify it - unless the task is specifically to change the model. With the Docker image, mount your own schemas - with `-v ./my-schemas:/app/resources` instead. + unless the task is specifically to change the model. - Provide cross-referenced dependency schemas with `-d` so type links resolve (e.g. RM depends on BASE). diff --git a/plugins/openehr-specs/skills/class-generation/references/bmm-publisher.md b/plugins/openehr-specs/skills/class-generation/references/bmm-publisher.md index 434b8c0..74da9f4 100644 --- a/plugins/openehr-specs/skills/class-generation/references/bmm-publisher.md +++ b/plugins/openehr-specs/skills/class-generation/references/bmm-publisher.md @@ -34,20 +34,24 @@ From each BMM schema the tool can produce: ## Arguments and options -- **Positional**: one or more schema ids **without** the `.bmm.json` extension (e.g. - `openehr_rm_1.2.0`), or literal `.bmm.json` paths, or `all` to process every schema in the input - directory. -- **`-d `** (repeatable): a dependency schema id or path, loaded for cross-reference +- **Positional**: one or more literal `.bmm.json` paths (the normal case: the component repo's own + schema, mounted into the container), or schema ids **without** the `.bmm.json` extension (e.g. + `openehr_rm_1.2.0`), which load the copy bundled in the image, or `all` for every bundled schema. + The bundled copies lag the repos, so a run from an id can render an older model without any error. +- **`-d `** (repeatable): a dependency schema path or id, loaded for cross-reference resolution only — **not** exported. Accepted by `asciidoc`, `legacy-adoc`, and `plantuml`. Example: - RM references BASE types, so generate RM with `-d openehr_base_1.3.0`. + RM references BASE types, so generate RM with `-d /in/openehr_base_1.3.0.bmm.json` (the sibling BASE + clone's file, mounted). Without it, links to BASE types come out as `link:/classes/`. - **`-o `** (`legacy-adoc` only): output directory for the class `.adoc` files. Default: `/legacy-adoc/` per schema. - **`-v`** / **`-vv`**: progress output / detailed file-write logging. ## Input / output -- **Input**: BMM schemas in `resources/` (`.bmm.json`, shipped with the image). Mount your own with - `-v ./my-schemas:/app/resources`. +- **Input**: `.bmm.json` files mounted into the container and passed by path (recommended: + `-v "$PWD/computable/BMM/.bmm.json":/in/.bmm.json:ro`), or the schemas bundled in + `resources/`, named by id. Mounting a directory over `/app/resources` replaces the bundled set, so a + dependency named by id must then be in that directory too, or the run exits 1. - **Output**: artefacts in `output/` — mount a volume to retrieve them: `-v ./out:/app/output`. - **`BMM_OUTPUT_DIR`** env var overrides the output path: `-e BMM_OUTPUT_DIR=/data/out`. @@ -67,20 +71,29 @@ shipped by a given image tag. ## Invocation examples (Docker) ```bash +# From a component repo's root: its own BMM plus the sibling BASE dependency, by path (the normal case) +docker run --rm --user $(id -u):$(id -g) \ + -v "$PWD/computable/BMM/openehr_rm_1.2.0.bmm.json":/in/openehr_rm_1.2.0.bmm.json:ro \ + -v "$PWD/../specifications-BASE/computable/BMM/openehr_base_1.3.0.bmm.json":/in/openehr_base_1.3.0.bmm.json:ro \ + -v ./out:/app/output \ + ghcr.io/openehr/bmm-publisher legacy-adoc -v -o /app/output/UML/classes \ + /in/openehr_rm_1.2.0.bmm.json -d /in/openehr_base_1.3.0.bmm.json + # All bundled schemas → AsciiDoc tables + SVG diagrams docker run --rm -v ./out:/app/output ghcr.io/openehr/bmm-publisher asciidoc all -# Single schema with a dependency, host-owned output, verbose +# Bundled schemas by id: reproduces published output, may lag the repos docker run --rm --user $(id -u):$(id -g) \ -v ./out:/app/output \ ghcr.io/openehr/bmm-publisher asciidoc -v openehr_rm_1.2.0 -d openehr_base_1.3.0 -# Legacy per-class tables -> ./out/UML/classes (copy into the spec repo's docs/UML/classes) +# Legacy per-class tables of the bundled BASE by id -> ./out/UML/classes (to compare with published output; +# for a spec repo, use the by-path example above) docker run --rm --user $(id -u):$(id -g) \ -v ./out:/app/output \ ghcr.io/openehr/bmm-publisher legacy-adoc -o /app/output/UML/classes openehr_base_1.3.0 -# Your own BMM schemas +# A directory of your own schemas replacing the bundled set (it must hold every dependency too) docker run --rm \ -v ./my-schemas:/app/resources \ -v ./out:/app/output \ diff --git a/plugins/openehr-specs/skills/regen-classes/SKILL.md b/plugins/openehr-specs/skills/regen-classes/SKILL.md index 76070b9..a5c0637 100644 --- a/plugins/openehr-specs/skills/regen-classes/SKILL.md +++ b/plugins/openehr-specs/skills/regen-classes/SKILL.md @@ -1,7 +1,7 @@ --- name: regen-classes description: Regenerate BMM-derived class tables and UML diagrams for a component via bmm-publisher -argument-hint: " [-d ...]" +argument-hint: " [-d ...]" allowed-tools: - "Bash(docker --version)" - "Bash(docker run * ghcr.io/openehr/bmm-publisher *)" @@ -10,26 +10,36 @@ allowed-tools: disable-model-invocation: true --- -Regenerate the class tables and UML diagrams for `$ARGUMENTS` with `bmm-publisher`. The arguments are a -schema id without `.bmm.json` (for example `openehr_rm_1.2.0`) plus any `-d ` flags -(RM depends on BASE: `openehr_rm_1.2.0 -d openehr_base_1.3.0`). The layouts and the placement into +Regenerate the class tables and UML diagrams for `$ARGUMENTS` with `bmm-publisher`, from the BMM files in +the component repo at the current directory, not from the copies bundled in the image (they lag the +repos, and a run from a bundled id exits 0 while rendering the older model). The arguments are a schema +id without `.bmm.json` (for example `openehr_rm_1.2.0`) plus any `-d ` flags (RM +depends on BASE: `openehr_rm_1.2.0 -d openehr_base_1.3.0`). The layouts and the placement into `docs/UML/` are in the `openehr-specs:class-generation` skill; this wrapper covers generation only. If `$ARGUMENTS` is empty, ask for the schema id and stop. 1. Run `docker --version`. If Docker is missing, say it is required (the image and its dev container both run on it) and stop. -2. Write to `./out` unless the user names another target. Ask before writing into a +2. Map each id to its file and Glob that it exists: the schema to `computable/BMM/.bmm.json`, and a + dependency `openehr__` to the sibling clone's + `../specifications-/computable/BMM/.bmm.json` (component upper-cased). If one is + missing, name it and stop; never fall back to the bundled id. +3. Write to `./out` unless the user names another target. Ask before writing into a `specifications-XX` repo's `docs/UML/`. -3. Pick the command by layout (step 2 of the `class-generation` workflow): `legacy-adoc -o +4. Pick the command by layout (step 2 of the `class-generation` workflow): `legacy-adoc -o /app/output/UML/classes` when the spec's chapters contain `include::{uml_export_dir}/classes/` - (the tables land in `./out/UML/classes`), otherwise `asciidoc`. Generate, mapping output ownership - to the host user: + (the tables land in `./out/UML/classes`), otherwise `asciidoc`. Mount each file read-only under + `/in/`, pass the paths, and map output ownership to the host user (RM with BASE shown): ```bash docker run --rm --user $(id -u):$(id -g) \ + -v "$PWD/computable/BMM/openehr_rm_1.2.0.bmm.json":/in/openehr_rm_1.2.0.bmm.json:ro \ + -v "$PWD/../specifications-BASE/computable/BMM/openehr_base_1.3.0.bmm.json":/in/openehr_base_1.3.0.bmm.json:ro \ -v ./out:/app/output \ - ghcr.io/openehr/bmm-publisher asciidoc -v $ARGUMENTS + ghcr.io/openehr/bmm-publisher asciidoc -v /in/openehr_rm_1.2.0.bmm.json -d /in/openehr_base_1.3.0.bmm.json ``` -4. List what was produced (`classes/`, `effective/`, diagram SVGs). Do not commit. Generated files are + Add one mount and one `-d /in/.bmm.json` per dependency. The `-v` log lists each file read: + check that every one is under `/in/`. +5. List what was produced (`classes/`, `effective/`, diagram SVGs). Do not commit. Generated files are never hand-edited: change the BMM and regenerate. To place the output, follow the `class-generation` skill. From c01feeb0fc315220072f89417fa1501591f63089 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 8 Oct 2026 14:52:35 +0200 Subject: [PATCH 2/4] feat(openehr-specs): scaffold revision 2 with a BASE dependency and repo tooling AGENTS.md build region: no longer claims the repo has no build tooling, and the class-table command loads the sibling BASE BMM with -d (new variable base_bmm_schema_id, inferred from the specifications-BASE clone), so links to BASE types resolve. New repos get a TODO(scaffold) marker for their own tooling; .claude/CLAUDE.md and the skill's rules say repo guidance goes into AGENTS.md outside the regions; first_release is confirmed only when manifest.json is created. Upgrade tests now follow the latest revision instead of assuming revision 1. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 + .../openehr-specs/skills/scaffold/SKILL.md | 6 +- .../skills/scaffold/assets/template-set.json | 13 ++- .../scaffold/assets/templates/agents-md.tmpl | 22 ++++- .../scaffold/assets/templates/claude-md.tmpl | 2 +- .../skills/scaffold/migrations/0002.json | 10 ++ .../skills/scaffold/references/maintaining.md | 9 +- .../skills/scaffold/scripts/scaffold.py | 13 +++ scripts/test_scaffold.py | 95 +++++++++++++++---- 9 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 plugins/openehr-specs/skills/scaffold/migrations/0002.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a5499..2af0eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Notable changes to the plugins in this repository. The format follows [Keep a Ch ### Changed - `class-generation` and `regen-classes` generate from the component repo's own BMM file and its sibling dependencies, mounted under `/in/` and passed by path. A bare schema id loads the copy bundled in the `bmm-publisher` image, which lags the repos and rendered an older model without any error; mounting a directory over `/app/resources` hid the bundled dependencies. `regen-classes` stops when a file is missing instead of falling back to an id. +- `scaffold` template set revision 2: the `AGENTS.md` build region no longer says the repo has no build tooling, and its class-table command loads the sibling BASE BMM with `-d` (new variable `base_bmm_schema_id`, inferred from the `specifications-BASE` clone), so links to BASE types resolve. A new `AGENTS.md` gets a `TODO(scaffold)` marker for the repo's own tooling, and `.claude/CLAUDE.md` says repo guidance belongs in `AGENTS.md` outside the regions. Existing repos upgrade on the next `/openehr-specs:scaffold` run. +- `scaffold` asks to confirm `first_release` only when it creates `manifest.json`, the one file that uses it, and its rules say where `/init`-style guidance goes. ## openehr-specs 0.4.0 (2026-10-07) diff --git a/plugins/openehr-specs/skills/scaffold/SKILL.md b/plugins/openehr-specs/skills/scaffold/SKILL.md index b814bbb..ece446d 100644 --- a/plugins/openehr-specs/skills/scaffold/SKILL.md +++ b/plugins/openehr-specs/skills/scaffold/SKILL.md @@ -38,7 +38,8 @@ AsciiDoc-specific. 2. **Variables.** When `status` is `needs_input`, ask for each missing or invalid variable, then plan again with `--var name=value`. A variable whose source is `default` or starts with `inferred` is a guess. Ask the user to confirm the ones that matter (`license`, `jira_project`, `bmm_schema_id`, - `default_branch`, `first_release`, `published_url`) and list the rest in one line. Never invent a + `base_bmm_schema_id`, `default_branch`, `published_url`, and `first_release` only when the plan creates + `manifest.json`, the one file that uses it) and list the rest in one line. Never invent a title, a Jira key or a filter id. Values the user confirmed earlier are recorded in `.claude/scaffold.json` and kept; a recorded default is not, so a BMM schema added later shows up as a new guess to confirm. `--overwrite` and `--pin` accept only the file ids and `agents:` @@ -92,6 +93,9 @@ AsciiDoc-specific. `pinned` in `.claude/scaffold.json`). Changing what a region says for every repo is a change to the plugin's template set. - Run the skill again later to upgrade: the recorded revision is compared with the latest. +- Repository-specific guidance, such as what `/init` would write into a root `CLAUDE.md`, goes into + `AGENTS.md` outside the regions. `.claude/CLAUDE.md` only imports `AGENTS.md`; a second `CLAUDE.md` + would duplicate it. To change the template set, add a revision, or understand a file strategy, read `${CLAUDE_SKILL_DIR}/references/maintaining.md`. diff --git a/plugins/openehr-specs/skills/scaffold/assets/template-set.json b/plugins/openehr-specs/skills/scaffold/assets/template-set.json index 01250ec..c295953 100644 --- a/plugins/openehr-specs/skills/scaffold/assets/template-set.json +++ b/plugins/openehr-specs/skills/scaffold/assets/template-set.json @@ -1,12 +1,18 @@ { "id": "openehr-specs/scaffold", - "revision": 1, + "revision": 2, "revisions": [ { "revision": 1, "released": "2026-10-07", "summary": "Initial set: AGENTS.md (plugin, build, conventions regions), .claude/CLAUDE.md and settings.json, manifest.json, .gitignore, .asciidoctorconfig, LICENSE, README.md.", "digest": "27be97e72c5c80329ed51091f36c7db81bae89732bcfa133ea7203f0b2a697ae" + }, + { + "revision": 2, + "released": "2026-10-08", + "summary": "AGENTS.md build region: no longer claims the repo has no build tooling, and the class-table command loads the sibling BASE BMM with -d (new variable base_bmm_schema_id); a TODO(scaffold) marker asks new repos to describe their own tooling. .claude/CLAUDE.md says where repo guidance goes.", + "digest": "e973f4d599169188dcef2ae965e6f642ad58f3cea9d09ef930e923a4ca54e5b7" } ], "variables": { @@ -83,6 +89,11 @@ "pattern": "^$|^openehr_[a-z0-9_]+_[0-9]+\\.[0-9]+\\.[0-9]+$", "description": "BMM schema id (openehr_base_1.3.0) when the repo holds computable/BMM/.bmm.json; empty otherwise. Switches on the class-generation sections." }, + "base_bmm_schema_id": { + "default": "", + "pattern": "^$|^openehr_base_[0-9]+\\.[0-9]+\\.[0-9]+$", + "description": "BASE BMM schema id (openehr_base_1.3.0) that the component's classes refer to, read from the sibling specifications-BASE clone; loaded with -d when the class tables are regenerated. Empty for BASE itself and for repos without a BMM." + }, "documents": { "default": "none yet (add them to `manifest.json`)", "description": "Published document ids, read from manifest.json when present." diff --git a/plugins/openehr-specs/skills/scaffold/assets/templates/agents-md.tmpl b/plugins/openehr-specs/skills/scaffold/assets/templates/agents-md.tmpl index 2951124..af53f02 100644 --- a/plugins/openehr-specs/skills/scaffold/assets/templates/agents-md.tmpl +++ b/plugins/openehr-specs/skills/scaffold/assets/templates/agents-md.tmpl @@ -44,7 +44,7 @@ The plugin carries the spec-authoring know-how — **prefer its skills/agents ov ## Build tool invocation -No build tooling lives in this repo. All `specifications-XX` repos, including `specifications-AA_GLOBAL` (boilerplate, references), are cloned as siblings under one parent directory; Docker is the only prerequisite. Run from that parent directory: +Docker is all you need to render the documents{{#if bmm_schema_id}} and regenerate the class tables{{/if}}. All `specifications-XX` repos, including `specifications-AA_GLOBAL` (boilerplate, references), are cloned as siblings under one parent directory. Render from that parent directory: ```bash # render HTML. The image is published from specifications-AA_GLOBAL; its entrypoint passes -q @@ -58,17 +58,35 @@ The build prints `generated ` and exits 0 even when includes are missing, ```bash # regenerate class tables (NEVER hand-edit docs/UML/classes/*.adoc) — run from this repo's root. # Pass the repo BMM by PATH: a bare schema id ({{bmm_schema_id}}) uses the image's bundled copy, which lags this repo. +{{/if}} +{{#if base_bmm_schema_id}} +# {{component}} classes refer to BASE types: load the sibling BASE BMM with -d, or their links come out as /classes/. +{{/if}} +{{#if bmm_schema_id}} OUT=$(mktemp -d) docker run --rm --user $(id -u):$(id -g) \ -v "$PWD/computable/BMM/{{bmm_schema_id}}.bmm.json":/in/{{bmm_schema_id}}.bmm.json:ro \ +{{/if}} +{{#if base_bmm_schema_id}} + -v "$PWD/../specifications-BASE/computable/BMM/{{base_bmm_schema_id}}.bmm.json":/in/{{base_bmm_schema_id}}.bmm.json:ro \ +{{/if}} +{{#if bmm_schema_id}} -v "$OUT":/out \ - ghcr.io/openehr/bmm-publisher legacy-adoc /in/{{bmm_schema_id}}.bmm.json -o /out + ghcr.io/openehr/bmm-publisher legacy-adoc \ +{{/if}} +{{#if base_bmm_schema_id}} + -d /in/{{base_bmm_schema_id}}.bmm.json \ +{{/if}} +{{#if bmm_schema_id}} + /in/{{bmm_schema_id}}.bmm.json -o /out # then diff "$OUT" against docs/UML/classes and copy over the tables you changed ``` To change a class/attribute/function/invariant, edit the BMM schema and regenerate — never touch the generated tables (see skill `openehr-specs:class-generation`). {{/if}} + + {{#if bmm_schema_id}} ## Gotchas diff --git a/plugins/openehr-specs/skills/scaffold/assets/templates/claude-md.tmpl b/plugins/openehr-specs/skills/scaffold/assets/templates/claude-md.tmpl index 5e68034..f62b99c 100644 --- a/plugins/openehr-specs/skills/scaffold/assets/templates/claude-md.tmpl +++ b/plugins/openehr-specs/skills/scaffold/assets/templates/claude-md.tmpl @@ -2,6 +2,6 @@ Guidance for Claude Code in this repository (a specification repo: AsciiDoc{{#if bmm_schema_id}} and BMM{{/if}} sources, not software). -All repository guidance for AI assistants is maintained in the root-level `AGENTS.md`: +All repository guidance for AI assistants is maintained in the root-level `AGENTS.md`. Add repository-specific guidance there, outside the `openehr-scaffold` regions (they are managed by `/openehr-specs:scaffold`), rather than in another `CLAUDE.md`: @../AGENTS.md diff --git a/plugins/openehr-specs/skills/scaffold/migrations/0002.json b/plugins/openehr-specs/skills/scaffold/migrations/0002.json new file mode 100644 index 0000000..6957d15 --- /dev/null +++ b/plugins/openehr-specs/skills/scaffold/migrations/0002.json @@ -0,0 +1,10 @@ +{ + "from": 1, + "to": 2, + "steps": [ + { + "op": "note", + "text": "AGENTS.md: if this repo has its own tooling (scripts, generators, a Makefile), describe what it produces and how to run it in a section outside the openehr-scaffold regions. New installs get a TODO(scaffold) marker for this; existing files do not, because text outside the regions belongs to the repo." + } + ] +} diff --git a/plugins/openehr-specs/skills/scaffold/references/maintaining.md b/plugins/openehr-specs/skills/scaffold/references/maintaining.md index d228839..250434c 100644 --- a/plugins/openehr-specs/skills/scaffold/references/maintaining.md +++ b/plugins/openehr-specs/skills/scaffold/references/maintaining.md @@ -16,8 +16,7 @@ scaffold/ └── scripts/scaffold.py # plan, apply, render, diff, check, seal ``` -`migrations/` does not exist yet, because git does not track an empty directory. Create it with the first -migration file. +`migrations/` holds one file per revision after the first, starting with `0002.json`. From the repo root: @@ -58,7 +57,8 @@ descriptor as confirmed, then inference, then a recorded default, then the defau names and empty required values are rejected. - Inference reads `manifest.json`, `.asciidoctorconfig`, the git remote, the directory name, - `computable/BMM`, `origin/HEAD` (for `default_branch`) and the `LICENSE` text. + `computable/BMM`, `origin/HEAD` (for `default_branch`), the `LICENSE` text, and the sibling + `specifications-BASE/computable/BMM` (for `base_bmm_schema_id`, the highest version; never for BASE itself). - The descriptor lists which recorded values were only defaults (`defaulted`). Inference replaces those, so a BMM schema added to the repo later is picked up as a new guess; a value the user confirmed is kept. - A value that fails its `pattern` is reported, not guessed: `SPEC{{component}}` is invalid for @@ -131,6 +131,9 @@ Surveyed on 2026-10-07 across the `specifications-*` clones (18 with a git direc - AsciiDoc specification repos only, with an optional BMM schema. An OpenAPI repo (`specifications-ITS-REST`, built with `make`) needs its own variant of `AGENTS.md`; until then, pin `agents`, `claude-md` and `asciidoctorconfig` there. +- One BMM dependency. The class-table command in `AGENTS.md` loads only BASE with `-d`, from the sibling + clone. A component whose classes also refer to another component (AM 2.x to LANG expression classes, for + example) needs further `-d` options, added by hand. - One BMM schema per repo. When several are found (AM has 1.4.0 and 2.4.0, LANG has three) the highest version is used and a warning lists the others; pass `--var bmm_schema_id=` to choose. - `json-merge` rewrites the file with two-space indentation when it adds something, and any rewrite drops diff --git a/plugins/openehr-specs/skills/scaffold/scripts/scaffold.py b/plugins/openehr-specs/skills/scaffold/scripts/scaffold.py index b2fbce1..56497bf 100644 --- a/plugins/openehr-specs/skills/scaffold/scripts/scaffold.py +++ b/plugins/openehr-specs/skills/scaffold/scripts/scaffold.py @@ -298,6 +298,18 @@ def put(name, value, where): if len(bmm) > 1: notes.append(f"several BMM schemas in computable/BMM ({', '.join(bmm)}): using {bmm[-1]}; " "pass --var bmm_schema_id= to choose another") + # the classes of every component but BASE refer to BASE types, which bmm-publisher only + # links when the BASE BMM is loaded as a dependency (-d); read it from the sibling clone + if not bmm[-1].startswith("openehr_base_"): + base_dir = repo.resolve().parent / "specifications-BASE" / "computable" / "BMM" + base = sorted((p.name[: -len(".bmm.json")] for p in base_dir.glob("openehr_base_*.bmm.json")), + key=version_key) if base_dir.is_dir() else [] + if base: + put("base_bmm_schema_id", base[-1], "sibling specifications-BASE") + else: + notes.append("no sibling specifications-BASE clone with a BMM schema: the class-table command in " + "AGENTS.md will not load BASE with -d, so links to BASE types break; clone it, or " + "pass --var base_bmm_schema_id=") head = git(repo, "symbolic-ref", "--short", "refs/remotes/origin/HEAD") if head: @@ -891,6 +903,7 @@ def sample_values(tset, bmm): if spec.get("required"): explicit[name] = "EXAMPLE" if name == "component" else "Example Title" explicit["bmm_schema_id"] = "openehr_example_1.0.0" if bmm else "" + explicit["base_bmm_schema_id"] = "openehr_base_1.0.0" if bmm else "" return resolve_variables(tset, explicit, {}, {}) diff --git a/scripts/test_scaffold.py b/scripts/test_scaffold.py index 144256d..813b455 100644 --- a/scripts/test_scaffold.py +++ b/scripts/test_scaffold.py @@ -59,6 +59,13 @@ def put(self, name, text): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") + def base_clone(self, *schema_ids): + """A sibling specifications-BASE clone holding the given BMM schema ids.""" + bmm = self.root / "specifications-BASE" / "computable" / "BMM" + bmm.mkdir(parents=True, exist_ok=True) + for schema_id in schema_ids: + (bmm / f"{schema_id}.bmm.json").write_text("{}", encoding="utf-8") + def copy_set(self): dst = self.root / f"set{len(list(self.root.glob('set*')))}" shutil.copytree(scaffold.SET_DIR, dst, ignore=shutil.ignore_patterns("__pycache__")) @@ -130,7 +137,8 @@ def test_descriptor_records_revision_variables_and_hashes(self): self.apply() desc = json.loads(self.text(".claude/scaffold.json")) self.assertEqual(list(desc)[:2], ["template_set", "revision"]) - self.assertEqual(desc["revision"], 1) + latest = json.loads((self.set_dir / "assets/template-set.json").read_text(encoding="utf-8"))["revision"] + self.assertEqual(desc["revision"], latest) self.assertEqual(desc["variables"]["component"], "DEMO") self.assertNotIn("license_name", desc["variables"]) # derived, not stored self.assertEqual(set(desc["files"]["agents"]["regions"]), {"plugin", "build", "conventions"}) @@ -144,6 +152,33 @@ def test_bmm_switches_on_the_class_generation_sections(self): self.assertIn("## Gotchas", agents) self.assertIn("and BMM sources", self.text(".claude/CLAUDE.md")) + def test_a_base_dependency_is_loaded_with_d_in_the_class_table_command(self): + self.apply(bmm_schema_id="openehr_demo_1.0.0", base_bmm_schema_id="openehr_base_1.3.0") + agents = self.text("AGENTS.md") + self.assertIn(' -v "$PWD/../specifications-BASE/computable/BMM/openehr_base_1.3.0.bmm.json"' + ':/in/openehr_base_1.3.0.bmm.json:ro \\\n', agents) + self.assertIn(" ghcr.io/openehr/bmm-publisher legacy-adoc \\\n" + " -d /in/openehr_base_1.3.0.bmm.json \\\n" + " /in/openehr_demo_1.0.0.bmm.json -o /out\n", agents) + + def test_without_a_base_dependency_the_command_has_no_d_option(self): + self.apply(bmm_schema_id="openehr_demo_1.0.0") + agents = self.text("AGENTS.md") + self.assertNotIn("-d /in/", agents) + self.assertNotIn("specifications-BASE/computable", agents) + self.assertIn(" ghcr.io/openehr/bmm-publisher legacy-adoc \\\n" + " /in/openehr_demo_1.0.0.bmm.json -o /out\n", agents) + + def test_agents_md_asks_for_the_repos_own_tooling_instead_of_denying_it(self): + self.apply() + agents = self.text("AGENTS.md") + self.assertNotIn("No build tooling lives in this repo", agents) + self.assertIn("TODO(scaffold): if this repo has its own tooling", agents) + + def test_claude_md_points_repo_guidance_to_agents_md(self): + self.apply() + self.assertIn("outside the `openehr-scaffold` regions", self.text(".claude/CLAUDE.md")) + def test_without_bmm_those_sections_are_absent(self): self.apply() agents = self.text("AGENTS.md") @@ -200,6 +235,31 @@ def test_bmm_schema_is_found(self): self.put("computable/BMM/openehr_demo_1.2.0.bmm.json", "{}") self.assertEqual(self.plan().values["bmm_schema_id"], "openehr_demo_1.2.0") + def test_base_bmm_comes_from_the_sibling_base_clone_highest_version_first(self): + self.put("computable/BMM/openehr_demo_1.0.0.bmm.json", "{}") + self.base_clone("openehr_base_1.2.0", "openehr_base_1.10.0", "openehr_base_1.3.0") + plan = self.plan() + self.assertEqual(plan.values["base_bmm_schema_id"], "openehr_base_1.10.0") + self.assertTrue(plan.sources["base_bmm_schema_id"].startswith("inferred")) + + def test_base_itself_gets_no_base_dependency(self): + repo = self.root / "specifications-BASE" + (repo / "computable/BMM").mkdir(parents=True) + (repo / "computable/BMM/openehr_base_1.3.0.bmm.json").write_text("{}", encoding="utf-8") + plan = self.plan(repo) + self.assertEqual(plan.values["base_bmm_schema_id"], "") + self.assertFalse(any("specifications-BASE" in w for w in plan.warnings)) + + def test_a_repo_without_bmm_gets_no_base_dependency(self): + self.base_clone("openehr_base_1.3.0") + self.assertEqual(self.plan().values["base_bmm_schema_id"], "") + + def test_a_missing_base_clone_is_reported(self): + self.put("computable/BMM/openehr_demo_1.0.0.bmm.json", "{}") + plan = self.plan() + self.assertEqual(plan.values["base_bmm_schema_id"], "") + self.assertTrue(any("specifications-BASE" in w for w in plan.warnings), plan.warnings) + def test_licence_family_is_recognised_from_the_text(self): text = (self.set_dir / "assets/templates/license-apache-2.0.txt").read_text(encoding="utf-8") self.put("LICENSE", text) @@ -378,39 +438,39 @@ def test_a_template_change_updates_untouched_regions_and_reports_edited_ones(sel self.apply() self.put("AGENTS.md", self.text("AGENTS.md").replace("# AGENTS.md", "# AGENTS.md\n\nMy intro.")) new_set = self.copy_set() - self.bump(new_set, edit=self.edit_conventions) + rev = self.bump(new_set, edit=self.edit_conventions) self.set_dir = new_set plan = self.plan() - self.assertEqual((plan.mode, plan.path), ("upgrade", [2])) + self.assertEqual((plan.mode, plan.path), ("upgrade", [rev])) self.assertEqual(self.actions(plan)["AGENTS.md"], "update") self.apply() agents = self.text("AGENTS.md") self.assertIn("a single line, for example", agents) self.assertIn("My intro.", agents) - self.assertEqual(json.loads(self.text(".claude/scaffold.json"))["revision"], 2) + self.assertEqual(json.loads(self.text(".claude/scaffold.json"))["revision"], rev) self.assertEqual(self.plan().mode, "current") def test_a_template_change_conflicts_with_a_locally_edited_region(self): self.apply() self.put("AGENTS.md", self.text("AGENTS.md").replace("understood, but use the leading form", "tolerated")) new_set = self.copy_set() - self.bump(new_set, edit=self.edit_conventions) + rev = self.bump(new_set, edit=self.edit_conventions) self.set_dir = new_set agents = next(a for a in self.plan().acts if a.id == "agents") self.assertEqual(agents.action, "conflict") self.apply() self.assertIn("tolerated", self.text("AGENTS.md")) # kept - self.assertEqual(self.plan().recorded, 2) + self.assertEqual(self.plan().recorded, rev) def test_the_path_chains_every_revision_between_recorded_and_latest(self): self.apply() new_set = self.copy_set() - self.bump(new_set, steps=[{"op": "note", "text": "two"}]) - self.bump(new_set, steps=[{"op": "note", "text": "three"}]) + first = self.bump(new_set, steps=[{"op": "note", "text": "two"}]) + second = self.bump(new_set, steps=[{"op": "note", "text": "three"}]) self.set_dir = new_set plan = self.plan() - self.assertEqual(plan.path, [2, 3]) - self.assertEqual([(s["revision"], s["text"]) for s in plan.steps], [(2, "two"), (3, "three")]) + self.assertEqual(plan.path, [first, second]) + self.assertEqual([(s["revision"], s["text"]) for s in plan.steps], [(first, "two"), (second, "three")]) def test_rename_moves_the_file_and_keeps_its_recorded_hash(self): self.apply() @@ -447,8 +507,8 @@ def test_delete_only_removes_a_file_that_is_unmodified(self): def test_a_missing_migration_file_is_an_error(self): self.apply() new_set = self.copy_set() - self.bump(new_set) - (new_set / "migrations/0002.json").unlink() + rev = self.bump(new_set) + (new_set / f"migrations/{rev:04d}.json").unlink() self.set_dir = new_set with self.assertRaises(scaffold.ScaffoldError): self.plan() @@ -493,9 +553,9 @@ def test_sealing_after_a_revision_bump_makes_it_sound_again(self): def test_a_revision_without_its_migration_file_is_flagged(self): dst = self.copy_set() - self.bump(dst) - (dst / "migrations/0002.json").unlink() - self.assertTrue(any("0002.json" in p for p in scaffold.check_template_set(dst))) + rev = self.bump(dst) + (dst / f"migrations/{rev:04d}.json").unlink() + self.assertTrue(any(f"{rev:04d}.json" in p for p in scaffold.check_template_set(dst))) def test_a_missing_template_and_a_broken_template_are_flagged(self): dst = self.copy_set() @@ -562,7 +622,7 @@ def test_a_conflicting_region_does_not_block_a_clean_one_in_the_same_file(self): def edit(dst): path = dst / "assets/templates/agents-md.tmpl" text = path.read_text(encoding="utf-8") - text = text.replace("Docker is the only prerequisite", "Docker is the sole prerequisite") + text = text.replace("Docker is all you need", "Docker is all it takes") text = text.replace("one line, for example", "a single line, for example") path.write_text(text, encoding="utf-8") @@ -575,7 +635,7 @@ def edit(dst): self.assertEqual((agents.action, regions["build"], regions["conventions"]), ("conflict", "update", "conflict")) self.apply() text = self.text("AGENTS.md") - self.assertIn("sole prerequisite", text) # clean region written + self.assertIn("Docker is all it takes", text) # clean region written self.assertIn("tolerated", text) # conflicting region kept self.assertNotIn("a single line, for example", text) @@ -596,6 +656,7 @@ def test_bmm_schemas_are_ordered_by_version_not_by_text(self): def test_a_single_bmm_schema_gives_no_warning(self): self.put("computable/BMM/openehr_demo_1.0.0.bmm.json", "{}") + self.base_clone("openehr_base_1.3.0") # without it, the missing BASE dependency is reported self.assertFalse(any("BMM" in w for w in self.plan().warnings)) def test_git_warnings_distinguish_the_cases(self): From b2a84a70adea85a3ece38b819e74a6e377296a6e Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 8 Oct 2026 14:52:35 +0200 Subject: [PATCH 3/4] docs(openehr-specs): cover changes without a ticket in amendment-record No invented or placeholder key; offer to raise a ticket; otherwise an entry without a Jira reference (266 of 974 existing entries have none); no entry when the author says none is needed. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../openehr-specs/skills/amendment-record/SKILL.md | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af0eeb..c2e6998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Notable changes to the plugins in this repository. The format follows [Keep a Ch - `class-generation` and `regen-classes` generate from the component repo's own BMM file and its sibling dependencies, mounted under `/in/` and passed by path. A bare schema id loads the copy bundled in the `bmm-publisher` image, which lags the repos and rendered an older model without any error; mounting a directory over `/app/resources` hid the bundled dependencies. `regen-classes` stops when a file is missing instead of falling back to an id. - `scaffold` template set revision 2: the `AGENTS.md` build region no longer says the repo has no build tooling, and its class-table command loads the sibling BASE BMM with `-d` (new variable `base_bmm_schema_id`, inferred from the `specifications-BASE` clone), so links to BASE types resolve. A new `AGENTS.md` gets a `TODO(scaffold)` marker for the repo's own tooling, and `.claude/CLAUDE.md` says repo guidance belongs in `AGENTS.md` outside the regions. Existing repos upgrade on the next `/openehr-specs:scaffold` run. - `scaffold` asks to confirm `first_release` only when it creates `manifest.json`, the one file that uses it, and its rules say where `/init`-style guidance goes. +- `amendment-record` covers changes without a Jira ticket: no invented or placeholder key, an offer to raise one, an entry without a reference otherwise, and none when the author says none is needed. ## openehr-specs 0.4.0 (2026-10-07) diff --git a/plugins/openehr-specs/skills/amendment-record/SKILL.md b/plugins/openehr-specs/skills/amendment-record/SKILL.md index e5bd951..5617037 100644 --- a/plugins/openehr-specs/skills/amendment-record/SKILL.md +++ b/plugins/openehr-specs/skills/amendment-record/SKILL.md @@ -93,6 +93,18 @@ Reference tickets as `{spec_tickets}/SPECXX-NNN[SPECXX-NNN^]` (the `{spec_ticket resolves to the site ticket URL). The full project-key table and CR↔PR linking phrasing are in `references/conventions.md`. +### No Ticket + +Entries without a Jira reference are established practice: 266 of the 974 entries across the +`specifications-*` repos carry none (surveyed 2026-10-08). When a change has no ticket: + +- Never invent a key or write a placeholder (`SPECXX-NNN`, `TBD`). +- Ask whether to raise one first: a problem report (`SPECPR`) for a defect, a change request in the + component's project for a change. If one is raised, reference it. +- Otherwise write the Details cell without a reference, in the same style as the other entries. +- Whether a change needs an entry at all is the author's call. When they say none is needed, for example + for a fix that restores published text lost by tooling, add none. + ## Version Numbering The issue number tracks the **document version**, independent of the component release version @@ -121,7 +133,7 @@ SPECRM-142 — add tags to LOCATABLE; addresses SPECPR-401 2. Choose the Issue number: bump patch, minor, or major per the table above, or reuse the top entry's number when the change ships in the same version increment (see "Grouping Multiple Changes" in `references/conventions.md`). If the right bump is unclear, state the assumed bump and proceed; the diff in step 8 lets the author correct it. Run `git -C status --short` and `git -C diff --stat` when the change is already in the working tree, so the entry describes what actually changed. 3. Add the new entry at the **top** of the table (after the header row) 4. Move `[[latest_issue]]` and `[[latest_issue_date]]` anchors to the new entry. When the version is reused, leave the Issue cell of the previous top entry blank. -5. Reference all relevant Jira CRs and PRs (see `references/conventions.md` for keys, phrasing, and detail-text style) +5. Reference all relevant Jira CRs and PRs (see `references/conventions.md` for keys, phrasing, and detail-text style); without a ticket, follow **No Ticket** above 6. If this is the first entry after a release, add a release boundary row directly below the new entry, labelled with the release just completed (the newest release with a date in the `releases` array of `manifest.json`; ask the author if unsure) 7. Use today's date as the completion date unless the author gives one (ask if uncertain; do not guess), in the format `dd Mon yyyy` (e.g., `17 Nov 2022`) 8. When invoked directly, show the diff of the record (`git -C diff -- `) and stop; do not publish or commit unless asked. Inside a larger flow (a release, an authoring task), return to that flow instead. From e7bd08ec1c5ffe4d11e572caab2d01cb29c3a4b9 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 8 Oct 2026 14:52:35 +0200 Subject: [PATCH 4/4] docs(openehr-specs): show the ITS-BMM expressions in the manifest example The example used the retired MagicDraw uml expressions; it now shows RM's ITS-BMM-RM url expression and ITS-BMM-BASE dependency. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../references/manifest-spec-entry.md | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e6998..a55dd28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Notable changes to the plugins in this repository. The format follows [Keep a Ch - `scaffold` template set revision 2: the `AGENTS.md` build region no longer says the repo has no build tooling, and its class-table command loads the sibling BASE BMM with `-d` (new variable `base_bmm_schema_id`, inferred from the `specifications-BASE` clone), so links to BASE types resolve. A new `AGENTS.md` gets a `TODO(scaffold)` marker for the repo's own tooling, and `.claude/CLAUDE.md` says repo guidance belongs in `AGENTS.md` outside the regions. Existing repos upgrade on the next `/openehr-specs:scaffold` run. - `scaffold` asks to confirm `first_release` only when it creates `manifest.json`, the one file that uses it, and its rules say where `/init`-style guidance goes. - `amendment-record` covers changes without a Jira ticket: no invented or placeholder key, an offer to raise one, an entry without a reference otherwise, and none when the author says none is needed. +- `authoring` reference `manifest-spec-entry.md`: the expressions example shows the current `ITS-BMM-` url expression and `ITS-BMM-BASE` dependency (as in RM) instead of the retired MagicDraw `uml` expressions. ## openehr-specs 0.4.0 (2026-10-07) diff --git a/plugins/openehr-specs/skills/authoring/references/manifest-spec-entry.md b/plugins/openehr-specs/skills/authoring/references/manifest-spec-entry.md index 4d0d94f..0573101 100644 --- a/plugins/openehr-specs/skills/authoring/references/manifest-spec-entry.md +++ b/plugins/openehr-specs/skills/authoring/references/manifest-spec-entry.md @@ -50,16 +50,17 @@ Each component's `manifest.json` contains a `specifications` array. Here is the "specifications": [ ... ], "expressions": [ { - "id": "openEHR_UML-RM.mdzip", - "type": "uml", - "title": "RM UML", - "description": "UML file for MagicDraw 19. Contains UML 2.5 standard XMI file." + "id": "ITS-BMM-RM", + "type": "url", + "title": "RM BMM", + "description": "BMM files for openEHR Reference Model classes", + "link": "https://github.com/openEHR/specifications-ITS-BMM/tree/master/components/RM" }, { - "id": "openEHR_UML-BASE.mdzip", + "id": "ITS-BMM-BASE", "dependency": { "component": "BASE", - "release": "latest" + "release": "development" } } ], @@ -93,5 +94,6 @@ When creating a new release: ## Expressions Array Expressions are computable artifacts associated with the component: -- **`type: "uml"`** — UML model file. Historically a MagicDraw `.mdzip`; class tables and diagrams are now generated from the component's BMM schema by `bmm-publisher` (see the `class-generation` skill). -- **`dependency`** — references an expression from another component (inherited) +- **`type: "url"`** — a link to a computable artifact. The class model is published as BMM files in `specifications-ITS-BMM`: give it the id `ITS-BMM-`, the title ` BMM` and a link to `.../specifications-ITS-BMM/tree/master/components/`, as RM and BASE do. +- **`dependency`** — references another component's expression by id (for example `ITS-BMM-BASE` in a component whose classes use BASE types), with that component and its release. +- **`type: "uml"`** — legacy MagicDraw `.mdzip` model. Do not add one: class tables and diagrams are generated from the component's BMM by `bmm-publisher` (see the `class-generation` skill).