From 84a40ed0f288901c6ae8397d07da20a0b263bf20 Mon Sep 17 00:00:00 2001 From: Gerald Fruhmann Date: Sat, 4 Jul 2026 20:27:47 +0200 Subject: [PATCH] chore: package as installable CLI + complete docs for first real deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Packaging: - Add [build-system] hatchling + tool.uv.package=true so qms-kit is a proper pip-installable CLI (was skipped before, entry point ignored) - hatchling added to dev dependencies Docs: - readme: fix docker-compose path, replace python -m cli with qms-kit, full 6-step quickstart, Phase 1 marked complete - docs/configuration.md: rewrite from TODO stub to full reference — all Pydantic model keys documented with types and descriptions, tracker_mapping example, validation error guide - docs/architecture.md: fix CLI invocation to qms-kit deploy - .markdownlint.json: disable MD033/MD060/MD013 (inline HTML and table pipe style are intentional in this repo) Co-Authored-By: Claude Sonnet 4.6 --- .markdownlint.json | 6 ++ docs/architecture.md | 2 +- docs/configuration.md | 178 ++++++++++++++++++++++++++++++++++-------- pyproject.toml | 11 +++ readme.md | 59 ++++++++------ uv.lock | 28 ++++++- 6 files changed, 223 insertions(+), 61 deletions(-) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..fb16fb9 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,6 @@ +{ + "default": true, + "MD033": false, + "MD060": false, + "MD013": false +} diff --git a/docs/architecture.md b/docs/architecture.md index 7a7034e..610f995 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -58,7 +58,7 @@ Running `deploy` twice produces the same result without duplicates. ### CLI layer -Single entry point (`python -m cli deploy --target --config `). +Single entry point (`qms-kit deploy --target --config `). Orchestrates: load config → merge overlay → render templates → call adapter. ## Self-hosted Stack diff --git a/docs/configuration.md b/docs/configuration.md index a608d46..daed269 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,49 +1,159 @@ # Configuration Reference -> TODO: Full reference will be written once the core config loader (Pydantic schema) is complete. +Configuration is split into two layers that are deep-merged at runtime (overlay wins on conflict). -## Overview - -Configuration is split into two layers: +## Files | File | Purpose | |------|---------| -| `config/core.yaml` | Tool-neutral QMS substance — clauses, documents, roles, records, KPIs | -| `config/.yaml` | Tool overlay template — copy and fill per client | -| `config/clients/.yaml` | Per-client filled overlay (gitignored, not in this repo) | +| `config/core.yaml` | Tool-neutral QMS substance — clauses, documents, roles, records, KPIs, CAPA states | +| `config/selfhosted.yaml` | Self-hosted overlay template (copy per client) | +| `config/clients/.yaml` | Per-client filled overlay (gitignored — never committed) | +| `config/clients/example.yaml` | Reference template showing all available keys | + +## Core config keys (`config/core.yaml`) + +These keys define the QMS substance and are shared across all targets. + +### `meta` + +| Key | Type | Description | +|-----|------|-------------| +| `meta.version` | string | Config schema version | +| `meta.standard` | string | QMS standard (e.g. `ISO 9001:2015`) | + +### `organisation` + +| Key | Type | Description | +|-----|------|-------------| +| `organisation.name` | string | Full legal name of the client organisation | +| `organisation.short` | string | Short identifier used in document IDs | +| `organisation.quality_officer` | string | Full name of the QMO | +| `organisation.management` | string | Full name of the approving manager | + +### `clauses` + +List of ISO 9001 clause objects (4–10). Each clause has: + +| Key | Type | Description | +|-----|------|-------------| +| `id` | string | Clause number (e.g. `"4"`, `"7.5"`) | +| `title` | string | Clause title | +| `documents` | list[string] | Document IDs that belong to this clause | + +### `documents` + +List of document definitions. Each document has: + +| Key | Type | Description | +|-----|------|-------------| +| `id` | string | Unique document ID (e.g. `quality-policy`) | +| `title` | string | Human-readable title | +| `template` | string or null | Template filename without `.md.j2` extension; null = no template | +| `clause` | string | Parent clause ID | + +### `record_types` + +List of record type definitions (NC, CAPA, Internal Audit, KPI). Each has: + +| Key | Type | Description | +|-----|------|-------------| +| `id` | string | Record type ID (e.g. `nc`, `capa`) | +| `label` | string | Human-readable label | +| `clause` | string | ISO clause this record type belongs to | +| `fields` | list[string] | Custom fields required on this record | + +### `kpis` + +List of KPI definitions. Each has: + +| Key | Type | Description | +|-----|------|-------------| +| `id` | string | KPI identifier | +| `label` | string | Human-readable name | +| `unit` | string | Measurement unit (e.g. `%`, `days`) | +| `target` | string | Target value | +| `frequency` | string | Measurement frequency | +| `clause` | string | ISO clause | + +### `capa_states` + +CAPA state machine definition. Each state has: + +| Key | Type | Description | +|-----|------|-------------| +| `id` | string | State identifier | +| `label` | string | Display name | +| `transitions` | list[string] | Valid next state IDs | + +--- + +## Overlay keys + +### Common + +| Key | Type | Description | +|-----|------|-------------| +| `target` | string | `selfhosted` / `atlassian` / `m365` | +| `organisation.*` | — | Overrides any `organisation` key from core | + +### Self-hosted (`selfhosted:`) + +#### XWiki (`selfhosted.xwiki`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `base_url` | string | — | XWiki instance URL (e.g. `https://wiki.acme.example`) | +| `space_key` | string | `QMS` | XWiki space key for the QMS | +| `username` | string | — | XWiki admin username | +| `parent_page` | string | `QM Manual` | Parent page for all QMS pages | + +Password via env var: `XWIKI_PASSWORD` + +#### Redmine (`selfhosted.redmine`) + +| Key | Type | Description | +|-----|------|-------------| +| `base_url` | string | Redmine instance URL (e.g. `https://redmine.acme.example`) | +| `project_key` | string | Redmine project identifier (e.g. `qms-acme`) | +| `tracker_mapping` | map | Maps record type IDs to Redmine tracker names | + +API key via env var: `REDMINE_API_KEY` + +**Tracker mapping example:** + +```yaml +tracker_mapping: + nc: "Nonconformity" # must match the tracker name in Redmine exactly + capa: "CAPA" + audit: "Internal Audit" + kpi: "KPI Measurement" +``` -## Core Config Keys +> **API limitation (verified):** Redmine tracker and custom field *definitions* cannot be created +> via the API. Run the one-time manual setup first — see [redmine-setup.md](redmine-setup.md). -See [`config/core.yaml`](../config/core.yaml) — all keys are documented inline. +--- -## Overlay Keys +## Environment variables -| Key | Description | -|-----|-------------| -| `target` | `selfhosted` / `atlassian` / `m365` | -| `organisation.name` | Full legal name of the client organisation | -| `organisation.short` | Short identifier used in document IDs | -| `organisation.quality_officer` | Full name of the QMO | -| `organisation.management` | Full name of the approving manager | +Secrets are never stored in config files. Pass them as environment variables before running `qms-kit deploy`. -### Self-hosted specific +| Variable | Required for | Description | +|----------|-------------|-------------| +| `XWIKI_PASSWORD` | Self-hosted | XWiki admin password | +| `REDMINE_API_KEY` | Self-hosted | Redmine API key (from My Account → API access key) | +| `CONFLUENCE_API_TOKEN` | Atlassian (Phase 2) | Confluence Cloud API token | +| `JIRA_API_TOKEN` | Atlassian (Phase 2) | Jira Cloud API token | +| `SHAREPOINT_CLIENT_SECRET` | M365 (Phase 3) | SharePoint Graph API client secret | -| Key | Description | -|-----|-------------| -| `xwiki.base_url` | XWiki instance URL | -| `xwiki.space_key` | XWiki space key for the QMS (default: `QMS`) | -| `redmine.base_url` | Redmine instance URL | -| `redmine.project_key` | Redmine project identifier | -| `redmine.tracker_mapping` | Maps record type IDs to Redmine tracker names | +--- -## Environment Variables +## Validation -Secrets are never stored in config files — pass them as environment variables: +The config is validated with Pydantic at load time. Common errors: -| Variable | Used by | -|----------|---------| -| `XWIKI_PASSWORD` | XWiki REST authentication | -| `REDMINE_API_KEY` | Redmine REST authentication | -| `CONFLUENCE_API_TOKEN` | Confluence REST authentication (Phase 2) | -| `JIRA_API_TOKEN` | Jira REST authentication (Phase 2) | -| `SHAREPOINT_CLIENT_SECRET` | SharePoint Graph API (Phase 3) | +- **Duplicate document IDs** → `ValueError: Duplicate document IDs: {...}` +- **Missing required field** → `ValidationError: Field required` +- **Missing selfhosted block** → `ValueError: selfhosted config block is required` +- **Unknown template variable** → `jinja2.UndefinedError: ... is undefined` diff --git a/pyproject.toml b/pyproject.toml index d329601..0f0b5cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,13 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src", "cli", "adapters"] + +[tool.uv] +package = true + [project] name = "qms-kit" version = "0.1.0" @@ -17,6 +27,7 @@ dev = [ "pytest-cov>=5.0", "mypy>=1.10", "bandit>=1.8", + "hatchling>=1.25", "types-pyyaml>=6.0", "types-requests>=2.31", ] diff --git a/readme.md b/readme.md index f15d8cf..ccf31f0 100644 --- a/readme.md +++ b/readme.md @@ -25,10 +25,9 @@ Fill in your organisation's content over the following two weeks. **qms-kit deploys the structure. You fill in the content.** -One command wires up spaces, document templates, issue trackers, approval workflows, and KPI -dashboards for ISO 9001 clauses 4–10 — in XWiki + Redmine (self-hosted), Confluence + Jira -(Atlassian), or SharePoint + Power Automate (M365). -Reuse the same kit for every client; swap the overlay config. +One command wires up spaces, document templates, issue trackers, and KPI dashboards for +ISO 9001 clauses 4–10 — in XWiki + Redmine (self-hosted), Confluence + Jira (Atlassian), +or SharePoint + Power Automate (M365). Reuse the same kit for every client; swap the overlay config. > ⚠️ **Scope:** qms-kit delivers a documentation scaffold for internal maturity and > customer/second-party audits. Accredited certification requires an additional operating phase @@ -41,8 +40,8 @@ Reuse the same kit for every client; swap the overlay config. - 📋 **ISO 9001 clauses 4–10 as structure** — every space, page, and tracker maps to a clause. - ⚙️ **Config-driven, idempotent** — declare your QMS in YAML; run deploy as often as needed. - 🔁 **Multi-tenant** — one core config, one overlay per client. No copy-paste between engagements. -- 🧩 **Adapter model** — self-hosted, Atlassian, and M365 are interchangeable modules; the core never changes. -- 📄 **Jinja2 templates** — policy, procedure, management review agenda, audit checklist, CAPA form — all with placeholders, ready to hand to your client. +- 🧩 **Adapter model** — self-hosted, Atlassian, and M365 are interchangeable modules. +- 📄 **Jinja2 templates** — quality policy, procedures, management review agenda, internal audit checklist, CAPA form — all with placeholders, ready to hand to your client. --- @@ -50,33 +49,44 @@ Reuse the same kit for every client; swap the overlay config. | Layer | What | |-------|------| -| **Structure** | Space / site / project, document tree, naming conventions | +| **Structure** | Space / project, document tree, naming conventions | | **Document templates** | Quality policy, procedures, MR agenda, internal audit checklist, CAPA/NC form | -| **Record containers** | Issue schemas for NC, CAPA, audits, objectives/KPIs | -| **Workflows** | Approval/review routing, CAPA escalation, review reminders | -| **Dashboards** | KPI and status overview | +| **Record containers** | Issue trackers for NC, CAPA, audits, KPIs | --- ## 🚀 Quickstart -> Self-hosted target (XWiki + Redmine via Docker Compose): +Self-hosted target (XWiki + Redmine via Docker Compose): ```bash -# 1. Copy and fill in your client overlay -cp config/selfhosted.yaml config/clients/acme.yaml -# edit acme.yaml: org name, XWiki URL, Redmine URL, credentials +# 1. Clone and install +git clone https://github.com/gerfru/qms-kit.git +cd qms-kit +uv sync -# 2. Start the stack (first time only — brings up XWiki + Redmine) -docker compose -f adapters/selfhosted/docker-compose.yml up -d +# 2. Start XWiki + Redmine +cp docker/.env.example docker/.env # fill in passwords +docker compose -f docker/docker-compose.yml up -d -# 3. Seed the QMS scaffold -python -m cli deploy --target selfhosted --config config/clients/acme.yaml +# 3. One-time Redmine setup (manual — API limitation) +# See docs/redmine-setup.md + +# 4. Create your client overlay +cp config/clients/example.yaml config/clients/acme.yaml +# edit acme.yaml: org name, URLs, credentials + +# 5. Dry-run first (no changes written) +export XWIKI_PASSWORD=Admin +export REDMINE_API_KEY= +qms-kit deploy --target selfhosted --config config/clients/acme.yaml --dry-run + +# 6. Deploy +qms-kit deploy --target selfhosted --config config/clients/acme.yaml ``` -> **Note on Redmine custom fields:** Field *definitions* (Root Cause, Effectiveness Check, Status) -> must be created once manually or via plugin — the Redmine API does not support creating them. -> The deployer seeds projects, issues, and field *values* automatically. +> **Redmine custom fields** (Root Cause, Effectiveness Check, etc.) must be created once +> manually — the Redmine API does not support creating field definitions. > See [docs/redmine-setup.md](docs/redmine-setup.md). → Full walkthrough: **[docs/setup.md](docs/setup.md)** @@ -87,7 +97,7 @@ python -m cli deploy --target selfhosted --config config/clients/acme.yaml | Target | Status | Stack | |--------|--------|-------| -| **Self-hosted** | ✅ Phase 1 | XWiki (docs) + Redmine (records) via Docker | +| **Self-hosted** | ✅ Phase 1 complete | XWiki (docs) + Redmine (records) via Docker | | **Atlassian** | 🔜 Phase 2 | Confluence + Jira via REST API | | **Microsoft 365** | 🔜 Phase 3 | SharePoint + Power Automate via PnP PowerShell | @@ -111,11 +121,10 @@ python -m cli deploy --target selfhosted --config config/clients/acme.yaml ## 🧱 Stack -Python · Jinja2 · YAML · Docker Compose · XWiki REST API · Redmine REST API +Python · Jinja2 · Pydantic · YAML · Click · Docker Compose · XWiki REST API · Redmine REST API Three layers above the adapters: a config loader with Pydantic schema validation, a Jinja2 -template renderer, and a single CLI entry point (`deploy --target`). -M365 adapter shells out to PnP PowerShell; everything else is pure Python. +template renderer, and a single CLI entry point (`qms-kit deploy --target`). --- diff --git a/uv.lock b/uv.lock index 9e2c912..f434f7c 100644 --- a/uv.lock +++ b/uv.lock @@ -243,6 +243,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, ] +[[package]] +name = "hatchling" +version = "1.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/4c/8717ccb844b4fa5a5ba6352e97d743ed24e9a22cf90b7c109c17030a46a1/hatchling-1.30.1.tar.gz", hash = "sha256:eee4fd45357f72ebb3d7a42e5d72cfb5e29ed426d79e8836288926c4258d5f2e", size = 56929, upload-time = "2026-06-02T00:09:41.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/49/2797ec0ef88008a653a8867bb8d1e5c223cd2df8e40390dd5c6a0279cbc5/hatchling-1.30.1-py3-none-any.whl", hash = "sha256:161eacafb3c6f91526e92116d21426369f2c36e98c36a864f11a96345ad4ee31", size = 77489, upload-time = "2026-06-02T00:09:40.139Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -677,7 +692,7 @@ wheels = [ [[package]] name = "qms-kit" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "click" }, { name = "jinja2" }, @@ -689,6 +704,7 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "bandit" }, + { name = "hatchling" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -700,6 +716,7 @@ dev = [ requires-dist = [ { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.8" }, { name = "click", specifier = ">=8.1" }, + { name = "hatchling", marker = "extra == 'dev'", specifier = ">=1.25" }, { name = "jinja2", specifier = ">=3.1" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" }, { name = "pydantic", specifier = ">=2.0" }, @@ -749,6 +766,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/8d/008761f6e1000600e5303db30d05724bdcf3d2d186cbb59fac79b52e39ed/stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7", size = 54463, upload-time = "2026-07-02T11:38:07.43Z" }, ] +[[package]] +name = "trove-classifiers" +version = "2026.6.1.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" }, +] + [[package]] name = "types-pyyaml" version = "6.0.12.20260518"